如何使用契约解析器和值提供者在反序列化期间自定义值设置

How to customize value setting during deserialization with contract resolver and value provider

我在序列化为 JSON 时加密某些字段以及在反序列化为特定 C# 期间解密这些字段的任务失败 class。

我把问题归结为最基本的问题,就是无法通过操作值自定义特定字段的反序列化,我也不知道是什么原因。我正在为每个字段使用自定义合同解析器和自定义值提供程序。我可以看到 GetValue 函数已执行,但 SetValue 从未执行过。

代码示例:

class Program
{
    static void Main(string[] args)
    {

        var text = "This is text";
        var number = 1;
        var anotherText = "This is another text";
        var anotherNumber = 2;
        var sampleInner = new SampleInner(anotherText, anotherNumber);
        var sample = new SampleMessage(text, number, sampleInner);

        var myCustomContractResolver = new MyCustomContractResolver();
        var jsonSettings = GetJsonSettings(myCustomContractResolver);

        Console.WriteLine("Serializing..");
        var json = JsonConvert.SerializeObject(sample, jsonSettings);
        Console.WriteLine(json);

        Console.WriteLine("Deserializing..");
        var sampleDeserialized = JsonConvert.DeserializeObject(json, typeof(SampleMessage), jsonSettings);
        Console.WriteLine(sampleDeserialized);

        Console.ReadLine();
    }

    private static JsonSerializerSettings GetJsonSettings(IContractResolver contractResolver)
    {
        var jsonSettings =
            new JsonSerializerSettings
            {
                ContractResolver = contractResolver
            };
        return jsonSettings;
    }
}

自定义合同解析器:

public class MyCustomContractResolver
    : DefaultContractResolver
{
    public MyCustomContractResolver()
    {
        NamingStrategy = new CamelCaseNamingStrategy();
    }

    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        var jsonProperties = base.CreateProperties(type, memberSerialization);

        foreach (var jsonProperty in jsonProperties)
        {
            var propertyInfo = type.GetProperty(jsonProperty.UnderlyingName);
            var defaultValueProvider = jsonProperty.ValueProvider;
            jsonProperty.ValueProvider = new MyValueProvider(defaultValueProvider);
        }

        return jsonProperties;
    }
}

以及在反序列化期间从不执行其 SetValue 的自定义值提供程序:

public class MyValueProvider
    : IValueProvider
{
    private readonly IValueProvider _valueProvider;

    public MyValueProvider(IValueProvider valueProvider)
    {
        _valueProvider = valueProvider;
    }

    public void SetValue(object target, object value)
    {
        //This is not executed during deserialization. Why?
        _valueProvider.SetValue(target, value);
        Console.WriteLine($"Value set: {value}");
    }

    public object GetValue(object target)
    {
        var value = _valueProvider.GetValue(target);
        Console.WriteLine($"Value get: {value}");
        return value;
    }
}

Here is the sample code to reproduce it 以备不时之需。

希望有人能让我知道我错过了什么:)

更新 1: 对象 I serialize/deserialize 是不可变的(没有 public 设置器),这是一项要求,因为我需要支持此类对象。正如评论指出的那样,没有 SetValue 执行

是有道理的

更新 2: 感谢@dbc 的精彩回答不,我知道反序列化为不可变对象的一个​​很好的解决方法。 The final version code after the accepted answer.

更新 3:给定问题,所选答案绝对正确。然而,在进一步调查之后,我决定采用一种稍微不同的方法,该方法适用于不可变和可变 classes,以防有人遇到类似情况。我现在使用合同解析器和 json 转换器的组合,而不是使用价值提供者,这样我可以通过合同解析器来决定如何根据类型 serialize/deserialize,以及 json 转换器我可以在 serialization/deserialization 期间访问值并根据需要进行操作。

基本上,在我的合同解析器上,我覆盖了创建属性的方法(我可以在其中访问我的原始类型属性)并有选择地指定要使用的 json 转换器。

protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
    var jsonProperties = base.CreateProperties(type, memberSerialization);

    //Filter here based on type, attribute or whatever and if want to customize a specific property type:
    foreach (var jsonProperty in jsonProperties)
    {
        jsonProperty.Converter = new MyJsonConverter();
    }

    return jsonProperties;
}

并且在 MyJsonConverter 中,我们可以选择写入 json 或从 json 读取时执行的操作:

public class MyJsonConverter
    : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        //I can do whatever with value
        var finalValue = $"{value}-edited";
        writer.WriteValue(finalValue);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        // I can do whatever with the value
        var value = (string)reader.Value;
        var newValue = "whatever";
        return newValue;
    }

    public override bool CanWrite => true;

    public override bool CanRead => true;

    public override bool CanConvert(Type objectType)
    {
        return true;
    }
}

IValueProvider.SetValue is not called for the properties of SampleInner is that SampleInner is immutable and so there are no set methods to be called. Instead, the JSON properties are matched to arguments of the type's single parameterized constructor by name (modulo case), deserialized to the type of the matched argument, and then passed into the constructor as explained here.

的原因

即使您要使属性可变,也不会为已经传递给构造函数的属性调用 setter,因为 Json.NET 做出(合理的)假设传递 属性 构造函数中的值足以设置 属性 的值。

那么,你有什么选择?

首先,您可以使用默认构造函数使您的类型可变。 setters 和构造函数可以是私有的,只要它们标有适当的属性:

public class SampleInner
{
    [JsonProperty] // Adding this attribute informs Json.NET that the private setter can be called.
    public string AnotherText { get; private set; }

    [JsonProperty]
    public int AnotherNumber { get; private set; }

    [JsonConstructor] // Adding this attribute informs Json.NET that this private constructor can be called
    private SampleInner() { }

    public SampleInner(string anotherText, int anotherNumber)
    {
        this.AnotherText = anotherText;
        this.AnotherNumber = anotherNumber;
    }       
}

现在有 setter 个要调用,您的 MyValueProvider.SetValue() 将被调用。演示 fiddle #1 here.

其次,如果你不能这样修改你的类型,你可以包装 constructor method called in some decorator that does the necessary pre-processing, however this is made difficult by the fact that JsonObjectContract.ParameterizedCreator is nonpublic. Thus you cannot get direct access to the parameterized constructor that Json.NET has chosen, in order to decorate it. You can, however, determine its arguments, which are specified by JsonObjectContract.CreatorParameters. When this collection is populated then either OverrideCreator 设置或(秘密)ParameterizedCreator 设置。这允许插入必要的逻辑,如下所示:

public class MyCustomContractResolver : DefaultContractResolver
{
    public MyCustomContractResolver() { NamingStrategy = new CamelCaseNamingStrategy(); }

    static ObjectConstructor<Object> GetParameterizedConstructor(JsonObjectContract contract)
    {
        if (contract.OverrideCreator != null)
            return contract.OverrideCreator;

        // Here we assume that JsonSerializerSettings.ConstructorHandling == ConstructorHandling.Default
        // If you would prefer AllowNonPublicDefaultConstructor then you need to remove the check on contract.DefaultCreatorNonPublic
        if (contract.CreatorParameters.Count > 0 && (contract.DefaultCreator == null || contract.DefaultCreatorNonPublic))
        {
            // OK, Json.NET has a parameterized constructor stashed away in JsonObjectContract.ParameterizedCreator
            // https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Serialization/JsonObjectContract.cs#L100
            // But, annoyingly, this value is internal so we cannot get it!
            // But because CreatorParameters.Count > 0 and OverrideCreator == null we can infer that such a constructor exists, and so call it using Activator.CreateInstance

            return (args) => Activator.CreateInstance(contract.CreatedType, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, args, CultureInfo.InvariantCulture);
        }

        return null;
    }

    static ObjectConstructor<Object> CustomizeConstructor(JsonObjectContract contract, ObjectConstructor<Object> constructor)
    {
        if (constructor == null)
            return null;
        return (args) =>
        {
            // Add here your customization logic.
            // You can match creator parameters to properties by property name if needed.
            foreach (var pair in args.Zip(contract.CreatorParameters, (a, p) => new { Value = a, Parameter = p }))
            {
                // Get the corresponding property in case you need to, e.g., check its attributes:
                var property = contract.Properties[pair.Parameter.PropertyName];

                if (property == null)
                    Console.WriteLine("Argument {0}: Value {1}", pair.Parameter.PropertyName, pair.Value);
                else
                    Console.WriteLine("Argument {0} (corresponding to JsonProperty {1}): Value {2}", pair.Parameter.PropertyName, property, pair.Value);
            }
            return constructor(args);
        };
    }

    protected override JsonObjectContract CreateObjectContract(Type objectType)
    {
        var contract = base.CreateObjectContract(objectType);

        contract.OverrideCreator = CustomizeConstructor(contract, GetParameterizedConstructor(contract));

        return contract;
    }

    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        var jsonProperties = base.CreateProperties(type, memberSerialization);

        foreach (var jsonProperty in jsonProperties)
        {
            var defaultValueProvider = jsonProperty.ValueProvider;
            jsonProperty.ValueProvider = new MyValueProvider(defaultValueProvider);
        }

        return jsonProperties;
    }
}

备注:

  • 如果默认构造函数存在但是是非公共的,上面的契约解析器假定它没有被使用。如果您希望使用非公共默认构造函数,则需要设置 JsonSerializerSettings.ConstructorHandling == ConstructorHandling.AllowNonPublicDefaultConstructor 并修改上面 GetParameterizedConstructor() 中的代码以删除对 contract.DefaultCreatorNonPublic:

    的检查
        if (contract.CreatorParameters.Count > 0 && contract.DefaultCreator == null)
    
  • 请求增强以允许访问和自定义 JsonObjectContract.ParameterizedCreator 是合理的。

    (我想你可以尝试通过反射直接访问 JsonObjectContract.ParameterizedCreator...)

演示 fiddle #2 here.