按照@dbc 的建议,我使用JsonConverterFactory 模式来解决我的问题。
这是我写的两个类。
转换器本身:
public class CustomObjectConverter<T> : JsonConverter<T> where T : class
{
private record struct UnmatchedParameterInfo(string Name, Type Type, bool AcceptsNullValue);
private record struct MatchedParameterInfo(string Name, Type Type, object Value);
// ConstructorMatcher is a helper class that will keep track of
// Json properties that can be matched with the parameters of
// a specific constructor as they are read from the reader
private class ConstructorMatcher
{
public ConstructorInfo ConstructorInfo { get; }
private Dictionary<string, UnmatchedParameterInfo> UnmatchedParameters { get; }
private Dictionary<string, MatchedParameterInfo> MatchedParameters { get; } = new();
private ImmutableList<string> ParameterList { get; }
public ConstructorMatcher(ConstructorInfo constructorInfo)
{
ConstructorInfo = constructorInfo ?? throw new ArgumentNullException(nameof(constructorInfo));
var parameters = constructorInfo.GetParameters();
if (parameters.Any(p => p.Name is null))
throw new Exception("<useful exception message>");
ParameterList = parameters.Select(p => p.Name!).ToImmutableList();
UnmatchedParameters =
parameters.ToDictionary(
p => p.Name!,
p => new UnmatchedParameterInfo(
p.Name!,
p.ParameterType,
AcceptsNullValue(p.ParameterType)),
StringComparer.CurrentCultureIgnoreCase);
}
// Checks is null can be assigned to a certain type
private bool AcceptsNullValue(Type type)
{
// This is improvable as it doesn't use reference types
// nullability information
return type.IsClass || Nullable.GetUnderlyingType(type) is not null;
}
public int MatchCount => MatchedParameters.Count;
public int UnmatchedCount => UnmatchedParameters.Count;
// Checks if the constructor still has a yet unmatched parameter
// named "name"
public bool HasParameter(string name) => UnmatchedParameters.ContainsKey(name);
// Returns the type that the constructor expects for parameter "name".
// The converter needs this because it has to know beforehand the type
// of the property it is about to deserialize.
public Type GetTypeForParameter(string name)
{
return UnmatchedParameters.TryGetValue(name, out var info)
? info.Type
: throw new Exception("<useful exception message>");
}
// Binds a value to a constructor parameter
public void AddParameterValue(string name, Type type, object? value)
{
if (!UnmatchedParameters.Remove(name, out var unmatchedParameterInfo))
throw new Exception("<useful exception message>");
if (unmatchedParameterInfo.Type != type)
throw new Exception("<useful exception message>");
MatchedParameters.Add(unmatchedParameterInfo.Name,
new MatchedParameterInfo(
unmatchedParameterInfo.Name,
unmatchedParameterInfo.Type,
value));
}
// Checks if the constructor has some unbound parameters
// that won't accept null values
public bool HasNonNullableUnmatchedParameters => UnmatchedParameters.Values.Any(upi => !upi.AcceptsNullValue);
// Gets parameter values in the righ order for constructor invocation
public object?[] GetInvocationParameters()
{
return ParameterList.Select(parameterName => MatchedParameters.TryGetValue(parameterName, out var mpi)
? mpi.Value
: null)
.ToArray();
}
}
// For each Json property:
// - read the Json property name,
// - eliminate constructors that don't have a parameter with
// a compatible name
// - get the expected type for this property (end throw if all
// constructors don't expect the same type for the same property)
// - deserialize the property value to the expected type
// - bind the deserialized value to the matching parameter of each
// candidate constructor
// When all properties are read, invoke the constructor that has the
// most parameters bound to values, with the less unbound parameters
// (with the condition that all unbound parameters can be bound to null)
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
var candidates = typeToConvert.GetConstructors().Select(ci => new ConstructorMatcher(ci)).ToImmutableList();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
var bestCandidate = candidates
.Where(c => !c.HasNonNullableUnmatchedParameters)
.OrderByDescending(c => c.MatchCount)
.ThenBy(c => c.UnmatchedCount)
.FirstOrDefault() ??
throw new NoSuitableConstructorFoundException("<useful exception message>");
return (T?)bestCandidate.ConstructorInfo.Invoke(bestCandidate.GetInvocationParameters());
}
if (reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException();
}
var propertyName = reader.GetString() ?? throw new Exception("<useful exception message>");
candidates = candidates.Where(c => c.HasParameter(propertyName)).ToImmutableList();
var possibleTypes = candidates.Select(c => c.GetTypeForParameter(propertyName))
.Distinct()
.ToArray();
if (possibleTypes.Length > 1)
throw new AmbiguousConfigurationException("<useful exception message>");
var propertyType = possibleTypes[0];
var value = JsonSerializer.Deserialize(ref reader, propertyType, options);
foreach (var constructorMatcher in candidates)
{
constructorMatcher.AddParameterValue(propertyName, propertyType, value);
}
}
throw new JsonException();
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
writer.WriteStartObject();
foreach (var property in typeof(T).GetProperties())
{
writer.WritePropertyName(options.PropertyNamingPolicy?.ConvertName(property.Name) ?? property.Name);
JsonSerializer.Serialize(value, options);
}
writer.WriteEndObject();
}
}
现在对于转换器工厂,我们必须解决的问题是“我们的转换器应该应用于哪些目标类型?”
我们想要处理所有 Json 对象,但没有什么明确的联系
具有特定类别的 CLR 类型的 Json 对象。
Json 对象可以反序列化为 POCO 类,也可以反序列化为字典
和结构。相反,集合类通常会从 Json 数组中反序列化。
一些 Json 对象也可以由序列化选项中的自定义转换器处理。
我很确定我们的策略可以改进,因为我们将自己限制在我们特定情况下足够的范围内。
我们选择将转换器应用于所有未实现 IEnumerable 的类(我猜主要限制是我们不处理结构)。
此外,如果我们在可以转换目标类型的序列化选项中找到更具体的转换器,我们将应用它而不是我们的转换器。
public class CustomObjectConverterFactory: JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsClass && !typeof(IEnumerable).IsAssignableFrom(typeToConvert);
}
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var moreSpecificConverter = options.Converters.FirstOrDefault(c => c != this && c.CanConvert(typeToConvert));
if (moreSpecificConverter is not null)
return moreSpecificConverter is JsonConverterFactory moreSpecificFactory
? moreSpecificFactory.CreateConverter(typeToConvert, options)
: moreSpecificConverter;
return (JsonConverter?)Activator.CreateInstance(typeof(CustomObjectConverter<>).MakeGenericType(typeToConvert));
}
}