是否可以使用属性扩展 JsonProperty 属性 class 以影响不同的序列化?

Is it possible to extend the JsonProperty attribute class with properties to affect different serializations?

我的问题类似于这个问题:How to ignore JsonProperty(PropertyName = "someName") when serializing json? 其中的解决方案适用于我,但我很想知道是否可以使用更多属性扩展 [JsonProperty] 属性(使用 Newtonsoft.json)?

一些背景:
我有一个应用程序(我们称它为 SmartModel),它根据用户输入在 c# 中生成一个物理模型。 SmartModel 由许多具有许多属性的 class 组成(例如 Pipe class 具有 LengthDiameter 等属性)。 SmartModel 写出一个 json 类型的 DTO,用于单独的应用程序以解决问题(我们称它为 Solver 和 DTO,SolverDTO)。然而,除了这个 SmartModel 还写出一个不同的 DTO 用于保存和打开目的(方便地称为 SmartModelDTO)。

在这方面,在SmartModel中的某些属性(例如[JsonProperty(SolverPropertyName = "someName")])之上有一个装饰器,然后设置一个合同解析器来序列化并写出(以json格式)会很方便):

  1. 生成SolverDTO时的SolverPropertyName
  2. 生成SmartModelDTO时UnderlyingName

(其中 UnderlyingName 默认情况下已经是 JsonProperty 的 属性 并且 SolverPropertyName 应该扩展 JsonProperty 的 属性与).


编辑

这里有一个最小的代码示例来解释我正在努力实现的目标:

我有一个名为 Pipe 的示例 class,如下所示:

class Pipe
{
    [JsonProperty(PropertyName = "z_axis_dimension")]
    public double Length { get; set; }

    [JsonProperty(PropertyName = "cross_sectional_dimension")]
    public double Diameter { get; set; }
}

我想以两种不同的方式进行序列化(将来可能更多)。对于 SmartModelDTO,我希望序列化程序使用 UnderlyingProperty,但对于 SolverDTO,我希望在 JsonProperty 属性中使用 PropertyName。为此,可以实施以下合同解析器:

class IgnoreJsonPropertyNameContractResolver : DefaultContractResolver
{
    protected override IList<JsonProperty> CreateProperties(
        Type type, 
        MemberSerialization memberSerialization)
    {
        IList<JsonProperty> list = base.CreateProperties(
            type, 
            memberSerialization);

        foreach (JsonProperty prop in list)
        {
            prop.PropertyName = prop.UnderlyingName;
        }

        return list;
    }
}

使用示例如下:

// Instance of Pipe:
Pipe pipe = new()
{
    Length = 10.2,
    Diameter = 5.5,
};

// Set some Json serializer settings:
JsonSerializerSettings jsonSerializerSettings = new();
jsonSerializerSettings.Formatting = Formatting.Indented;

// Serialize Pipe where property values are obtained from the
// JsonProperty PropertyName:
string solverJsonString = JsonConvert.SerializeObject(
    pipe, jsonSerializerSettings);
Console.WriteLine($"Serialized string for SolverDTO:");
Console.WriteLine($"{solverJsonString}");
Console.WriteLine();

// Set a new contract resolver to return the
// JsonProperty UnderlyingName instead of the PropertyName:
jsonSerializerSettings.ContractResolver =
    new IgnoreJsonPropertyNameContractResolver();

// Serialize Pipe where property values are obtained from the
// JsonProperty UnderlyingName:
string smartModelJsonString = JsonConvert.SerializeObject(
    pipe, jsonSerializerSettings);
Console.WriteLine($"Serialized string for SmartModelDTO:");
Console.WriteLine($"{smartModelJsonString}");

Console.ReadLine();

给出以下输出:

Serialized string for SolverDTO:
{
  "z_axis_dimension": 10.2,
  "cross_sectional_dimension": 5.5
}

Serialized string for SmartModelDTO:
{
  "Length": 10.2,
  "Diameter": 5.5
}

但是,我希望在 Pipe 中有一个功能来标记属性,例如:

class Pipe
{
    [JsonProperty(
    SolverPropertyName = "z_axis_dimension", 
    APIPropertyName = "distance")]
    public double Length { get; set; }

    [JsonProperty(
    SolverPropertyName = "cross_sectional_dimension", 
    APIPropertyName = "diameter")]
    public double Diameter { get; set; }
}

然后设置与上述类似的不同合同解析器来序列化 Pipe 对象,但一个序列化使用 SolverPropertyName,另一个序列化 APIPropertyName,等等...

可以扩展 [JsonProperty] class 例如除了 PropertyName?

之外,还具有 SolverPropertyNameAPIPropertyName 等属性

由于缺少代码和示例(在撰写本文时),我不确定我是否完全符合您的要求。但这将用于从同一基础生成不同的序列化输出。

定义一个包含所有属性的接口

public interface IBase
{
    string PropertyOne { get; set; }
    string PropertyTwo { get; set; }
    string PropertyThree { get; set; }
    string PropertyFour { get; set; }
}

然后你的第一个 class 被序列化,然后使用我们定义的接口。 这里我们使用 JsonProperty 属性来定义我们想要的输出名称,这与属性的实际名称不同。

我们还使用 NewtonSoft 的 ShouldSerialize 功能来抑制输出第四个 属性,这在本例中是我们不想要的。

public class First: IBase
{
    [JsonProperty("PropertyNumber1")]
    public string PropertyOne { get; set; }

    [JsonProperty("PropertyNumber2")]
    public string PropertyTwo { get; set; }

    [JsonProperty("PropertyNumber3")]
    public string PropertyThree { get; set; }

    public string PropertyFour { get; set; }
    public bool ShouldSerializePropertyFour() { return false; }
}

然后我们类似地定义我们的另一个class,但是这里我们抑制第一个属性,并通过JsonProperty

给其余的一个不同的名字
public class Second : IBase
{
    [JsonProperty("SecondProperty")]
    public string PropertyTwo { get; set; }

    [JsonProperty("ThirdProperty")]
    public string PropertyThree { get; set; }

    [JsonProperty("FourthProperty")]
    public string PropertyFour { get; set; }

    public string PropertyOne { get; set; }
    public bool ShouldSerializePropertyOne() { return false; }
}

然后当我们序列化 classes 时,我们只得到我们想要的属性,而且它们有不同的名称。

除了使用接口,你还可以使用抽象 class 来做同样的事情,但在这里你需要在抽象 class 中将属性定义为 virtual 并在中覆盖它们child classes。请注意,为了使 ShouldSerialize 起作用,您需要先覆盖它,然后再抑制它!

我会将其添加为单独的答案。我认为我提供的另一个答案对您仍然有效,并且代码更简洁易读。

下面的这个方法可行,但它(恕我直言)比较混乱(可能会改进)并且可能导致更多错误。

Newtonsoft 的 JsonProperty 属性是密封的,因此我们无法覆盖它。这意味着我们首先需要创建自己的属性。 在这里我们定义了两个属性,它们对应于您要使用的名称。

[AttributeUsage(AttributeTargets.Property)]
public class MyJsonPropertyAttribute : Attribute 
{
    public string SolverPropertyName { get; set; }
    public string APIPropertyName { get; set; }
}

然后你可以这样装饰你的class

public class Pipe
{
    [MyJsonProperty(
    SolverPropertyName = "z_axis_dimension",
    APIPropertyName = "distance")]
    public double Length { get; set; }

    [MyJsonProperty(
    SolverPropertyName = "cross_sectional_dimension",
    APIPropertyName = "diameter")]
    public double Diameter { get; set; }
}

接下来我们需要定义我们将调用的自定义合同解析器 DualNameContractResolver。在构造过程中,我们需要指定要使用的属性名称 属性。您也可以将其更改为枚举(这样可以减少错误)。 如果您指定了错误的名称,则将应用 Newtonsoft 的默认序列化。

public class DualNameContractResolver : DefaultContractResolver
{
    private readonly string _propertyDecoratorToUse;

    public DualNameContractResolver(string propertyDecoratorToUse)
    {
        _propertyDecoratorToUse = propertyDecoratorToUse ?? throw new ArgumentNullException(nameof(propertyDecoratorToUse));
    }

    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var property = base.CreateProperty(member, memberSerialization);
        CustomAttributeNamedArgument namedArgument = member.CustomAttributes.SelectMany(c => c.NamedArguments).Where(w => w.MemberName.Equals(_propertyDecoratorToUse)).FirstOrDefault();
        if(namedArgument.TypedValue.Value != null)
            property.PropertyName = namedArgument.TypedValue.Value.ToString();

        return property;
    }
}

如您所见,在 CreateProperty 方法中,我们查找请求的 属性,如果找到则应用定义的值(即序列化名称)。

然后您可以像这样使用所有这些。

Pipe pipe = new()
{
    Length = 10.2,
    Diameter = 5.5,
};

var solverResolver = new DualNameContractResolver("SolverPropertyName");                
var apiResolver = new DualNameContractResolver("APIPropertyName");    
JsonSerializerSettings jsonSerializerSettings = new();
jsonSerializerSettings.Formatting = Formatting.Indented;

jsonSerializerSettings.ContractResolver = solverResolver;
string json1 = JsonConvert.SerializeObject(pipe, jsonSerializerSettings);

jsonSerializerSettings.ContractResolver = apiResolver;
string json2 = JsonConvert.SerializeObject(pipe, jsonSerializerSettings);