Reputation: 937
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?
Upvotes: 1
Views: 3584
Reputation: 116826
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:
JsonConverter.ReadJson(JsonReader, Type objectType, Object, JsonSerializer)
The objectType
indicates the required type to deserialize, e.g. Expression<Func<TTable, bool>>
for PredicateSerializationConverter
.
JsonConverter.WriteJson(JsonWriter, Object value, JsonSerializer serializer)
.
Here the value
is the actual expression being serialized, so of course its type is known.
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.
Upvotes: 3