Search code examples
c#tuples.net-standard-2.0orleans.net-standard-2.1

How to properly inherit from tuples so that serializer generation from orleans is correct?


I've a project with netstandard2.0 as TargetFramework and following Nuget Packages :

microsoft.orleans.core -> Version="2.2.0"
microsoft.orleans.orleanscodegenerator.build -> Version="2.2.0"

This project has a DTO that implements a tuple as follows :

public sealed class SomeDetailsDto : Tuple<Guid, Guid>
    {
        public SomeDetailsDto(Guid firstGuidId, Guid secondGuidId)
            : base(firstGuidId, secondGuidId)
        {
        }

        public Guid firstGuidId => Item1;

        public Guid secondGuidId => Item2;
    }

This DTO will be used in a grain method. The orleans generated serializer code looks as follows :

[global::System.CodeDom.Compiler.GeneratedCodeAttribute(@"Orleans-CodeGenerator", @"2.0.0.0"), global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute, global::Orleans.CodeGeneration.SerializerAttribute(typeof(global::SomeDetailsDto))]
    internal sealed class OrleansCodeGenSomeDetailsDtoSerializer
    {
        private readonly global::System.Func<global::System.Tuple<global::System.Guid, global::System.Guid>, global::System.Guid> getField0;
        private readonly global::System.Action<global::System.Tuple<global::System.Guid, global::System.Guid>, global::System.Guid> setField0;
        private readonly global::System.Func<global::System.Tuple<global::System.Guid, global::System.Guid>, global::System.Guid> getField1;
        private readonly global::System.Action<global::System.Tuple<global::System.Guid, global::System.Guid>, global::System.Guid> setField1;
        public OrleansCodeGenSomeDetailsDtoSerializer(global::Orleans.Serialization.IFieldUtils fieldUtils)
        {
            global::System.Reflection.FieldInfo field0 = typeof(global::System.Tuple<global::System.Guid, global::System.Guid>).GetField(@"m_Item1", (System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public));
            getField0 = (global::System.Func<global::System.Tuple<global::System.Guid, global::System.Guid>, global::System.Guid>)fieldUtils.GetGetter(field0);
            setField0 = (global::System.Action<global::System.Tuple<global::System.Guid, global::System.Guid>, global::System.Guid>)fieldUtils.GetReferenceSetter(field0);
            global::System.Reflection.FieldInfo field1 = typeof(global::System.Tuple<global::System.Guid, global::System.Guid>).GetField(@"m_Item2", (System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public));
            getField1 = (global::System.Func<global::System.Tuple<global::System.Guid, global::System.Guid>, global::System.Guid>)fieldUtils.GetGetter(field1);
            setField1 = (global::System.Action<global::System.Tuple<global::System.Guid, global::System.Guid>, global::System.Guid>)fieldUtils.GetReferenceSetter(field1);
        }

        [global::Orleans.CodeGeneration.CopierMethodAttribute]
        public global::System.Object DeepCopier(global::System.Object original, global::Orleans.Serialization.ICopyContext context)
        {
            global::SomeDetailsDto input = ((global::SomeDetailsDto)original);
            global::SomeDetailsDto result = (global::SomeDetailsDto)global::System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof(global::SomeDetailsDto));
            context.RecordCopy(original, result);
            setField0(result, getField0(input));
            setField1(result, getField1(input));
            return result;
        }

        [global::Orleans.CodeGeneration.SerializerMethodAttribute]
        public void Serializer(global::System.Object untypedInput, global::Orleans.Serialization.ISerializationContext context, global::System.Type expected)
        {
            global::SomeDetailsDto input = (global::SomeDetailsDto)untypedInput;
            context.SerializeInner(getField0(input), typeof(global::System.Guid));
            context.SerializeInner(getField1(input), typeof(global::System.Guid));
        }

        [global::Orleans.CodeGeneration.DeserializerMethodAttribute]
        public global::System.Object Deserializer(global::System.Type expected, global::Orleans.Serialization.IDeserializationContext context)
        {
            global::SomeDetailsDto result = (global::SomeDetailsDto)global::System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof(global::SomeDetailsDto));
            context.RecordObject(result);
            setField0(result, (global::System.Guid)context.DeserializeInner(typeof(global::System.Guid)));
            setField1(result, (global::System.Guid)context.DeserializeInner(typeof(global::System.Guid)));
            return (global::SomeDetailsDto)result;
        }
    }

This is all totally fine.

But recently I updated the TargetFramework to netstandard2.1, microsoft.orleans.core to "3.0.2", and instead of microsoft.orleans.orleanscodegenerator.build (2.2.0), I installed Microsoft.Orleans.CodeGenerator.MSBuild (3.0.2).

With the above setup, I got the below warning :
warning ORL1001: Type SomeDetailsDto has a base type which belongs to a reference assembly. Serializer generation for this type may not include important base type fields.

Restore, Build and Publish commands are all working. However, the generated serializer is faulty because of which tests are failing.
Below is the orleans generated code for the same DTO

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("OrleansCodeGen", "2.0.0.0"), global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute, global::Orleans.CodeGeneration.SerializerAttribute(typeof(global::SomeDetailsDto))]
    internal sealed class OrleansCodeGenSomeDetailsDtoSerializer
    {
        public OrleansCodeGenSomeDetailsDtoSerializer(global::Orleans.Serialization.IFieldUtils fieldUtils)
        {
        }

        [global::Orleans.CodeGeneration.CopierMethodAttribute]
        public object DeepCopier(object original, global::Orleans.Serialization.ICopyContext context)
        {
            global::SomeDetailsDto input = ((global::SomeDetailsDto)original);
            global::SomeDetailsDto result = (global::SomeDetailsDto)global::System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof(global::SomeDetailsDto));
            context.RecordCopy(original, result);
            return result;
        }

        [global::Orleans.CodeGeneration.SerializerMethodAttribute]
        public void Serializer(object untypedInput, global::Orleans.Serialization.ISerializationContext context, global::System.Type expected)
        {
            global::SomeDetailsDto input = (global::SomeDetailsDto)untypedInput;
        }

        [global::Orleans.CodeGeneration.DeserializerMethodAttribute]
        public object Deserializer(global::System.Type expected, global::Orleans.Serialization.IDeserializationContext context)
        {
            global::SomeDetailsDto result = (global::SomeDetailsDto)global::System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof(global::SomeDetailsDto));
            context.RecordObject(result);
            return (global::SomeDetailsDto)result;
        }
    }

The latest serializer has ignored Item1 & Item2 of the Tuple as mentioned in the warning. I searched a lot on this issue and didn't find anything. I could use a custom serializer as mentioned in this orleans documentation. But I've another DTO which is also getting the earlier mentioned warning.

What might be a better way to address this issue?


Solution

  • I would recommend not inheriting from Tuple. Instead, put those fields on your class so that your class looks like this:

    [Serializable]
    public sealed class SomeDetailsDto
    {
      public SomeDetailsDto(Guid firstGuidId, Guid secondGuidId)
      {
        FirstGuidId = firstGuidId;
        SecondGuidId = secondGuidId;
      }
    
      public Guid FirstGuidId { get; }
    
      public Guid SecondGuidId { get; }
    }
    

    Note that I also added the [Serializable] attribute to your class. I also recommend you always add [Serializable] to types which you intend to serialize. The code generator will do its best to infer that you want a serializer generated for that type anyway (because it appears in a grain interface method signature and by other heuristics), but that process cannot be made perfectly accurate - so it's best to be explicit.

    If you want to have some common type indicating that your class has 2 fields (assuming that is useful to you), then you could define an interface or your own tuple-like types and implement that on your DTO.

    As the build warning message points out, Tuple is defined in a special type of assembly called a ref-only assembly. What that means is that the Orleans code generator is not able to see any of the fields in the Tuple class (those ref-only assemblies omit private members and all IL code, so they are not present at all). Without knowing what fields are present on the base class, the code generator cannot generate a correct serializer for it. This is a limitation caused by ref-only assemblies and there has been some discussion on the Orleans repository for how to rectify this limitation.

    There is a feature request for supporting inheriting from Tuple and collection types here: https://github.com/dotnet/orleans/issues/6158