diff --git a/src/MessagePack/Formatters/FrozenCollectionFormatters.cs b/src/MessagePack/Formatters/FrozenCollectionFormatters.cs new file mode 100644 index 000000000..f757b5597 --- /dev/null +++ b/src/MessagePack/Formatters/FrozenCollectionFormatters.cs @@ -0,0 +1,171 @@ +// Copyright (c) All contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#if NET8_0_OR_GREATER + +using System.Collections.Frozen; +using System.Collections.Generic; +using MessagePack.Formatters; + +#pragma warning disable SA1402 // File may only contain a single type +#pragma warning disable SA1649 // File name should match first type name + +namespace MessagePack.ImmutableCollection +{ + public sealed class FrozenDictionaryFormatter : IMessagePackFormatter?> + where TKey : notnull + { + private readonly IEqualityComparer? comparer; + + public FrozenDictionaryFormatter() + { + } + + public FrozenDictionaryFormatter(IEqualityComparer? comparer) + { + this.comparer = comparer; + } + + public void Serialize(ref MessagePackWriter writer, FrozenDictionary? value, MessagePackSerializerOptions options) + { + if (value is null) + { + writer.WriteNil(); + return; + } + + IFormatterResolver resolver = options.Resolver; + IMessagePackFormatter keyFormatter = resolver.GetFormatterWithVerify(); + IMessagePackFormatter valueFormatter = resolver.GetFormatterWithVerify(); + + // https://github.com/dotnet/runtime/blob/4c500699b938d53993b928b93543b8dbe68f69aa/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenHashTable.cs#L134C2-L134C2 + // FrozenDictionary.Count uses FrozenHashTable's Count property which is O(1). + var count = value.Count; + writer.WriteMapHeader(count); + if (count == 0) + { + return; + } + + foreach (KeyValuePair item in value) + { + writer.CancellationToken.ThrowIfCancellationRequested(); + keyFormatter.Serialize(ref writer, item.Key, options); + valueFormatter.Serialize(ref writer, item.Value, options); + } + } + + public FrozenDictionary? Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) + { + if (reader.TryReadNil()) + { + return default; + } + + var count = reader.ReadMapHeader(); + if (count == 0) + { + return FrozenDictionary.Empty; + } + + IFormatterResolver resolver = options.Resolver; + IMessagePackFormatter keyFormatter = resolver.GetFormatterWithVerify(); + IMessagePackFormatter valueFormatter = resolver.GetFormatterWithVerify(); + IEqualityComparer comparer = this.comparer ?? options.Security.GetEqualityComparer(); + + // https://github.com/dotnet/runtime/blob/4c500699b938d53993b928b93543b8dbe68f69aa/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenDictionary.cs#L87 + // FrozenDictionary.ToFrozenDictionary internally allocates Dictionary object. + var dictionary = new Dictionary(count, comparer); + options.Security.DepthStep(ref reader); + try + { + for (var i = 0; i < count; i++) + { + reader.CancellationToken.ThrowIfCancellationRequested(); + dictionary.Add(keyFormatter.Deserialize(ref reader, options), valueFormatter.Deserialize(ref reader, options)); + } + } + finally + { + reader.Depth--; + } + + return dictionary.ToFrozenDictionary(comparer); + } + } + + public sealed class FrozenSetFormatter : IMessagePackFormatter?> + { + private readonly IEqualityComparer? comparer; + + public FrozenSetFormatter() + { + } + + public FrozenSetFormatter(IEqualityComparer comparer) + { + this.comparer = comparer; + } + + public void Serialize(ref MessagePackWriter writer, FrozenSet? value, MessagePackSerializerOptions options) + { + if (value is null) + { + writer.WriteNil(); + return; + } + + var count = value.Count; + writer.WriteArrayHeader(count); + if (count == 0) + { + return; + } + + IMessagePackFormatter formatter = options.Resolver.GetFormatterWithVerify(); + foreach (var item in value) + { + writer.CancellationToken.ThrowIfCancellationRequested(); + formatter.Serialize(ref writer, item, options); + } + } + + public FrozenSet? Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) + { + if (reader.TryReadNil()) + { + return default; + } + + var count = reader.ReadArrayHeader(); + if (count == 0) + { + return FrozenSet.Empty; + } + + IMessagePackFormatter formatter = options.Resolver.GetFormatterWithVerify(); + IEqualityComparer comparer = this.comparer ?? options.Security.GetEqualityComparer(); + + // https://github.com/dotnet/runtime/blob/4c500699b938d53993b928b93543b8dbe68f69aa/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenSet.cs#L41 + // FrozenSet.ToFrozenSet internally allocates HashSet object. + var set = new HashSet(count, comparer); + options.Security.DepthStep(ref reader); + try + { + for (var i = 0; i < count; i++) + { + reader.CancellationToken.ThrowIfCancellationRequested(); + set.Add(formatter.Deserialize(ref reader, options)); + } + } + finally + { + reader.Depth--; + } + + return set.ToFrozenSet(comparer); + } + } +} + +#endif diff --git a/src/MessagePack/Resolvers/ImmutableCollectionResolver.cs b/src/MessagePack/Resolvers/ImmutableCollectionResolver.cs index 0f3faf285..de645990e 100644 --- a/src/MessagePack/Resolvers/ImmutableCollectionResolver.cs +++ b/src/MessagePack/Resolvers/ImmutableCollectionResolver.cs @@ -2,6 +2,9 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +#if NET8_0_OR_GREATER +using System.Collections.Frozen; +#endif using System.Collections.Generic; using System.Collections.Immutable; using System.Reflection; @@ -52,6 +55,10 @@ internal static class ImmutableCollectionGetFormatterHelper { typeof(IImmutableQueue<>), typeof(InterfaceImmutableQueueFormatter<>) }, { typeof(IImmutableSet<>), typeof(InterfaceImmutableSetFormatter<>) }, { typeof(IImmutableStack<>), typeof(InterfaceImmutableStackFormatter<>) }, +#if NET8_0_OR_GREATER + { typeof(FrozenDictionary<,>), typeof(FrozenDictionaryFormatter<,>) }, + { typeof(FrozenSet<>), typeof(FrozenSetFormatter<>) }, +#endif }; internal static object? GetFormatter(Type t) diff --git a/src/MessagePack/net8.0/PublicAPI.Unshipped.txt b/src/MessagePack/net8.0/PublicAPI.Unshipped.txt index d7903ded6..c3c792ffc 100644 --- a/src/MessagePack/net8.0/PublicAPI.Unshipped.txt +++ b/src/MessagePack/net8.0/PublicAPI.Unshipped.txt @@ -26,6 +26,16 @@ MessagePack.Formatters.Vector3Formatter.Serialize(ref MessagePack.MessagePackWri MessagePack.Formatters.Vector4Formatter MessagePack.Formatters.Vector4Formatter.Deserialize(ref MessagePack.MessagePackReader reader, MessagePack.MessagePackSerializerOptions! options) -> System.Numerics.Vector4 MessagePack.Formatters.Vector4Formatter.Serialize(ref MessagePack.MessagePackWriter writer, System.Numerics.Vector4 value, MessagePack.MessagePackSerializerOptions! options) -> void +MessagePack.ImmutableCollection.FrozenDictionaryFormatter +MessagePack.ImmutableCollection.FrozenDictionaryFormatter.FrozenDictionaryFormatter() -> void +MessagePack.ImmutableCollection.FrozenDictionaryFormatter.FrozenDictionaryFormatter(System.Collections.Generic.IEqualityComparer? comparer) -> void +MessagePack.ImmutableCollection.FrozenDictionaryFormatter.Serialize(ref MessagePack.MessagePackWriter writer, System.Collections.Frozen.FrozenDictionary? value, MessagePack.MessagePackSerializerOptions! options) -> void +MessagePack.ImmutableCollection.FrozenDictionaryFormatter.Deserialize(ref MessagePack.MessagePackReader reader, MessagePack.MessagePackSerializerOptions! options) -> System.Collections.Frozen.FrozenDictionary? +MessagePack.ImmutableCollection.FrozenSetFormatter +MessagePack.ImmutableCollection.FrozenSetFormatter.FrozenSetFormatter() -> void +MessagePack.ImmutableCollection.FrozenSetFormatter.FrozenSetFormatter(System.Collections.Generic.IEqualityComparer! comparer) -> void +MessagePack.ImmutableCollection.FrozenSetFormatter.Serialize(ref MessagePack.MessagePackWriter writer, System.Collections.Frozen.FrozenSet? value, MessagePack.MessagePackSerializerOptions! options) -> void +MessagePack.ImmutableCollection.FrozenSetFormatter.Deserialize(ref MessagePack.MessagePackReader reader, MessagePack.MessagePackSerializerOptions! options) -> System.Collections.Frozen.FrozenSet? MessagePack.MessagePackSerializerOptions.CompressionMinLength.get -> int MessagePack.MessagePackSerializerOptions.SuggestedContiguousMemorySize.get -> int MessagePack.MessagePackSerializerOptions.WithCompressionMinLength(int compressionMinLength) -> MessagePack.MessagePackSerializerOptions! diff --git a/tests/MessagePack.Tests/ExtensionTests/FrozenCollectionTest.cs b/tests/MessagePack.Tests/ExtensionTests/FrozenCollectionTest.cs new file mode 100644 index 000000000..b8a950a7e --- /dev/null +++ b/tests/MessagePack.Tests/ExtensionTests/FrozenCollectionTest.cs @@ -0,0 +1,116 @@ +// Copyright (c) All contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#if NET8_0_OR_GREATER +#nullable enable + +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Xunit; + +namespace MessagePack.Tests.ExtensionTests +{ + public class FrozenCollectionTest + { + private T Convert(T value) + { + MessagePackSerializerOptions options = MessagePackSerializerOptions.Standard; + return MessagePackSerializer.Deserialize(MessagePackSerializer.Serialize(value, options), options); + } + + [Fact] + public void EmptySet() + { + { + var empty = FrozenSet.Empty; + Convert(empty).IsStructuralEqual(empty); + } + + { + var empty = FrozenSet.Empty; + Convert(empty).IsStructuralEqual(empty); + } + + { + var empty = FrozenSet.Empty; + Convert(empty).IsStructuralEqual(empty); + } + + { + var empty = FrozenSet.Empty; + Convert(empty).IsStructuralEqual(empty); + } + } + + [Fact] + public void EmptyDictionary() + { + { + var empty = FrozenDictionary.Empty; + Convert(empty).IsStructuralEqual(empty); + } + + { + var empty = FrozenDictionary.Empty; + Convert(empty).IsStructuralEqual(empty); + } + + { + var empty = FrozenDictionary.Empty; + Convert(empty).IsStructuralEqual(empty); + } + + { + var empty = FrozenDictionary.Empty; + Convert(empty).IsStructuralEqual(empty); + } + + { + var empty = FrozenDictionary.Empty; + Convert(empty).IsStructuralEqual(empty); + } + + { + var empty = FrozenDictionary.Empty; + Convert(empty).IsStructuralEqual(empty); + } + + { + var empty = FrozenDictionary.Empty; + Convert(empty).IsStructuralEqual(empty); + } + + { + var empty = FrozenDictionary.Empty; + Convert(empty).IsStructuralEqual(empty); + } + } + + [Fact] + public void IntSet() + { + for (var i = 1; i < 11; i++) + { + var array = new int[1 << i]; + Random.Shared.NextBytes(MemoryMarshal.AsBytes(array)); + var set = array.ToFrozenSet(); + Convert(set).IsStructuralEqualIgnoreCollectionOrder(set); + } + } + + [Fact] + public void IntDictionary() + { + for (var i = 1; i < 11; i++) + { + var array = new KeyValuePair[1 << i]; + Random.Shared.NextBytes(MemoryMarshal.AsBytes>(array)); + var dictionary = array.ToFrozenDictionary(); + Convert(dictionary).IsStructuralEqualIgnoreCollectionOrder(dictionary); + } + } + } +} +#endif