dotnet / orleans

Cloud Native application framework for .NET
https://docs.microsoft.com/dotnet/orleans
MIT License
10.07k stars 2.02k forks source link

Generic surrogate types for Orleans serializer do not work with non-generic derived types #8347

Open david-obee opened 1 year ago

david-obee commented 1 year ago

I'm running into an issue with using surrogate types for an external type, which is generic, but which has a sub-type which is not generic. Reading the documentation, it sounded like my use case would be supported, but I think it's really only in a restricted subset of scenarios that using generic surrogates works.

In my use case, I have in the external library (that should not take a dependency on Orleans):

public class SomeId<T>
{
    public T Id { get; set; }

    public SomeId(T id)
    {
        Id = id;
    }
}

I then have in my assembly (which is then using Orleans), the sub-type, surrogate, and surrogate converter defined:

public class IntId : SomeId<int>
{
    public IntId(int id) : base(id)
    {}
}
[GenerateSerializer]
public class SomeIdSurrogate<T>
{
    [Id(0)]
    public T Id { get; set; }
}

[RegisterConverter]
public sealed class SomeIdSurrogateConverter<T> : 
    IConverter<SomeId<T>, SomeIdSurrogate<T>>, IPopulator<SomeId<T>, SomeIdSurrogate<T>> where T : notnull
{
    public SomeId<T> ConvertFromSurrogate(in SomeIdSurrogate<T> surrogate)
    {
        return new SomeId<T>(surrogate.Id);
    }

    public SomeIdSurrogate<T> ConvertToSurrogate(in SomeId<T> value)
    {
        return new SomeIdSurrogate<T> { Id = value.Id };
    }

    public void Populate(in SomeIdSurrogate<T> surrogate, SomeId<T> value)
    {
        value.Id = surrogate.Id;
    }
}

The issue I have is that I get the error System.ArgumentException: The number of generic arguments provided doesn't equal the arity of the generic type definition. (Parameter 'instantiation') at this point in the Orleans CodecProvider code:

https://github.com/dotnet/orleans/blob/v7.1.0/src/Orleans.Serialization/Serializers/CodecProvider.cs#L559

private bool TryGetSurrogateCodec(Type fieldType, Type searchType, out Type surrogateCodecType, out object[] constructorArguments)
{
    if (_converters.TryGetValue(searchType, out var converterType))
    {
        if (converterType.IsGenericTypeDefinition)
        {
            converterType = converterType.MakeGenericType(fieldType.GetGenericArguments());
        }

...

When searching for fieldType of IntId, we reach the point where searchType is SomeId`1[T], for which we have a key in _converters, and the value is then SomeIdSurrogateConverter`1[T]. So far so good, it means we've correctly searched up the type hierarchy until we find our converter for the base type.

The issue is that then on line 559, where we call converterType.MakeGenericType(fieldType.GetGenericArguments()). Here, fieldType is not generic, so GetGenericArguments returns an empty array. In fact, this will always fail so long as the derived fieldType has a different number of generic parameters than the searchType. It should never have more generic parameters, but it seems reasonable that it might have fewer.

This might be too niche a use case to want to support in the serializer. I think I can work around it by introducing non-generic intermediaries, which can then have non-generic type converters, but this isn't ideal. It would be great if the serializer could figure out the type parameters of the base type, in the case that the derived type isn't generic and specifies the type parameters as part of it's definition. I am conscious though that that might incur quite a bit of additional complexity. In that case, it might be valuable to expand the documentation instead to make clear the restrictions, i.e. derived types need the same number of generic parameters, and they must be in the same order as the base type.

Thanks!

luckyycode commented 1 year ago

the same behavior (I may be wrong though in explaining) exists even without surrogates, to reproduce I simply need some base class in the same project, e.g.

interface IObj<out T> { }
class BaseObj<T1, T2> : IObj<T> { /*properties*/ }, t1 and t2 are primitives
class Obj : BaseObj<long, int> 

and if I do grainMethod<T>(IObj<T> obj) with Obj object as input parameter I get the same error as you do, inspecting in the debugger and I see that one generic argument is missing, does not matter which serializer I use, neither built-in one with GenerateSerializer attribute or SystemTextJson serializer work (but as I see the error appears way before serialization, so just two cents)

for now my solution is to avoid generics usage, so I had to rewrite 1000 classes (uh, I want to migrate to Orleans 7 hehe)