diff --git a/Directory.Packages.props b/Directory.Packages.props index 38a02aab77e..39c9d1897d6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -54,7 +54,7 @@ - + @@ -93,7 +93,7 @@ - + diff --git a/Orleans.sln b/Orleans.sln index 281379e1c27..802adea46c6 100644 --- a/Orleans.sln +++ b/Orleans.sln @@ -212,6 +212,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Orleans.GrainDirectory.Redi EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tester.Redis", "test\Extensions\Tester.Redis\Tester.Redis.csproj", "{F13247A0-70C9-4200-9CB1-2002CB8105E0}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Orleans.Serialization.Protobuf", "src\Serializers\Orleans.Serialization.Protobuf\Orleans.Serialization.Protobuf.csproj", "{A073C0EE-8732-42F9-A22E-D47034E25076}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -466,14 +468,6 @@ Global {16B9B850-ED3B-4B45-B0F2-3F802D44F382}.Debug|Any CPU.Build.0 = Debug|Any CPU {16B9B850-ED3B-4B45-B0F2-3F802D44F382}.Release|Any CPU.ActiveCfg = Release|Any CPU {16B9B850-ED3B-4B45-B0F2-3F802D44F382}.Release|Any CPU.Build.0 = Release|Any CPU - {CCEF897C-F4F8-48F0-8F95-CC1487EE2936}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CCEF897C-F4F8-48F0-8F95-CC1487EE2936}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CCEF897C-F4F8-48F0-8F95-CC1487EE2936}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CCEF897C-F4F8-48F0-8F95-CC1487EE2936}.Release|Any CPU.Build.0 = Release|Any CPU - {F13247A0-70C9-4200-9CB1-2002CB8105E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F13247A0-70C9-4200-9CB1-2002CB8105E0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F13247A0-70C9-4200-9CB1-2002CB8105E0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F13247A0-70C9-4200-9CB1-2002CB8105E0}.Release|Any CPU.Build.0 = Release|Any CPU {D1214CD3-EB99-4420-9E30-A50ACFD66A48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D1214CD3-EB99-4420-9E30-A50ACFD66A48}.Debug|Any CPU.Build.0 = Debug|Any CPU {D1214CD3-EB99-4420-9E30-A50ACFD66A48}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -562,6 +556,18 @@ Global {2268B639-02B8-4903-B719-65F7EBD05D52}.Debug|Any CPU.Build.0 = Debug|Any CPU {2268B639-02B8-4903-B719-65F7EBD05D52}.Release|Any CPU.ActiveCfg = Release|Any CPU {2268B639-02B8-4903-B719-65F7EBD05D52}.Release|Any CPU.Build.0 = Release|Any CPU + {CCEF897C-F4F8-48F0-8F95-CC1487EE2936}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CCEF897C-F4F8-48F0-8F95-CC1487EE2936}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CCEF897C-F4F8-48F0-8F95-CC1487EE2936}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CCEF897C-F4F8-48F0-8F95-CC1487EE2936}.Release|Any CPU.Build.0 = Release|Any CPU + {F13247A0-70C9-4200-9CB1-2002CB8105E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F13247A0-70C9-4200-9CB1-2002CB8105E0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F13247A0-70C9-4200-9CB1-2002CB8105E0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F13247A0-70C9-4200-9CB1-2002CB8105E0}.Release|Any CPU.Build.0 = Release|Any CPU + {A073C0EE-8732-42F9-A22E-D47034E25076}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A073C0EE-8732-42F9-A22E-D47034E25076}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A073C0EE-8732-42F9-A22E-D47034E25076}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A073C0EE-8732-42F9-A22E-D47034E25076}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -640,8 +646,6 @@ Global {8E01A6EB-DE96-4DFD-AA7D-B07078F12372} = {FE2E08C6-9C3B-4AEE-AE07-CCA387580D7A} {D53D80CC-3E14-4499-B03F-610A5D3F6359} = {A6573187-FD0D-4DF7-91D1-03E07E470C0A} {16B9B850-ED3B-4B45-B0F2-3F802D44F382} = {4C5D66BF-EE1C-4DD8-8551-D1B7F3768A34} - {CCEF897C-F4F8-48F0-8F95-CC1487EE2936} = {A734945A-36DC-485E-B84D-3C2D395BC7BE} - {F13247A0-70C9-4200-9CB1-2002CB8105E0} = {082D25DB-70CA-48F4-93E0-EC3455F494B8} {D1214CD3-EB99-4420-9E30-A50ACFD66A48} = {FE2E08C6-9C3B-4AEE-AE07-CCA387580D7A} {65D8F6B3-DEE2-412B-95F2-77274461D58C} = {4CD3AA9E-D937-48CA-BB6C-158E12257D23} {A145AFFC-E0CF-4861-AB0C-427C7670FF37} = {4CD3AA9E-D937-48CA-BB6C-158E12257D23} @@ -666,6 +670,9 @@ Global {671AE42C-974A-467B-BE89-0A3F706B5B21} = {A734945A-36DC-485E-B84D-3C2D395BC7BE} {28B35216-0C6E-4CF1-8C14-7D9A4BE161A5} = {A734945A-36DC-485E-B84D-3C2D395BC7BE} {2268B639-02B8-4903-B719-65F7EBD05D52} = {A734945A-36DC-485E-B84D-3C2D395BC7BE} + {CCEF897C-F4F8-48F0-8F95-CC1487EE2936} = {A734945A-36DC-485E-B84D-3C2D395BC7BE} + {F13247A0-70C9-4200-9CB1-2002CB8105E0} = {082D25DB-70CA-48F4-93E0-EC3455F494B8} + {A073C0EE-8732-42F9-A22E-D47034E25076} = {4CD3AA9E-D937-48CA-BB6C-158E12257D23} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7BFB3429-B5BB-4DB1-95B4-67D77A864952} diff --git a/src/Serializers/Directory.Build.props b/src/Serializers/Directory.Build.props deleted file mode 100644 index e28577bcfd1..00000000000 --- a/src/Serializers/Directory.Build.props +++ /dev/null @@ -1,29 +0,0 @@ - - - <_ParentDirectoryBuildPropsPath Condition="'$(_DirectoryBuildPropsFile)' != ''">$([System.IO.Path]::Combine('..', '$(_DirectoryBuildPropsFile)')) - - - - - - false - - - - true - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Serializers/Orleans.Serialization.Protobuf/Orleans.Serialization.Protobuf.csproj b/src/Serializers/Orleans.Serialization.Protobuf/Orleans.Serialization.Protobuf.csproj index 05e2c729e56..25862014110 100644 --- a/src/Serializers/Orleans.Serialization.Protobuf/Orleans.Serialization.Protobuf.csproj +++ b/src/Serializers/Orleans.Serialization.Protobuf/Orleans.Serialization.Protobuf.csproj @@ -1,15 +1,11 @@ - - Microsoft.Orleans.OrleansGoogleUtils - Microsoft Orleans Google Utilities - Library of utility types for Google of Microsoft Orleans. - $(PackageTags) ProtoBuf - $(DefaultTargetFrameworks) - - Orleans.Serialization.Protobuf - OrleansGoogleUtils + Microsoft.Orleans.Serialization.Protobuf + $(DefaultTargetFrameworks);netstandard2.1 + Google.Protobuf integration for Orleans.Serialization + true + false @@ -17,4 +13,8 @@ + + + + diff --git a/src/Serializers/Orleans.Serialization.Protobuf/ProtobufCodec.cs b/src/Serializers/Orleans.Serialization.Protobuf/ProtobufCodec.cs new file mode 100644 index 00000000000..2e0ebdc139b --- /dev/null +++ b/src/Serializers/Orleans.Serialization.Protobuf/ProtobufCodec.cs @@ -0,0 +1,219 @@ +using Google.Protobuf; +using Orleans.Serialization.Buffers; +using Orleans.Serialization.Buffers.Adaptors; +using Orleans.Serialization.Cloning; +using Orleans.Serialization.Codecs; +using Orleans.Serialization.Serializers; +using Orleans.Serialization.WireProtocol; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace Orleans.Serialization; + +[Alias(WellKnownAlias)] +public sealed class ProtobufCodec : IGeneralizedCodec, IGeneralizedCopier, ITypeFilter +{ + public const string WellKnownAlias = "protobuf"; + + private static readonly Type SelfType = typeof(ProtobufCodec); + private static readonly ConcurrentDictionary MessageParsers = new(); + + private readonly ICodecSelector[] _serializableTypeSelectors; + private readonly ICopierSelector[] _copyableTypeSelectors; + + /// + /// Initializes a new instance of the class. + /// + /// Filters used to indicate which types should be serialized by this codec. + /// Filters used to indicate which types should be copied by this codec. + public ProtobufCodec( + IEnumerable serializableTypeSelectors, + IEnumerable copyableTypeSelectors) + { + _serializableTypeSelectors = serializableTypeSelectors.Where(t => string.Equals(t.CodecName, WellKnownAlias, StringComparison.Ordinal)).ToArray(); + _copyableTypeSelectors = copyableTypeSelectors.Where(t => string.Equals(t.CopierName, WellKnownAlias, StringComparison.Ordinal)).ToArray(); + } + + /// + public object DeepCopy(object input, CopyContext context) + { + if (!context.TryGetCopy(input, out object result)) + { + if (input is not IMessage protobufMessage) + { + throw new InvalidOperationException("Input is not a protobuf message"); + } + + var messageSize = protobufMessage.CalculateSize(); + using var buffer = new PooledArrayBufferWriter(); + var spanBuffer = buffer.GetSpan(messageSize)[..messageSize]; + protobufMessage.WriteTo(spanBuffer); + + result = protobufMessage.Descriptor.Parser.ParseFrom(spanBuffer); + + context.RecordCopy(input, result); + } + + return result; + } + + /// + bool IGeneralizedCodec.IsSupportedType(Type type) + { + if (type == SelfType) + { + return true; + } + + foreach (var selector in _serializableTypeSelectors) + { + if (selector.IsSupportedType(type)) + { + return IsProtobufMessage(type); + } + } + + return false; + } + + /// + bool IGeneralizedCopier.IsSupportedType(Type type) + { + foreach (var selector in _copyableTypeSelectors) + { + if (selector.IsSupportedType(type)) + { + return IsProtobufMessage(type); + } + } + + return false; + } + + /// + bool? ITypeFilter.IsTypeAllowed(Type type) + { + if (!typeof(IMessage).IsAssignableFrom(type)) + { + return null; + } + + return ((IGeneralizedCodec)this).IsSupportedType(type) || ((IGeneralizedCopier)this).IsSupportedType(type); + } + + private static bool IsProtobufMessage(Type type) + { + if (!MessageParsers.ContainsKey(type.TypeHandle)) + { + if (Activator.CreateInstance(type) is not IMessage protobufMessageInstance) + { + return false; + } + + MessageParsers.TryAdd(type.TypeHandle, protobufMessageInstance.Descriptor.Parser); + } + + return true; + } + + /// + object IFieldCodec.ReadValue(ref Reader reader, Field field) + { + if (field.IsReference) + { + return ReferenceCodec.ReadReference(ref reader, field.FieldType); + } + + field.EnsureWireTypeTagDelimited(); + + var placeholderReferenceId = ReferenceCodec.CreateRecordPlaceholder(reader.Session); + object result = null; + Type type = null; + uint fieldId = 0; + + while (true) + { + var header = reader.ReadFieldHeader(); + if (header.IsEndBaseOrEndObject) + { + break; + } + + fieldId += header.FieldIdDelta; + switch (fieldId) + { + case 0: + ReferenceCodec.MarkValueField(reader.Session); + type = reader.Session.TypeCodec.ReadLengthPrefixed(ref reader); + break; + case 1: + if (type is null) + { + ThrowTypeFieldMissing(); + } + + if (!MessageParsers.TryGetValue(type.TypeHandle, out var messageParser)) + { + throw new ArgumentException($"No parser found for the expected type {type.Name}", nameof(TInput)); + } + + ReferenceCodec.MarkValueField(reader.Session); + var length = (int)reader.ReadVarUInt32(); + + using (var buffer = new PooledArrayBufferWriter()) + { + var spanBuffer = buffer.GetSpan(length)[..length]; + reader.ReadBytes(spanBuffer); + result = messageParser.ParseFrom(spanBuffer); + } + break; + default: + reader.ConsumeUnknownField(header); + break; + } + } + + ReferenceCodec.RecordObject(reader.Session, result, placeholderReferenceId); + return result; + } + + private static void ThrowTypeFieldMissing() => throw new RequiredFieldMissingException("Serialized value is missing its type field."); + + /// + void IFieldCodec.WriteField(ref Writer writer, uint fieldIdDelta, Type expectedType, object value) + { + if (ReferenceCodec.TryWriteReferenceField(ref writer, fieldIdDelta, expectedType, value)) + { + return; + } + + if (value is not IMessage protobufMessage) + { + throw new ArgumentException("The provided value for serialization in not an instance of IMessage"); + } + + writer.WriteFieldHeader(fieldIdDelta, expectedType, SelfType, WireType.TagDelimited); + + // Write the type name + ReferenceCodec.MarkValueField(writer.Session); + writer.WriteFieldHeaderExpected(0, WireType.LengthPrefixed); + writer.Session.TypeCodec.WriteLengthPrefixed(ref writer, value.GetType()); + + var messageSize = protobufMessage.CalculateSize(); + + using var buffer = new PooledArrayBufferWriter(); + var spanBuffer = buffer.GetSpan(messageSize)[..messageSize]; + + // Write the serialized payload + protobufMessage.WriteTo(spanBuffer); + + ReferenceCodec.MarkValueField(writer.Session); + writer.WriteFieldHeaderExpected(1, WireType.LengthPrefixed); + writer.WriteVarUInt32((uint)spanBuffer.Length); + writer.Write(spanBuffer); + + writer.WriteEndObject(); + } +} diff --git a/src/Serializers/Orleans.Serialization.Protobuf/ProtobufSerializer.cs b/src/Serializers/Orleans.Serialization.Protobuf/ProtobufSerializer.cs deleted file mode 100644 index 5824c72dfe6..00000000000 --- a/src/Serializers/Orleans.Serialization.Protobuf/ProtobufSerializer.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Reflection; -using Google.Protobuf; - -namespace Orleans.Serialization -{ - /// - /// An implementation of IExternalSerializer for usage with Protobuf types. - /// - public class ProtobufSerializer : IExternalSerializer - { - private static readonly ConcurrentDictionary Parsers = new ConcurrentDictionary(); - - /// - /// Determines whether this serializer has the ability to serialize a particular type. - /// - /// The type of the item to be serialized - /// A value indicating whether the type can be serialized - public bool IsSupportedType(Type itemType) - { - if (typeof(IMessage).IsAssignableFrom(itemType)) - { - if (!Parsers.ContainsKey(itemType.TypeHandle)) - { - var prop = itemType.GetProperty("Parser", BindingFlags.Public | BindingFlags.Static); - if (prop == null) - { - return false; - } - - var parser = prop.GetValue(null, null); - Parsers.TryAdd(itemType.TypeHandle, parser as MessageParser); - } - return true; - } - return false; - } - - /// - public object DeepCopy(object source, ICopyContext context) - { - if (source == null) - { - return null; - } - - dynamic dynamicSource = source; - return dynamicSource.Clone(); - } - - /// - public void Serialize(object item, ISerializationContext context, Type expectedType) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (item == null) - { - // Special handling for null value. - // Since in this ProtobufSerializer we are usually writing the data lengh as 4 bytes - // we also have to write the Null object as 4 bytes lengh of zero. - context.StreamWriter.Write(0); - return; - } - - IMessage iMessage = item as IMessage; - if (iMessage == null) - { - throw new ArgumentException("The provided item for serialization in not an instance of " + typeof(IMessage), "item"); - } - // The way we write the data is potentially in-efficinet, - // since we are first writing to ProtoBuff's internal CodedOutputStream - // and then take its internal byte[] and write it into out own BinaryTokenStreamWriter. - // Writing byte[] to BinaryTokenStreamWriter may sometimes copy the byte[] and sometimes just append ass ArraySegment without copy. - // In the former case it will be a secodnd copy. - // It would be more effecient to write directly into BinaryTokenStreamWriter - // but protobuff does not currently support writing directly into a given arbitary stream - // (it does support System.IO.Steam but BinaryTokenStreamWriter is not compatible with System.IO.Steam). - // Alternatively, we could force to always append to BinaryTokenStreamWriter, but that could create a lot of small ArraySegments. - // The plan is to ask the ProtoBuff team to add support for some "InputStream" interface, like Bond does. - byte[] outBytes = iMessage.ToByteArray(); - context.StreamWriter.Write(outBytes.Length); - context.StreamWriter.Write(outBytes); - } - - /// - public object Deserialize(Type expectedType, IDeserializationContext context) - { - if (expectedType == null) - { - throw new ArgumentNullException(nameof(expectedType)); - } - - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - var typeHandle = expectedType.TypeHandle; - MessageParser parser; - if (!Parsers.TryGetValue(typeHandle, out parser)) - { - throw new ArgumentException("No parser found for the expected type " + expectedType, nameof(expectedType)); - } - - var reader = context.StreamReader; - int length = reader.ReadInt(); - byte[] data = reader.ReadBytes(length); - - object message = parser.ParseFrom(data); - - return message; - } - } -} \ No newline at end of file diff --git a/src/Serializers/Orleans.Serialization.Protobuf/SerializationHostingExtensions.cs b/src/Serializers/Orleans.Serialization.Protobuf/SerializationHostingExtensions.cs new file mode 100644 index 00000000000..e2b2b536add --- /dev/null +++ b/src/Serializers/Orleans.Serialization.Protobuf/SerializationHostingExtensions.cs @@ -0,0 +1,70 @@ +using Google.Protobuf; +using Microsoft.Extensions.DependencyInjection; +using Orleans.Serialization.Cloning; +using Orleans.Serialization.Serializers; +using Orleans.Serialization.Utilities.Internal; +using System; + +namespace Orleans.Serialization; + +/// +/// Extension method for . +/// +public static class SerializationHostingExtensions +{ + private static readonly ServiceDescriptor ServiceDescriptor = new (typeof(ProtobufCodec), typeof(ProtobufCodec)); + + /// + /// Adds support for serializing and deserializing Protobuf IMessage types using . + /// + /// The serializer builder. + public static ISerializerBuilder AddProtobufSerializer( + this ISerializerBuilder serializerBuilder) + => serializerBuilder.AddProtobufSerializer( + isSerializable: type => typeof(IMessage).IsAssignableFrom(type), + isCopyable: type => typeof(IMessage).IsAssignableFrom(type)); + + /// + /// Adds support for serializing and deserializing Protobuf IMessage types using . + /// + /// The serializer builder. + /// A delegate used to indicate which types should be serialized by this codec. + /// A delegate used to indicate which types should be copied by this codec. + public static ISerializerBuilder AddProtobufSerializer( + this ISerializerBuilder serializerBuilder, + Func isSerializable, + Func isCopyable) + { + var services = serializerBuilder.Services; + + if (isSerializable != null) + { + services.AddSingleton(new DelegateCodecSelector + { + CodecName = ProtobufCodec.WellKnownAlias, + IsSupportedTypeDelegate = isSerializable + }); + } + + if (isCopyable != null) + { + services.AddSingleton(new DelegateCopierSelector + { + CopierName = ProtobufCodec.WellKnownAlias, + IsSupportedTypeDelegate = isCopyable + }); + } + + if (!services.Contains(ServiceDescriptor)) + { + services.AddSingleton(); + services.AddFromExisting(); + services.AddFromExisting(); + services.AddFromExisting(); + + serializerBuilder.Configure(options => options.WellKnownTypeAliases[ProtobufCodec.WellKnownAlias] = typeof(ProtobufCodec)); + } + + return serializerBuilder; + } +} \ No newline at end of file diff --git a/src/Serializers/Orleans.Serialization.ProtobufNet/Orleans.Serialization.ProtobufNet.csproj b/src/Serializers/Orleans.Serialization.ProtobufNet/Orleans.Serialization.ProtobufNet.csproj deleted file mode 100644 index ed77c006c16..00000000000 --- a/src/Serializers/Orleans.Serialization.ProtobufNet/Orleans.Serialization.ProtobufNet.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - Microsoft.Orleans.ProtobufNet - Microsoft Orleans ProtobufNet - Library of utility types for ProtobufNet of Microsoft Orleans. - $(PackageTags) ProtoBuf protobuf-net - $(DefaultTargetFrameworks) - - - - Orleans.Serialization.ProtobufNet - ProtobufNetUtils - - - - - - - diff --git a/src/Serializers/Orleans.Serialization.ProtobufNet/ProtobufNetSerializer.cs b/src/Serializers/Orleans.Serialization.ProtobufNet/ProtobufNetSerializer.cs deleted file mode 100644 index ee356096056..00000000000 --- a/src/Serializers/Orleans.Serialization.ProtobufNet/ProtobufNetSerializer.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using System.Buffers; -using System.Collections.Concurrent; -using System.IO; -using Orleans.Serialization.Cloning; -using Orleans.Serialization.Serializers; -using Orleans.Serialization.WireProtocol; - -namespace Orleans.Serialization.ProtobufNet -{ - /// - /// An implementation of IExternalSerializer for usage with Protobuf types, using the protobuf-net library. - /// - [WellKnownAlias("pb-net")] - public class ProtobufNetSerializer : IGeneralizedCodec, IGeneralizedCopier - { - private static readonly ConcurrentDictionary Cache = new(); - - /// - /// Determines whether this serializer has the ability to serialize a particular type. - /// - /// The type of the item to be serialized - /// A value indicating whether the type can be serialized - public bool IsSupportedType(Type itemType) - { - if (Cache.TryGetValue(itemType.TypeHandle, out var cacheItem)) - { - return cacheItem.IsSupported; - } - - return Cache.GetOrAdd(itemType.TypeHandle, new ProtobufTypeCacheItem(itemType)).IsSupported; - } - - public void WriteField(ref Buffers.Writer writer, uint fieldIdDelta, Type expectedType, object value) where TBufferWriter : IBufferWriter - { - - } - - public object ReadValue(ref Buffers.Reader reader, Field field) => throw new NotImplementedException(); - - /// - public void Serialize(object item, ISerializationContext context, Type expectedType) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (item == null) - { - // Special handling for null value. - // Since in this ProtobufSerializer we are usually writing the data lengh as 4 bytes - // we also have to write the Null object as 4 bytes lengh of zero. - context.StreamWriter.Write(0); - return; - } - - using (var stream = new MemoryStream()) - { - ProtoBuf.Serializer.Serialize(stream, item); - // The way we write the data is potentially in-efficinet, - // since we are first writing to ProtoBuff's internal CodedOutputStream - // and then take its internal byte[] and write it into out own BinaryTokenStreamWriter. - // Writing byte[] to BinaryTokenStreamWriter may sometimes copy the byte[] and sometimes just append ass ArraySegment without copy. - // In the former case it will be a secodnd copy. - // It would be more effecient to write directly into BinaryTokenStreamWriter - // but protobuff does not currently support writing directly into a given arbitary stream - // (it does support System.IO.Steam but BinaryTokenStreamWriter is not compatible with System.IO.Steam). - // Alternatively, we could force to always append to BinaryTokenStreamWriter, but that could create a lot of small ArraySegments. - // The plan is to ask the ProtoBuff team to add support for some "InputStream" interface, like Bond does. - byte[] outBytes = stream.ToArray(); - context.StreamWriter.Write(outBytes.Length); - context.StreamWriter.Write(outBytes); - } - } - - /// - public object Deserialize(Type expectedType, IDeserializationContext context) - { - if (expectedType == null) - { - throw new ArgumentNullException(nameof(expectedType)); - } - - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - var reader = context.StreamReader; - int length = reader.ReadInt(); - byte[] data = reader.ReadBytes(length); - - object message = null; - using (var stream = new MemoryStream(data)) - { - message = ProtoBuf.Serializer.Deserialize(expectedType, stream); - } - - return message; - } - - public object DeepCopy(object input, CopyContext context) - { - if (input == null) - { - return null; - } - - var cacheItem = Cache[input.GetType().TypeHandle]; - return cacheItem.IsImmutable ? input : ProtoBuf.Serializer.DeepClone((object)input); - } - } -} diff --git a/src/Serializers/Orleans.Serialization.ProtobufNet/ProtobufTypeCacheItem.cs b/src/Serializers/Orleans.Serialization.ProtobufNet/ProtobufTypeCacheItem.cs deleted file mode 100644 index 745949bf52e..00000000000 --- a/src/Serializers/Orleans.Serialization.ProtobufNet/ProtobufTypeCacheItem.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using ProtoBuf; - -namespace Orleans.Serialization.ProtobufNet -{ - public class ProtobufTypeCacheItem - { - public readonly bool IsSupported; - public readonly bool IsImmutable; - - public ProtobufTypeCacheItem(Type type) - { - IsSupported = type.IsDefined(typeof(ProtoContractAttribute), false); - IsImmutable = type.IsDefined(typeof(ImmutableAttribute), false); - } - } -} diff --git a/test/Orleans.Serialization.UnitTests/Orleans.Serialization.UnitTests.csproj b/test/Orleans.Serialization.UnitTests/Orleans.Serialization.UnitTests.csproj index 7ef2cde2337..c1bfc52eeac 100644 --- a/test/Orleans.Serialization.UnitTests/Orleans.Serialization.UnitTests.csproj +++ b/test/Orleans.Serialization.UnitTests/Orleans.Serialization.UnitTests.csproj @@ -1,4 +1,4 @@ - + true @@ -16,6 +16,8 @@ + + @@ -24,6 +26,11 @@ + + + + + \ No newline at end of file diff --git a/test/Orleans.Serialization.UnitTests/ProtobufSerializerTests.cs b/test/Orleans.Serialization.UnitTests/ProtobufSerializerTests.cs new file mode 100644 index 00000000000..0ae491fdbac --- /dev/null +++ b/test/Orleans.Serialization.UnitTests/ProtobufSerializerTests.cs @@ -0,0 +1,112 @@ +#nullable enable +using System; +using Google.Protobuf; +using Microsoft.Extensions.DependencyInjection; +using Orleans.Serialization.Cloning; +using Orleans.Serialization.Codecs; +using Orleans.Serialization.Serializers; +using Orleans.Serialization.TestKit; +using Xunit; +using Xunit.Abstractions; + +namespace Orleans.Serialization.UnitTests; + +[Trait("Category", "BVT")] +public class ProtobufSerializerTests : FieldCodecTester> +{ + public ProtobufSerializerTests(ITestOutputHelper output) : base(output) + { + } + + protected override void Configure(ISerializerBuilder builder) + { + builder.AddProtobufSerializer(); + } + + protected override MyProtobufClass? CreateValue() => new() { IntProperty = 30, StringProperty = "hello", SubClass = new MyProtobufClass.Types.SubClass { Id = Guid.NewGuid().ToByteString() } }; + + protected override MyProtobufClass?[] TestValues => new MyProtobufClass?[] + { + null, + new () { SubClass = new MyProtobufClass.Types.SubClass { Id = Guid.NewGuid().ToByteString() } }, + new () { IntProperty = 150, StringProperty = new string('c', 20), SubClass = new MyProtobufClass.Types.SubClass { Id = Guid.NewGuid().ToByteString() } }, + new () { IntProperty = -150_000, StringProperty = new string('c', 6_000), SubClass = new MyProtobufClass.Types.SubClass { Id = Guid.NewGuid().ToByteString() } }, + }; + + [Fact] + public void ProtobufSerializerDeepCopyTyped() + { + var original = new MyProtobufClass { IntProperty = 30, StringProperty = "hi", SubClass = new MyProtobufClass.Types.SubClass { Id = Guid.NewGuid().ToByteString() } }; + var copier = ServiceProvider.GetRequiredService>(); + var result = copier.Copy(original); + + Assert.Equal(original.IntProperty, result.IntProperty); + Assert.Equal(original.StringProperty, result.StringProperty); + } + + [Fact] + public void ProtobufSerializerDeepCopyUntyped() + { + var original = new MyProtobufClass { IntProperty = 30, StringProperty = "hi", SubClass = new MyProtobufClass.Types.SubClass { Id = Guid.NewGuid().ToByteString() } }; + var copier = ServiceProvider.GetRequiredService(); + var result = (MyProtobufClass)copier.Copy((object)original); + + Assert.Equal(original.IntProperty, result.IntProperty); + Assert.Equal(original.StringProperty, result.StringProperty); + } + + [Fact] + public void ProtobufSerializerRoundTripThroughCodec() + { + var original = new MyProtobufClass { IntProperty = 30, StringProperty = "hi", SubClass = new MyProtobufClass.Types.SubClass { Id = Guid.NewGuid().ToByteString() } }; + var result = RoundTripThroughCodec(original); + + Assert.Equal(original.IntProperty, result.IntProperty); + Assert.Equal(original.StringProperty, result.StringProperty); + } + + [Fact] + public void ProtobufSerializerRoundTripThroughUntypedSerializer() + { + var original = new MyProtobufClass { IntProperty = 30, StringProperty = "hi", SubClass = new MyProtobufClass.Types.SubClass { Id = Guid.NewGuid().ToByteString() } }; + var untypedResult = RoundTripThroughUntypedSerializer(original, out _); + + var result = Assert.IsType(untypedResult); + Assert.Equal(original.IntProperty, result.IntProperty); + Assert.Equal(original.StringProperty, result.StringProperty); + } +} + +[Trait("Category", "BVT")] +public class ProtobufCodecCopierTests : CopierTester> +{ + public ProtobufCodecCopierTests(ITestOutputHelper output) : base(output) + { + } + + protected override void Configure(ISerializerBuilder builder) + { + builder.AddProtobufSerializer(); + } + protected override IDeepCopier CreateCopier() => ServiceProvider.GetRequiredService().GetDeepCopier(); + + protected override MyProtobufClass? CreateValue() => new MyProtobufClass { IntProperty = 30, StringProperty = "hello", SubClass = new MyProtobufClass.Types.SubClass { Id = Guid.NewGuid().ToByteString() } }; + + protected override MyProtobufClass?[] TestValues => new MyProtobufClass?[] + { + null, + new () { SubClass = new MyProtobufClass.Types.SubClass { Id = Guid.NewGuid().ToByteString() } }, + new () { IntProperty = 150, StringProperty = new string('c', 20), SubClass = new MyProtobufClass.Types.SubClass { Id = Guid.NewGuid().ToByteString() } }, + new () { IntProperty = -150_000, StringProperty = new string('c', 6_000), SubClass = new MyProtobufClass.Types.SubClass { Id = Guid.NewGuid().ToByteString() } }, + }; +} + +public static class ProtobufGuidExtensions +{ + public static ByteString ToByteString(this Guid guid) + { + Span span = stackalloc byte[16]; + guid.TryWriteBytes(span); + return ByteString.CopyFrom(span); + } +} diff --git a/test/Orleans.Serialization.UnitTests/protobuf-model.proto b/test/Orleans.Serialization.UnitTests/protobuf-model.proto new file mode 100644 index 00000000000..3250ed7da8c --- /dev/null +++ b/test/Orleans.Serialization.UnitTests/protobuf-model.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; +option csharp_namespace = "Orleans.Serialization.UnitTests"; + +message MyProtobufClass { + int32 int_property = 1; + string string_property = 2; + SubClass sub_class = 3; + + message SubClass { + bytes id = 1; + } +}