c# - 如何使用合约解析器和值提供者在反序列化过程中自定义值设置

标签 c# json.net json-deserialization value-provider

我未能完成在序列化为 JSON 时加密某些字段以及在反序列化为特定 C# 类期间解密这些字段的任务。

我把问题归结为最基本的问题,就是我无法通过操作值来自定义特定字段的反序列化,而且我也不知道原因。我正在为每个字段使用自定义契约(Contract)解析器和自定义值提供程序。我可以看到 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:我序列化/反序列化的对象是不可变的(没有公共(public) setter ),这是一个要求,因为我需要支持此类对象。正如评论指出的那样,没有执行 SetValue 是有道理的

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

更新 3:鉴于问题,所选答案绝对正确。然而,在进一步调查之后,我决定采用一种稍微不同的方法,该方法适用于不可变类和可变类,以防有人处于类似情况。我现在使用合约解析器和 json 转换器的组合来代替使用值提供程序,这样通过合约解析器,我可以根据类型决定如何序列化/反序列化,并且使用 json 转换器,我可以在序列化/反序列化期间访问值并进行操作根据需要。

基本上,在我的契约(Contract)解析器上,我重写了创建属性的方法(我可以在其中访问原始 Type 属性),并有选择地指定要使用的 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不要求 SampleInner 的属性是 SampleInner不可变,因此没有可调用的 set 方法。相反,JSON 属性按名称(模数大小写)与该类型的单个参数化构造函数的参数进行匹配,反序列化为匹配参数的类型,然后传递到构造函数中,如所述 here 所示。 .

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

那么,你有什么选择?

首先,您可以使用默认构造函数使类型可变。只要用适当的属性标记,setter 和构造函数就可以是私有(private)的:

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 包装起来。在某些装饰器中调用的方法进行必要的预处理,但是由于JsonObjectContract.ParameterizedCreator这一事实而使这变得困难。是非公开。因此,您无法直接访问 Json.NET 选择的参数化构造函数来装饰它。但是,您可以确定其参数,这些参数由 JsonObjectContract.CreatorParameters 指定。 。填充此集合后,则 OverrideCreator设置或设置( secret )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;
    }
}

注释:

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

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

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

演示 fiddle #2 here .

关于c# - 如何使用合约解析器和值提供者在反序列化过程中自定义值设置,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57030272/

相关文章:

c# - 如何反序列化 JSON 中名为 "default"的 JSON 字段? C#

c# - 支持多种编程语言的 ORM(对象关系管理器)解决方案

c# - 在 List<interface> 或 List<abstract Class> 中设置/确定具体类型的最佳做法是什么?

c# - 未捕获的类型错误 : Cannot read property 'mData' of undefined for JQuery DataTable

c# - 如何在 asp.net web api 中返回 json 错误消息?

arrays - 如何从JSON传递List <dynamic>到List <MyModel>?

c# - 将某个 JSON 值映射到枚举值 C#

c# - Microsoft 功能区不在设计器中呈现

c# - IEnumerable 中每个项目的自定义 json 序列化

json - 包含空值的 BsonDocument 的 Newtonsoft.Json.JsonConvert.SerializeObject 因 InvalidCastException 而失败