简体   繁体   中英

How to apply a generic JsonConverter to a property of a generic class when the property type includes the generic type?

I have a problem with Json.NET. I need to use different generic JsonConverters for different properties in one class that is also generic, when the property types include the generic type parameter of the class itself.

Let's have the following generic class where the parameter TTable is used by some properties requiring conversion:

public class ExpresssionDTO<TTable> : BaseDTO where TTable : class
{
    [JsonProperty(ItemConverterType = typeof(PredicateSerializationConverter<>))]
    public ICollection<Expression<Func<TTable, bool>>> Predicates { get; set; } = new List<Expression<Func<TTable, bool>>>();

    [JsonConverter(converterType: typeof(FilterSerializationConverter<>))]
    public Expression<Func<TTable, object>> Filter { get; set; } = null;
}

With converters:

public class PredicateSerializationConverter<TTable> : ExpressionSerializer<TTable, bool> where TTable : class
{
    public PredicateSerializationConverter() :base()
    {
    }
}

public class FilterSerializationConverter<TTable> : ExpressionSerializer<TTable, object> where TTable : class
{
    public FilterSerializationConverter() : base()
    {
    }
}

public class ExpressionSerializer<T, U> : JsonConverter where T : class     
{
    ...
}

In my contract resolver I have an error:

Cannot create an instance of WebFoundationClassesCore.ServiceClasses.Converters.PredicateSerializationConverter`1[TTable] because Type.ContainsGenericParameters is true.

This problem also raised for DefaultContractResolver .

Is there any way to solve my problem?

What you want to do is to use the generic parameter TTable as the generic parameter for your custom JSON converters like so:

public class ExpresssionDTO<TTable> : BaseDTO where TTable : class
{
    [JsonProperty(ItemConverterType = typeof(PredicateSerializationConverter<TTable>))] // Here
    public ICollection<Expression<Func<TTable, bool>>> Predicates { get; set; } = new List<Expression<Func<TTable, bool>>>();

    [JsonConverter(converterType: typeof(FilterSerializationConverter<TTable>))] // And here
    public Expression<Func<TTable, object>> Filter { get; set; } = null;
}

But you cannot because c# forbids generic parameters in attributes . It seems you are hoping that, if you specify an open generic type for the converter, and the parent object type is also a generic type with the same number of generic arguments, then Json.NET will automatically construct the converter by plugging in the parent's generic arguments -- here TTable . Unfortunately, this is not implemented.

So, what are your options?

Firstly , you could create a custom contract resolver inheriting from DefaultContractResolver that constructs and applies the appropriate concrete generic converters. Since you are applying the converters to properties, you will need to override DefaultContractResolver.CreateProperty and set JsonProperty.Converter or JsonProperty.ItemConverter as required.

Secondly , you could abandon the generic converter approach and create non-generic converters for serializing filters and predicates. You can do this because, while using generics when writing converters is convenient and readable, it isn't strictly necessary, as all required type information is passed into the non-generic read and write methods:

You question does not show a minimal reproducible example for your ExpressionSerializer<T, U> . If you can easily rewrite it to be non-generic, then you should consider doing so. If not, you could adopt the decorator pattern and wrap your existing generic converters in a decorator that infers the required generic parameters from the objectType or value like so:

public class GenericFuncExpressionArgumentConverterDecorator : JsonConverter
{
    readonly Type openGenericConverterType;
    volatile Tuple<Type, JsonConverter> converterCache;
            
    public GenericFuncExpressionArgumentConverterDecorator(Type openGenericConverterType)
    {
        if (openGenericConverterType == null)
            throw new ArgumentNullException();
        if (!openGenericConverterType.IsSubclassOf(typeof(JsonConverter)))
            throw new ArgumentException(string.Format("{0} is not a JsonConvreter", GetType().Name));
        if (!openGenericConverterType.IsGenericTypeDefinition)
            throw new ArgumentException(string.Format("{0} is not an open generic type", GetType().Name));
        this.openGenericConverterType = openGenericConverterType;
    }

    public override bool CanConvert(Type objectType) => 
        throw new NotImplementedException(string.Format("{0} is intended to be applied via a JsonConverter or JsonProperty attribute", GetType().Name));

    JsonConverter GetConverter(Type objectType)
    {
        var cache = converterCache;
        if (cache != null && cache.Item1 == objectType)
            return cache.Item2;
        // Despite the documentation, Expression<T> is not actually sealed in .Net 5!
        // https://github.com/dotnet/runtime/blob/master/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/LambdaExpression.cs#L174
        var expressionType = objectType.BaseTypesAndSelf().Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Expression<>)).FirstOrDefault();
        if (expressionType == null)
            throw new JsonSerializationException(string.Format("Invalid expression type {0}", objectType));
        var delegateType = objectType.GetGenericArguments().Single();
        if (!delegateType.IsGenericType || delegateType.GetGenericTypeDefinition() != typeof(Func<,>))
            throw new JsonSerializationException(string.Format("Invalid delegate type {0}", delegateType));
        var argType = delegateType.GetGenericArguments()[0];
        var converterType = openGenericConverterType.MakeGenericType(new [] { argType });
        var converter = (JsonConverter)Activator.CreateInstance(converterType);
        converterCache = Tuple.Create(objectType, converter);
        return converter;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) =>
        GetConverter(objectType).ReadJson(reader, objectType, existingValue, serializer);

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) =>
        GetConverter(value.GetType()).WriteJson(writer, value, serializer);
}

public static class TypeExtensions
{
    public static IEnumerable<Type> BaseTypesAndSelf(this Type type)
    {
        while (type != null)
        {
            yield return type;
            type = type.BaseType;
        }
    }
}

And then apply it to your model as follows, passing the open generic converter types as converter arguments:

public class ExpresssionDTO<TTable> : BaseDTO where TTable : class
{
    [JsonProperty(ItemConverterType = typeof(GenericFuncExpressionArgumentConverterDecorator), ItemConverterParameters = new object [] { typeof(PredicateSerializationConverter<>) })]
    public ICollection<Expression<Func<TTable, bool>>> Predicates { get; set; } = new List<Expression<Func<TTable, bool>>>();

    [JsonConverter(typeof(GenericFuncExpressionArgumentConverterDecorator), new object [] { typeof(FilterSerializationConverter<>) })]
    public Expression<Func<TTable, object>> Filter { get; set; } = null;
}

Demo fiddle here .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM