我有一个泛型ValueObject类,如下所示

public abstract class ValueObject : 
    IEquatable<ValueObject>, 
    IComparable, 
    IComparable<ValueObject> {

    protected abstract IEnumerable<object> GetEqualityComponents();
}

此类没有任何属性.为了实现一个值对象,我可以创建任何新类,如下所示

public sealed class PersonName : ValueObject {

    private PersonName(string firstName, string middleName, string lastName) {
        FirstName = firstName;
        MiddleName = middleName;
        LastName = lastName;
    }

    public string FirstName { get; private set; }
    public string MiddleName { get; private set; }
    public string LastName { get; private set; }

    public static PersonName Create(string firstName, string middleName, string lastName) {
        //Validate input and create object
        return new PersonName(firstName, middleName, lastName);
    }

    protected override IEnumerable<object> GetEqualityComponents() {
        yield return FirstName;
        yield return MiddleName;
        yield return LastName;
    }
}

请注意

  • 对象具有单个私有构造函数
  • 属性具有私有setter
  • 对象创建通过单个Create静态方法进行

我正在努力使用Newtonsoft.json进行json序列化和反序列化.我也试图理解this answer和它所指的original source code背后的逻辑.

我正在努力实现的是如何制作一个通用的JsonConverter,与链接答案中显示的相比,它将能够

  • 获取静态创建方法
  • 在按名称映射参数的反序列化过程中,使用该方法构造对象.

有没有办法实现这一点?

EDIT

我忘了提到,我可以在任何其他类中使用值对象进行序列化,例如,当在这样的类中使用时

public class Individual {
    public PersonName Name { get; }
    public string Citizenship { get; }
}

我可以像这样序列化json

{
    "$type":"Entities.Individual, myassembly",
    "name":{
        "$type":"ValueObjects.PersonName, myassembly",
        "firstName": "John",
        "middleName": "",
        "lastName": "Wayne"
    },
    "citizenship": "American"
}

推荐答案

与自定义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进行验证.

    详细信息请参见100101.

  • 解析器假定每个ValueObject子类型只有一个Create()方法.如果您曾经引入具有多个Create()个方法的值对象,则必须相应地更新CreateObjectContract(),例如,通过 Select 具有最多参数的Create()个方法.

  • 有关何时使用自定义转换器与何时使用自定义解析器的指导,请参见100.在实践中,自定义解析器往往在TypeNameHandling和引用保留方面发挥得更好.

演示小提琴here.

Csharp相关问答推荐

如何从C#中有类.x和类.y的类列表中映射List(字符串x,字符串y)?

是否可以将gltf转换为字节数组,然后将字节数组转换回文件?

为什么使用DXGI输出复制和Direct 3D时捕获的图像数据全为零?

FromServices不使用WebAppliationFactory程序>

如何将字节数组转换为字符串并返回?

Unity如何在PlayerPrefs中保存数据?

为什么将鼠标悬停在DateTimeOffset上只显示Hour值?

(乌龙)1&#比c#中的UL&#慢吗?

如何从ASP.NET核心MVC视图和Blazor传递数据

在实体框架中处理通用实体&S变更跟踪器

方法从数据表中只 Select 一个条件?

集合表达式没有目标类型

HttpRequestMessage.SetPolicyExecutionContext不会将上下文传递给策略

如何在不复制或使用输出的情况下定义项目依赖

如何在特定环境中运行dotnet测试?

用于请求用户返回列表的C#Google API

当我手动停止和关闭系统并打开时,Windows服务未启动

流畅的验证--如何为属性重用规则?

Xamarin.Forms项目中缺少MainPage.xaml

有没有更好的方法来使用LINQ获取整行的计算组