Newtonsoft.Json DefaultContractResolver从字典中删除密钥

问题描述

我有以下课程(我不想更改课程以解决问题。.):

public class Test
{
    public Dictionary<string,string> Data;    

    [PrivateField]
    [JsonIgnore]
    public string Name { get { return Data["Name"]; } set { Data.Add("Name",value); } }

    public string NotPrivate { get { return Data["NotPrivate"]; } set { Data.Add("NotPrivate",value); } }
}

我想在序列化过程中从数据属性删除特定的键 ,在我的情况下,字典中的键为“名称”,因为它被标记为私有。

    var test = new test();
    var settings = new JsonSerializerSettings();
    settings.ContractResolver = new IgnorePrivatePropertiesContractResolver();

    var places = JsonConvert.SerializeObject(test,settings);


public class IgnorePrivatePropertiesContractResolver : DefaultContractResolver

在我尝试过的IgnorePrivatePropertiesContractResolver中:

override IList<JsonProperty> CreateProperties(Type type,MemberSerialization memberSerialization)

但是我无法从JsonProperty中获取字典。

我也尝试过:

JsonProperty CreateProperty(MemberInfo member,MemberSerialization memberSerialization)

但是我无法从MemberInfo中获取字典。

解决方法

Json.NET没有内置功能来在序列化时从字典中过滤键。因此,您将需要创建一个进行必要过滤的custom JsonConverter。然后,由于您无法更改课程,因此需要使用custom contract resolver应用转换器。

首先,自定义JsonConverter

public class KeyFilteringDictionaryConverter<TKey,TValue> : JsonConverter<IDictionary<TKey,TValue>>
{
    readonly HashSet<TKey> toSkip;

    public KeyFilteringDictionaryConverter(IEnumerable<TKey> toSkip) => this.toSkip = toSkip?.ToHashSet() ?? throw new ArgumentNullException(nameof(toSkip));

    public override void WriteJson(JsonWriter writer,IDictionary<TKey,TValue> value,JsonSerializer serializer) => serializer.Serialize(writer,new KeyFilteringDictionarySurrogate<TKey,TValue>(value,toSkip));
    public override bool CanRead => false;
    public override IDictionary<TKey,TValue> ReadJson(JsonReader reader,Type objectType,TValue> existingValue,bool hasExistingValue,JsonSerializer serializer) => throw new NotImplementedException();
}

public class KeyFilteringDictionarySurrogate<TKey,TValue> : IReadOnlyDictionary<TKey,TValue>
{
    readonly IDictionary<TKey,TValue> dictionary;
    readonly HashSet<TKey> toSkip;

    public KeyFilteringDictionarySurrogate(IDictionary<TKey,TValue> dictionary,IEnumerable<TKey> toSkip) : this(dictionary,toSkip ?.ToHashSet()) { }
    public KeyFilteringDictionarySurrogate(IDictionary<TKey,HashSet<TKey> toSkip) 
    { 
        this.dictionary = dictionary ?? throw new ArgumentNullException(nameof(dictionary));
        this.toSkip = toSkip ?? throw new ArgumentNullException(nameof(toSkip));
    }

    public bool ContainsKey(TKey key) => !toSkip.Contains(key) && dictionary.ContainsKey(key);
    public bool TryGetValue(TKey key,out TValue value)
    {
        if (toSkip.Contains(key))
        {
            value = default(TValue);
            return false;
        }
        return dictionary.TryGetValue(key,out value); 
    }
    public TValue this[TKey key] => toSkip.Contains(key) ? throw new KeyNotFoundException() : dictionary[key];
    public IEnumerable<TKey> Keys => this.Select(p => p.Key);
    public IEnumerable<TValue> Values => this.Select(p => p.Value);
    public int Count => this.Count(); // Could be made faster?
    public IEnumerator<KeyValuePair<TKey,TValue>> GetEnumerator() => dictionary.Where(p => !toSkip.Contains(p.Key)).GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

接下来,自定义ContractResolver。创建所有属性信息后,最简单的应用自定义转换器的位置将位于CreateProperties()中。覆盖CreateObjectContract()也可以。

public class IgnorePrivatePropertiesContractResolver : DefaultContractResolver
{
    protected override IList<JsonProperty> CreateProperties(Type type,MemberSerialization memberSerialization)
    {
        var jsonProperties = base.CreateProperties(type,memberSerialization);
        // Apply to all string-keyed dictionary properties named "Data" that do not already have a converter
        foreach (var dataProperty in jsonProperties.Where(p => p.PropertyName == "Data" && p.Converter == null))
        {
            var keyValuePairTypes = dataProperty.PropertyType.GetDictionaryKeyValueTypes().ToList();
            if (keyValuePairTypes.Count == 1 && keyValuePairTypes[0][0] == typeof(string))
            {
                // Filter all properties with PrivateFieldAttribute applied
                var ignoreProperties = jsonProperties.Where(p => p.AttributeProvider.GetAttributes(typeof(PrivateFieldAttribute),true).Any()).Select(p => p.PropertyName).ToHashSet();
                if (ignoreProperties.Count > 0)
                {
                    dataProperty.Converter = (JsonConverter)Activator.CreateInstance(typeof(KeyFilteringDictionaryConverter<,>).MakeGenericType(keyValuePairTypes[0]),new object [] { ignoreProperties });
                }
            }
        }
        return jsonProperties;
    }
}

public static class TypeExtensions
{
    public static IEnumerable<Type> GetInterfacesAndSelf(this Type type)
        => (type ?? throw new ArgumentNullException()).IsInterface ? new[] { type }.Concat(type.GetInterfaces()) : type.GetInterfaces();

    public static IEnumerable<Type []> GetDictionaryKeyValueTypes(this Type type)
        => type.GetInterfacesAndSelf().Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IDictionary<,>)).Select(t => t.GetGenericArguments());
}

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property,AllowMultiple=false)]
public class PrivateFieldAttribute : System.Attribute { }

最后,使用如下:

var test = new Test
{
    Data = new Dictionary<string,string>
    {
        {"SomeAdditionalKey","Some additional value"},},Name = "private value",NotPrivate = "public value",};

var resolver = new IgnorePrivatePropertiesContractResolver();

var settings = new JsonSerializerSettings
{
    ContractResolver = resolver,};

var json = JsonConvert.SerializeObject(test,Formatting.Indented,settings);

注意:

  • 此解决方案实际上并未从字典中删除私钥,只是跳过了对其进行序列化。通常,序列化实际上不会修改要序列化的对象。如果您确实需要在序列化过程中实际修改字典,则合同解析器可以应用自定义OnSerializing()方法来执行此操作,而不是应用转换器。

  • 您的问题并未确切指定如何确定应将KeyFilteringDictionaryConverter应用于的属性。当同一类中还有一个成员为Dictionary<string,TValue>的成员时,我将其应用于所有名为"Data"的{​​{1}}属性。您可以将其限制为PrivateFieldAttribute,或使用任何其他适合您的逻辑,只要对于任何类型Test的转换器仅应用于类型为IDictionary<string,TValue>的属性。 / p>

  • 您可能想statically cache the contract resolver for best performance

  • 转换器在反序列化时不会尝试删除私有属性,因为您的问题不需要这样做。

  • 您的TValue类在序列化时包含Test 两次的值:一次作为属性,一次作为字典属性的成员:

    NotPrivate

    您可能希望将其从一个位置或另一个位置排除。

  • 您写道,我无法从JsonProperty中获取字典。

    是正确的。合同解析器定义了如何将.Net 类型映射到JSON。因此,在合同创建期间特定实例不可用。这就是为什么必须应用自定义转换器的原因,因为在序列化和反序列化期间会将特定实例传递到转换器中。

演示小提琴here

,
public class IgnorePrivatePropertiesContractResolver : DefaultContractResolver
{

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

        if (!(type.Name.Equals(typeof(Test).Name)))
        {
            return properties;
        }

        var dataProperty = properties.FirstOrDefault(prop => prop.PropertyName.Equals("Data"));

        if (dataProperty == null)
            return properties;

        var privatePropertiesNames = properties.Where(prop => prop.AttributeProvider.GetAttributes(false).OfType<PrivateFieldAttribute>().Any())
            .Select(privateProperty => privateProperty.PropertyName);

        
        dataProperty.ValueProvider = new JsonPropertyExtraDataValueProvider(privatePropertiesNames);

        return properties;
    }

}

public class JsonPropertyExtraDataValueProvider : IValueProvider
{

    private IEnumerable<string> _privateKeys;

    public JsonPropertyExtraDataValueProvider(IEnumerable<string> privateKeys)
    {

        _privateKeys = privateKeys;
    }

    public void SetValue(object target,object value)
    {
        throw new NotImplementedException();
    }

    public object GetValue(object target)
    {
        Dictionary<string,string> value = ((Test)target).ExtraData.ToDictionary(pr => pr.Key,pr => pr.Value);
        foreach (var privateKey in _privateKeys)
            value.Remove(privateKey);
        return value;
    }
}