与自定义JsonConverter
不同,您可以编写一个custom contract resolver,它在从ValueObject
继承的对象中查找静态Create()
方法,并将调用它的委托分配给JsonObjectContract.OverrideCreator
.这样做之后,静态方法将被调用,而不是普通构造函数.
为此,首先创建以下合同解析器:
public class ValueObjectResolver : DefaultContractResolver
{
const string CreateMethodName = "Create";
protected override JsonObjectContract CreateObjectContract(Type objectType)
{
var contract = base.CreateObjectContract(objectType);
if (typeof(ValueObject).IsAssignableFrom(objectType))
{
try
{
var createMethod = objectType.GetMethod(CreateMethodName, BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
if (createMethod != null)
{
contract.OverrideCreator = (args) => createMethod.Invoke(null, args) ?? throw new InvalidOperationException($"null created object {objectType.Name}");
contract.CreatorParameters.Clear();
foreach (var param in CreateStaticConstructorParameters(createMethod, contract.Properties))
contract.CreatorParameters.Add(param);
}
}
catch (AmbiguousMatchException ex)
{
// TODO: Handle multiple Create() methods with dfferent argument lists however you want.
Debug.WriteLine(ex);
}
}
return contract;
}
JsonPropertyCollection CreateStaticConstructorParameters(MethodInfo createMethod, JsonPropertyCollection memberProperties)
{
// Adapted from https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Serialization/DefaultContractResolver.cs#L743
// By https://github.com/JamesNK/
// License: https://github.com/JamesNK/Newtonsoft.Json/blob/master/LICENSE.md
var parameterCollection = new JsonPropertyCollection(createMethod.DeclaringType!);
foreach (var parameterInfo in createMethod.GetParameters().Where(i => i.Name != null))
{
var matchingMemberProperty = MatchProperty(memberProperties, parameterInfo.Name, parameterInfo.ParameterType);
if (CreatePropertyFromConstructorParameter(matchingMemberProperty, parameterInfo) is {} property)
parameterCollection.AddProperty(property);
}
return parameterCollection;
}
static JsonProperty? MatchProperty(JsonPropertyCollection properties, string? name, Type type)
{
if (name == null)
return null;
var property = properties.GetClosestMatchProperty(name);
// must match type as well as name
if (property?.PropertyType == type)
return property;
return null;
}
}
要使用它,请按照docs的建议将实例静态缓存到某个位置:
public static DefaultContractResolver Resolver { get; } = new ValueObjectResolver()
{
NamingStrategy = new CamelCaseNamingStrategy(),
};
现在,您将能够往返包含ValueObject
个数据模型,如下所示:
Individual individual1 = new()
{
Name = PersonName.Create("John", "", "Wayne"),
Citizenship = "American",
};
JsonSerializerSettings settings = new()
{
ContractResolver = Resolver,
// Add other settings as required.
// TODO, CAUTION -- If you use TypeNameHandling you should write a custom serialization binder for security reasons.
// See https://www.newtonsoft.com/json/help/html/t_newtonsoft_json_typenamehandling.htm
TypeNameHandling = TypeNameHandling.Objects,
};
var json = JsonConvert.SerializeObject(individual1, Formatting.Indented, settings);
var individual2 = JsonConvert.DeserializeObject<Individual>(json, settings);
备注:
为简单起见,我直接拨打MethodInfo.Invoke()
.如果性能成为一个问题,您可以使用反射来制造一个委托.
C#record types也支持value-based equality.你可能会考虑是否可以使用它们,而不是发明类似的东西.
我注意到你用的是TypeNameHandling
.请注意,此设置存在安全风险.正如docs中所解释的那样:
当您的应用程序从外部源反序列化JSON时,应谨慎使用TypeNameHandling.当使用None以外的值进行反序列化时,传入类型应使用自定义SerializationBinder进行验证.
详细信息请参见100和101.
解析器假定每个ValueObject
子类型只有一个Create()
方法.如果您曾经引入具有多个Create()
个方法的值对象,则必须相应地更新CreateObjectContract()
,例如,通过 Select 具有最多参数的Create()
个方法.
有关何时使用自定义转换器与何时使用自定义解析器的指导,请参见100.在实践中,自定义解析器往往在TypeNameHandling
和引用保留方面发挥得更好.
演示小提琴here.