diff --git a/Directory.Packages.props b/Directory.Packages.props index a84cc1e8590..192a57a583e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -33,6 +33,7 @@ + diff --git a/Orleans.slnx b/Orleans.slnx index 3a3e39ca595..3c4b5275e7d 100644 --- a/Orleans.slnx +++ b/Orleans.slnx @@ -42,6 +42,7 @@ + diff --git a/src/Orleans.Serialization.MemoryPack/MemoryPackCodec.cs b/src/Orleans.Serialization.MemoryPack/MemoryPackCodec.cs new file mode 100644 index 00000000000..53c870be287 --- /dev/null +++ b/src/Orleans.Serialization.MemoryPack/MemoryPackCodec.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Concurrent; +using System.Reflection; +using MemoryPack; +using Microsoft.Extensions.Options; +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; + +namespace Orleans.Serialization; + +/// +/// A serialization codec which uses . +/// +/// +/// MemoryPack codec performs slightly worse than default Orleans serializer, if performance is critical for your application, consider using default serialization. +/// +[Alias(WellKnownAlias)] +public class MemoryPackCodec : IGeneralizedCodec, IGeneralizedCopier, ITypeFilter +{ + private static readonly ConcurrentDictionary SupportedTypes = new(); + private static readonly Type SelfType = typeof(MemoryPackCodec); + + private readonly ICodecSelector[] _serializableTypeSelectors; + private readonly ICopierSelector[] _copyableTypeSelectors; + private readonly MemoryPackCodecOptions _options; + + /// + /// The well-known type alias for this codec. + /// + public const string WellKnownAlias = "memorypack"; + + /// + /// 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. + /// The MemoryPack codec options. + public MemoryPackCodec( + IEnumerable serializableTypeSelectors, + IEnumerable copyableTypeSelectors, + IOptions options) + { + _serializableTypeSelectors = serializableTypeSelectors.Where(t => string.Equals(t.CodecName, WellKnownAlias, StringComparison.Ordinal)).ToArray(); + _copyableTypeSelectors = copyableTypeSelectors.Where(t => string.Equals(t.CopierName, WellKnownAlias, StringComparison.Ordinal)).ToArray(); + _options = options.Value; + } + + /// + void IFieldCodec.WriteField(ref Writer writer, uint fieldIdDelta, Type expectedType, object value) + { + if (ReferenceCodec.TryWriteReferenceField(ref writer, fieldIdDelta, expectedType, value)) + { + return; + } + + // The schema type when serializing the field is the type of the codec. + 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 bufferWriter = new BufferWriterBox(new()); + try + { + MemoryPackSerializer.Serialize(value.GetType(), bufferWriter, value, _options.SerializerOptions); + + ReferenceCodec.MarkValueField(writer.Session); + writer.WriteFieldHeaderExpected(1, WireType.LengthPrefixed); + writer.WriteVarUInt32((uint)bufferWriter.Value.Length); + bufferWriter.Value.CopyTo(ref writer); + } + finally + { + bufferWriter.Value.Dispose(); + } + + writer.WriteEndObject(); + } + + /// + 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(); + } + + ReferenceCodec.MarkValueField(reader.Session); + var length = reader.ReadVarUInt32(); + + var bufferWriter = new BufferWriterBox(new()); + try + { + reader.ReadBytes(ref bufferWriter, (int)length); + result = MemoryPackSerializer.Deserialize(type, bufferWriter.Value.AsReadOnlySequence(), _options.SerializerOptions); + } + finally + { + bufferWriter.Value.Dispose(); + } + + break; + default: + reader.ConsumeUnknownField(header); + break; + } + } + + ReferenceCodec.RecordObject(reader.Session, result, placeholderReferenceId); + return result; + } + + /// + bool IGeneralizedCodec.IsSupportedType(Type type) + { + if (type == SelfType) + { + return true; + } + + if (CommonCodecTypeFilter.IsAbstractOrFrameworkType(type)) + { + return false; + } + + foreach (var selector in _serializableTypeSelectors) + { + if (selector.IsSupportedType(type)) + { + return true; + } + } + + if (_options.IsSerializableType?.Invoke(type) is bool value) + { + return value; + } + + return IsMemoryPackContract(type); + } + + /// + object IDeepCopier.DeepCopy(object input, CopyContext context) + { + if (context.TryGetCopy(input, out object result)) + { + return result; + } + + var bufferWriter = new BufferWriterBox(new()); + try + { + MemoryPackSerializer.Serialize(input.GetType(), bufferWriter, input, _options.SerializerOptions); + + var sequence = bufferWriter.Value.AsReadOnlySequence(); + result = MemoryPackSerializer.Deserialize(input.GetType(), sequence, _options.SerializerOptions); + } + finally + { + bufferWriter.Value.Dispose(); + } + + context.RecordCopy(input, result); + return result; + } + + /// + bool IGeneralizedCopier.IsSupportedType(Type type) + { + if (CommonCodecTypeFilter.IsAbstractOrFrameworkType(type)) + { + return false; + } + + foreach (var selector in _copyableTypeSelectors) + { + if (selector.IsSupportedType(type)) + { + return true; + } + } + + if (_options.IsCopyableType?.Invoke(type) is bool value) + { + return value; + } + + return IsMemoryPackContract(type); + } + + /// + bool? ITypeFilter.IsTypeAllowed(Type type) => (((IGeneralizedCopier)this).IsSupportedType(type) || ((IGeneralizedCodec)this).IsSupportedType(type)) ? true : null; + + private static bool IsMemoryPackContract(Type type) + { + if (SupportedTypes.TryGetValue(type, out bool isMemoryPackContract)) + { + return isMemoryPackContract; + } + + isMemoryPackContract = type.GetCustomAttribute() is not null; + + SupportedTypes.TryAdd(type, isMemoryPackContract); + return isMemoryPackContract; + } + + private static void ThrowTypeFieldMissing() => throw new RequiredFieldMissingException("Serialized value is missing its type field."); +} diff --git a/src/Orleans.Serialization.MemoryPack/MemoryPackCodecOptions.cs b/src/Orleans.Serialization.MemoryPack/MemoryPackCodecOptions.cs new file mode 100644 index 00000000000..1415b1155fa --- /dev/null +++ b/src/Orleans.Serialization.MemoryPack/MemoryPackCodecOptions.cs @@ -0,0 +1,25 @@ +using System; +using MemoryPack; + +namespace Orleans.Serialization; + +/// +/// Options for . +/// +public class MemoryPackCodecOptions +{ + /// + /// Gets or sets the . + /// + public MemoryPackSerializerOptions SerializerOptions { get; set; } = MemoryPackSerializerOptions.Default; + + /// + /// Gets or sets a delegate used to determine if a type is supported by the MemoryPack serializer for serialization and deserialization. + /// + public Func IsSerializableType { get; set; } + + /// + /// Gets or sets a delegate used to determine if a type is supported by the MemoryPack serializer for copying. + /// + public Func IsCopyableType { get; set; } +} diff --git a/src/Orleans.Serialization.MemoryPack/Orleans.Serialization.MemoryPack.csproj b/src/Orleans.Serialization.MemoryPack/Orleans.Serialization.MemoryPack.csproj new file mode 100644 index 00000000000..295e67fd411 --- /dev/null +++ b/src/Orleans.Serialization.MemoryPack/Orleans.Serialization.MemoryPack.csproj @@ -0,0 +1,23 @@ + + + + README.md + Microsoft.Orleans.Serialization.MemoryPack + $(DefaultTargetFrameworks);netstandard2.1 + MemoryPack integration for Orleans.Serialization + true + false + + + + + + + + + + + + + + diff --git a/src/Orleans.Serialization.MemoryPack/README.md b/src/Orleans.Serialization.MemoryPack/README.md new file mode 100644 index 00000000000..24fd5500089 --- /dev/null +++ b/src/Orleans.Serialization.MemoryPack/README.md @@ -0,0 +1,87 @@ +# Microsoft Orleans Serialization for MemoryPack + +## Introduction +Microsoft Orleans Serialization for MemoryPack provides MemoryPack serialization support for Microsoft Orleans using the MemoryPack format. This high-performance binary serialization format is ideal for scenarios requiring efficient serialization and deserialization. + +## Getting Started +To use this package, install it via NuGet: + +```shell +dotnet add package Microsoft.Orleans.Serialization.MemoryPack +``` + +## Example - Configuring MemoryPack Serialization +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Orleans.Hosting; +using Orleans.Serialization; + +var builder = Host.CreateApplicationBuilder(args) + .UseOrleans(siloBuilder => + { + siloBuilder + .UseLocalhostClustering() + // Configure MemoryPack as a serializer + .AddSerializer(serializerBuilder => serializerBuilder.AddMemoryPackSerializer()); + }); + +// Run the host +await builder.RunAsync(); +``` + +## Example - Using MemoryPack with a Custom Type +```csharp +using Orleans; +using Orleans.Serialization.Cloning; +using Orleans.Serialization.Codecs; +using Orleans.Serialization.Configuration; +using Orleans.Serialization.Serializers; +using MemoryPack; +namespace ExampleGrains; + +// Define a class with MemoryPack attributes +[MemoryPackable] +public partial class MyMemoryPackClass +{ + public string Name { get; set; } + public int Age { get; set; } + public List Tags { get; set; } +} + +// You can use it directly in your grain interfaces and implementation +public interface IMyGrain : IGrainWithStringKey +{ + Task GetData(); + Task SetData(MyMemoryPackClass data); +} + +public class MyGrain : Grain, IMyGrain +{ + private MyMemoryPackClass _data; + + public Task GetData() + { + return Task.FromResult(_data); + } + + public Task SetData(MyMemoryPackClass data) + { + _data = data; + return Task.CompletedTask; + } +} +``` + +## Documentation +For more comprehensive documentation, please refer to: +- [Microsoft Orleans Documentation](https://learn.microsoft.com/dotnet/orleans/) +- [Orleans Serialization](https://learn.microsoft.com/en-us/dotnet/orleans/host/configuration-guide/serialization) +- [MemoryPack for C#](https://github.com/Cysharp/MemoryPack) + +## Feedback & Contributing +- If you have any issues or would like to provide feedback, please [open an issue on GitHub](https://github.com/dotnet/orleans/issues) +- Join our community on [Discord](https://aka.ms/orleans-discord) +- Follow the [@msftorleans](https://twitter.com/msftorleans) Twitter account for Orleans announcements +- Contributions are welcome! Please review our [contribution guidelines](https://github.com/dotnet/orleans/blob/main/CONTRIBUTING.md) +- This project is licensed under the [MIT license](https://github.com/dotnet/orleans/blob/main/LICENSE) diff --git a/src/Orleans.Serialization.MemoryPack/SerializationHostingExtensions.cs b/src/Orleans.Serialization.MemoryPack/SerializationHostingExtensions.cs new file mode 100644 index 00000000000..50f5ffccee3 --- /dev/null +++ b/src/Orleans.Serialization.MemoryPack/SerializationHostingExtensions.cs @@ -0,0 +1,92 @@ +using System; +using MemoryPack; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Orleans.Serialization.Cloning; +using Orleans.Serialization.Serializers; +using Orleans.Serialization.Utilities.Internal; + +namespace Orleans.Serialization; + +/// +/// Extension method for . +/// +public static class SerializationHostingExtensions +{ + private static readonly ServiceDescriptor ServiceDescriptor = new(typeof(MemoryPackCodec), typeof(MemoryPackCodec)); + + /// + /// Adds support for serializing and deserializing values 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. + /// The MemoryPack serializer options. + public static ISerializerBuilder AddMemoryPackSerializer( + this ISerializerBuilder serializerBuilder, + Func isSerializable = null, + Func isCopyable = null, + MemoryPackSerializerOptions memoryPackSerializerOptions = null) + { + return serializerBuilder.AddMemoryPackSerializer( + isSerializable, + isCopyable, + optionsBuilder => optionsBuilder.Configure(options => + { + if (memoryPackSerializerOptions is not null) + { + options.SerializerOptions = memoryPackSerializerOptions; + } + }) + ); + } + + /// + /// Adds support for serializing and deserializing values 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. + /// A delegate used to configure the options for the MemoryPack codec. + public static ISerializerBuilder AddMemoryPackSerializer( + this ISerializerBuilder serializerBuilder, + Func isSerializable, + Func isCopyable, + Action> configureOptions = null) + { + var services = serializerBuilder.Services; + if (configureOptions != null) + { + configureOptions(services.AddOptions()); + } + + if (isSerializable != null) + { + services.AddSingleton(new DelegateCodecSelector + { + CodecName = MemoryPackCodec.WellKnownAlias, + IsSupportedTypeDelegate = isSerializable + }); + } + + if (isCopyable != null) + { + services.AddSingleton(new DelegateCopierSelector + { + CopierName = MemoryPackCodec.WellKnownAlias, + IsSupportedTypeDelegate = isCopyable + }); + } + + if (!services.Contains(ServiceDescriptor)) + { + services.AddSingleton(); + services.AddFromExisting(); + services.AddFromExisting(); + services.AddFromExisting(); + serializerBuilder.Configure(options => options.WellKnownTypeAliases[MemoryPackCodec.WellKnownAlias] = typeof(MemoryPackCodec)); + } + + return serializerBuilder; + } +} diff --git a/src/api/Orleans.Serialization.MemoryPack/Orleans.Serialization.MemoryPack.cs b/src/api/Orleans.Serialization.MemoryPack/Orleans.Serialization.MemoryPack.cs new file mode 100644 index 00000000000..55327288ab3 --- /dev/null +++ b/src/api/Orleans.Serialization.MemoryPack/Orleans.Serialization.MemoryPack.cs @@ -0,0 +1,45 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +namespace Orleans.Serialization +{ + [Alias("memorypack")] + public partial class MemoryPackCodec : Serializers.IGeneralizedCodec, Codecs.IFieldCodec, Cloning.IGeneralizedCopier, Cloning.IDeepCopier, ITypeFilter + { + public const string WellKnownAlias = "memorypack"; + public MemoryPackCodec(System.Collections.Generic.IEnumerable serializableTypeSelectors, System.Collections.Generic.IEnumerable copyableTypeSelectors, Microsoft.Extensions.Options.IOptions options) { } + + object Cloning.IDeepCopier.DeepCopy(object input, Cloning.CopyContext context) { throw null; } + + bool Cloning.IGeneralizedCopier.IsSupportedType(System.Type type) { throw null; } + + object Codecs.IFieldCodec.ReadValue(ref Buffers.Reader reader, WireProtocol.Field field) { throw null; } + + void Codecs.IFieldCodec.WriteField(ref Buffers.Writer writer, uint fieldIdDelta, System.Type expectedType, object value) { } + + bool? ITypeFilter.IsTypeAllowed(System.Type type) { throw null; } + + bool Serializers.IGeneralizedCodec.IsSupportedType(System.Type type) { throw null; } + } + + public partial class MemoryPackCodecOptions + { + public System.Func IsCopyableType { get { throw null; } set { } } + + public System.Func IsSerializableType { get { throw null; } set { } } + + public MemoryPack.MemoryPackSerializerOptions SerializerOptions { get { throw null; } set { } } + } + + public static partial class SerializationHostingExtensions + { + public static ISerializerBuilder AddMemoryPackSerializer(this ISerializerBuilder serializerBuilder, System.Func isSerializable = null, System.Func isCopyable = null, MemoryPack.MemoryPackSerializerOptions memoryPackSerializerOptions = null) { throw null; } + + public static ISerializerBuilder AddMemoryPackSerializer(this ISerializerBuilder serializerBuilder, System.Func isSerializable, System.Func isCopyable, System.Action> configureOptions = null) { throw null; } + } +} diff --git a/test/Orleans.Serialization.UnitTests/MemoryPackSerializerTests.cs b/test/Orleans.Serialization.UnitTests/MemoryPackSerializerTests.cs new file mode 100644 index 00000000000..22b8c0585df --- /dev/null +++ b/test/Orleans.Serialization.UnitTests/MemoryPackSerializerTests.cs @@ -0,0 +1,148 @@ +#nullable enable +using System; +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; + +/// +/// Tests for Orleans' MemoryPack serialization support. +/// +/// MemoryPack is a binary serialization format that provides: +/// - More compact representation than JSON +/// - Faster serialization/deserialization than text formats +/// - Cross-platform and cross-language support +/// +/// Orleans' MemoryPack integration: +/// - Leverages the MemoryPack-CSharp library +/// - Supports union types (discriminated unions) +/// - Provides both serialization and deep copy functionality +/// - Can be used for specific types while using Orleans' native format for others +/// +/// This is useful when: +/// - Interoperating with systems that use MemoryPack +/// - Requiring a compact binary format with broad language support +/// - Needing better performance than JSON but more portability than Orleans' native format +/// +[Trait("Category", "BVT")] +public class MemoryPackCodecTests : FieldCodecTester> +{ + public MemoryPackCodecTests(ITestOutputHelper output) : base(output) + { + } + + protected override void Configure(ISerializerBuilder builder) + { + builder.AddMemoryPackSerializer(); + } + + protected override MyMemoryPackClass? CreateValue() => new() { IntProperty = 30, StringProperty = "hello", SubClass = new() { Id = Guid.NewGuid() } }; + + protected override MyMemoryPackClass?[] TestValues => new MyMemoryPackClass?[] + { + null, + new() { SubClass = new() { Id = Guid.NewGuid() } }, + new() { IntProperty = 150, StringProperty = new string('c', 20), SubClass = new() { Id = Guid.NewGuid() } }, + new() { IntProperty = 150_000, StringProperty = new string('c', 6_000), SubClass = new() { Id = Guid.NewGuid() } }, + new() { Union = new MyMemoryPackUnionVariant1 { IntProperty = 1 } }, + new() { Union = new MyMemoryPackUnionVariant2 { StringProperty = "String" } }, + }; + + [Fact] + public void MemoryPackSerializerDeepCopyTyped() + { + var original = new MyMemoryPackClass { IntProperty = 30, StringProperty = "hi", SubClass = new() { Id = Guid.NewGuid() } }; + var copier = ServiceProvider.GetRequiredService>(); + var result = copier.Copy(original); + + Assert.Equal(original.IntProperty, result.IntProperty); + Assert.Equal(original.StringProperty, result.StringProperty); + Assert.Equal(original.SubClass.Id, result.SubClass.Id); + } + + [Fact] + public void MemoryPackSerializerDeepCopyUntyped() + { + var original = new MyMemoryPackClass { IntProperty = 30, StringProperty = "hi", SubClass = new() { Id = Guid.NewGuid() } }; + var copier = ServiceProvider.GetRequiredService(); + var result = (MyMemoryPackClass)copier.Copy((object)original); + + Assert.Equal(original.IntProperty, result.IntProperty); + Assert.Equal(original.StringProperty, result.StringProperty); + Assert.Equal(original.SubClass.Id, result.SubClass.Id); + } + + [Fact] + public void MemoryPackSerializerRoundTripThroughCodec() + { + var original = new MyMemoryPackClass { IntProperty = 30, StringProperty = "hi", SubClass = new() { Id = Guid.NewGuid() } }; + var result = RoundTripThroughCodec(original); + + Assert.Equal(original.IntProperty, result.IntProperty); + Assert.Equal(original.StringProperty, result.StringProperty); + } + + [Fact] + public void MemoryPackSerializerRoundTripThroughUntypedSerializer() + { + var original = new MyMemoryPackClass { IntProperty = 30, StringProperty = "hi", SubClass = new() { Id = Guid.NewGuid() } }; + 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 MemoryPackUnionCodecTests : FieldCodecTester> +{ + public MemoryPackUnionCodecTests(ITestOutputHelper output) : base(output) + { + } + + protected override void Configure(ISerializerBuilder builder) + { + builder.AddMemoryPackSerializer(); + } + + protected override IMyMemoryPackUnion? CreateValue() => new MyMemoryPackUnionVariant1() { IntProperty = 30 }; + + protected override IMyMemoryPackUnion?[] TestValues => new IMyMemoryPackUnion?[] + { + null, + new MyMemoryPackUnionVariant1 { IntProperty = 1 }, + new MyMemoryPackUnionVariant2 { StringProperty = "String" }, + }; +} + + +[Trait("Category", "BVT")] +public class MemoryPackCodecCopierTests : CopierTester> +{ + public MemoryPackCodecCopierTests(ITestOutputHelper output) : base(output) + { + } + + protected override void Configure(ISerializerBuilder builder) + { + builder.AddMemoryPackSerializer(); + } + protected override IDeepCopier CreateCopier() => ServiceProvider.GetRequiredService().GetDeepCopier(); + + protected override MyMemoryPackClass? CreateValue() => new() { IntProperty = 30, StringProperty = "hello", SubClass = new() { Id = Guid.NewGuid() } }; + + protected override MyMemoryPackClass?[] TestValues => new MyMemoryPackClass?[] + { + null, + new() { SubClass = new() { Id = Guid.NewGuid() } }, + new() { IntProperty = 150, StringProperty = new string('c', 20), SubClass = new() { Id = Guid.NewGuid() } }, + new() { IntProperty = 150_000, StringProperty = new string('c', 6_000), SubClass = new() { Id = Guid.NewGuid() } }, + }; +} diff --git a/test/Orleans.Serialization.UnitTests/Models.cs b/test/Orleans.Serialization.UnitTests/Models.cs index 723acc6badc..dbd1526d9ae 100644 --- a/test/Orleans.Serialization.UnitTests/Models.cs +++ b/test/Orleans.Serialization.UnitTests/Models.cs @@ -6,6 +6,7 @@ using System.Runtime.Serialization; using System.Text.Json; using System.Text.Json.Nodes; +using MemoryPack; using MessagePack; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -963,4 +964,41 @@ public sealed record MyMessagePackUnionVariant2 : IMyMessagePackUnion [Key(0)] public string StringProperty { get; init; } } + + [MemoryPackable] + public partial record MyMemoryPackClass + { + public int IntProperty { get; init; } + + public string StringProperty { get; init; } + + public MyMemoryPackSubClass SubClass { get; init; } + + public IMyMemoryPackUnion Union { get; init; } + } + + [MemoryPackable] + public partial record MyMemoryPackSubClass + { + public Guid Id { get; init; } + } + + [MemoryPackable] + [MemoryPackUnion(0, typeof(MyMemoryPackUnionVariant1))] + [MemoryPackUnion(1, typeof(MyMemoryPackUnionVariant2))] + public partial interface IMyMemoryPackUnion + { + } + + [MemoryPackable] + public partial record MyMemoryPackUnionVariant1 : IMyMemoryPackUnion + { + public int IntProperty { get; init; } + } + + [MemoryPackable] + public partial record MyMemoryPackUnionVariant2 : IMyMemoryPackUnion + { + public string StringProperty { get; init; } + } } diff --git a/test/Orleans.Serialization.UnitTests/Orleans.Serialization.UnitTests.csproj b/test/Orleans.Serialization.UnitTests/Orleans.Serialization.UnitTests.csproj index 4b4e67305af..5d04473c0a1 100644 --- a/test/Orleans.Serialization.UnitTests/Orleans.Serialization.UnitTests.csproj +++ b/test/Orleans.Serialization.UnitTests/Orleans.Serialization.UnitTests.csproj @@ -39,6 +39,7 @@ +