我有一个泛型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相关问答推荐

自定义JsonEditor,用于将SON序列化为抽象类

获取ASP.NET核心身份认证cookie名称

如何从泛型方法返回一个可为空的T,其中T:notnull?

实现List T,为什么LINQ之后它不会返回MyList?<>(无法强制转换WhereListIterator `1类型的对象)'

将XPS转换为PDF C#

默认情况下,.NET通用主机(Host.CreateDefaultBuilder)中是否包含UseConsoleLifetime?

ASP.NET核心REST API返回一个非常大的数字

如何将ASP.NET Core 2.1(在.NET框架上运行)更新到较新的版本?

共享暂存/生产环境中Azure事件中心的建议配置

JsonSchema.Net删除假阳性判断结果

C#动态设置ServerReport报表参数

为什么此名称不再被识别?名称不存在于当前上下文中?

CRL已过期,但ChainStatus告诉我RevocationStatus未知

如何从非异步任务中正确返回TypeResult

Blazor服务器项目中的Blazor/.NET 8/Web API不工作

在等待OnGetAsync时打开Razor Page显示微调器

如何解决System.StackOverflowException:抛出System.StackOverflowException类型的异常.&# 39;生成随机代码时发生异常?

如何使用moq和xUnit对删除操作进行单元测试?

阻止CLR释放已封送的双字符指针的内存?

PowerShell:如何[引用]数组中的元素?