【问题标题】:Access custom attributes of .NET class inside custom json converter在自定义 json 转换器中访问 .NET 类的自定义属性
【发布时间】:2018-10-04 02:06:03
【问题描述】:

在我的项目中,我编写了一个自定义 json 转换器来修剪字符串属性中存在的空格。

这是我们将使用的典型类的示例,

public class Candidate
{
    public string CandidateName { get; set; }
}

这是我的自定义 json 转换器

public class StringSanitizingConverter : JsonConverter
{       
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(string);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue , JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.String)
            if (reader.Value != null)
            {
                string sanitizedString = (reader.Value as string).Trim();

                if (StringSanitizeOptions.HasFlag(StringSanitizeOptions.ToLowerCase))
                    sanitizedString = sanitizedString.ToLowerInvariant();

                return sanitizedString;
            }

        return reader.Value;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var text = (string)value;
        if (text == null)
            writer.WriteNull();
        else
            writer.WriteValue(text.Trim());
    }
}

使用我的自定义转换器,我现在可以使用我的“候选人”作为其参数之一,通过修剪发送到操作方法的任何空格来格式化字符串。

public void Post(ComplexType complexTypeParameter){
}

到目前为止一切正常。后来我想增强这个 json 转换器,以根据在 Candidate 类中设置为字符串属性的属性来格式化字符串属性。例如,假设我已经这样编写了我的候选类,

 public class Candidate
 {
     [StringSanitizingOptions(Option.ToLowerCase)]
     public string CandidateName { get; set; }
 }

如果我想根据 json 转换器内部的自定义属性配置来格式化类的字符串属性,我无法在自定义转换器的 ReadJson 方法中访问此自定义属性及其配置。

这是我迄今为止尝试过的,但没有运气,

  1. 不存在于objectTypeCustomAttributes 属性中
    参数发送到ReadJson() 方法。

  2. 试图查看是否可以在 ReadJson() 方法中提取属性的父类,以便我可以对类应用反射以提取赋予其任何属性的自定义属性,但我也无法提取。

【问题讨论】:

  • 我想根据json转换器内部的自定义属性配置来格式化类的字符串属性——我不明白你的意思。你能用minimal reproducible example 澄清一下吗?一般来说,Json.NET 是一个基于契约的序列化器;每种类型都有自己的合同,通常不是由父母签订的。但也许你想要NewtonSoft JsonConverter - Access other propertiesPassing additional information to a JsonConverter 之类的东西?
  • 从上面的代码中可以看出,我使用自定义转换器来修剪 Candidate.CandidateName 中存在的空白,这是默认的 require 行为。如果属性存在 [StringSanitizingOptions(Option.ToLowerCase)] 属性,我想要的基本上是将 Candidate.CandidateName 值转换为小写。对我来说,这里的挑战是我不确定如何访问自定义 JsonConverter 类的 ReadJson 方法中赋予 CandidateName 属性的自定义属性。
  • @dbc 我在链接dotnetfiddle.net/3mmNPj 中查看了您的小提琴代码。显然,您似乎已经编写了这段代码来自定义序列化过程。如果我想根据属性中存在的自定义属性自定义反序列化过程。您能否就如何实现这一目标发表一些想法?

标签: asp.net-web-api json.net


【解决方案1】:

包含对象的堆栈不适用于JsonConverter.ReadJson(),因此您无法在ReadJson() 中做您想做的事情。

相反,您可以创建一个custom contract resolver,它根据正在为其生成合同的对象的属性应用适当配置的StringSanitizingConverter 实例。

首先,假设您的数据模型、属性和JsonConverter 如下所示(我必须修改一些内容以使您的代码编译并包含一些额外的测试用例):

public class Candidate
{
    [StringSanitizingOptions(Option.ToLowerCase)]
    public string CandidateName { get; set; }

    [StringSanitizingOptions(Option.DoNotTrim)]
    public string StringLiteral { get; set; }

    public string DefaultString { get; set; }

    public List<string> DefaultStrings { get; set; }
}

[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Field | System.AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public class StringSanitizingOptionsAttribute : System.Attribute
{
    public Option StringSanitizeOptions { get; set; }

    public StringSanitizingOptionsAttribute(Option stringSanitizeOptions)
    {
        this.StringSanitizeOptions = stringSanitizeOptions;
    }
}

[Flags]
public enum Option
{
    Default = 0,
    ToLowerCase = (1<<0),
    DoNotTrim = (1<<1),
}

public static class StringSanitizeOptionsExtensions
{
    public static bool HasFlag(this Option options, Option flag)
    {
        return (options & flag) == flag;
    }
}

public class StringSanitizingConverter : JsonConverter
{
    readonly Option options;

    public StringSanitizingConverter() : this(Option.Default) { }

    public StringSanitizingConverter(Option options)
    {
        this.options = options;
    }

    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(string);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.String)
            if (reader.Value != null)
            {
                var sanitizedString = (reader.Value as string);

                if (!options.HasFlag(Option.DoNotTrim))
                    sanitizedString = sanitizedString.Trim();

                if (options.HasFlag(Option.ToLowerCase))
                    sanitizedString = sanitizedString.ToLowerInvariant();

                return sanitizedString;
            }

        return reader.Value;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        // WriteJson is never called with null
        var text = (string)value;

        if (!options.HasFlag(Option.DoNotTrim))
            text = text.Trim();

        writer.WriteValue(text);
    }
}

接下来,从How to add metadata to describe which properties are dates in JSON.Net中获取ConfigurableContractResolver,并定义扩展方法JsonContractExtensions.AddStringConverters()

public static class JsonContractExtensions
{
    public static JsonContract AddStringConverters(this JsonContract contract)
    {
        if (contract is JsonPrimitiveContract)
        {
            if (contract.UnderlyingType == typeof(string))
                contract.Converter = new StringSanitizingConverter();
        }
        else if (contract is JsonObjectContract)
        {
            var objectContract = (JsonObjectContract)contract;
            foreach (var property in objectContract.Properties)
            {
                if (property.PropertyType == typeof(string))
                {
                    var attr = property.AttributeProvider.GetAttributes(typeof(StringSanitizingOptionsAttribute), true)
                        .Cast<StringSanitizingOptionsAttribute>()
                        .SingleOrDefault();
                    if (attr != null)
                    {
                        property.Converter = property.MemberConverter = new StringSanitizingConverter(attr.StringSanitizeOptions);
                    }
                }
            }
        }
        return contract;
    }
}

public class ConfigurableContractResolver : DefaultContractResolver
{
    // This contract resolver taken from the answer to
    // https://stackoverflow.com/questions/46047308/how-to-add-metadata-to-describe-which-properties-are-dates-in-json-net
    // https://stackoverflow.com/a/46083201/3744182

    readonly object contractCreatedPadlock = new object();
    event EventHandler<ContractCreatedEventArgs> contractCreated;
    int contractCount = 0;

    void OnContractCreated(JsonContract contract, Type objectType)
    {
        EventHandler<ContractCreatedEventArgs> created;
        lock (contractCreatedPadlock)
        {
            contractCount++;
            created = contractCreated;
        }
        if (created != null)
        {
            created(this, new ContractCreatedEventArgs(contract, objectType));
        }
    }

    public event EventHandler<ContractCreatedEventArgs> ContractCreated
    {
        add
        {
            lock (contractCreatedPadlock)
            {
                if (contractCount > 0)
                {
                    throw new InvalidOperationException("ContractCreated events cannot be added after the first contract is generated.");
                }
                contractCreated += value;
            }
        }
        remove
        {
            lock (contractCreatedPadlock)
            {
                if (contractCount > 0)
                {
                    throw new InvalidOperationException("ContractCreated events cannot be removed after the first contract is generated.");
                }
                contractCreated -= value;
            }
        }
    }

    protected override JsonContract CreateContract(Type objectType)
    {
        var contract = base.CreateContract(objectType);
        OnContractCreated(contract, objectType);
        return contract;
    }
}

public class ContractCreatedEventArgs : EventArgs
{
    public JsonContract Contract { get; private set; }
    public Type ObjectType { get; private set; }

    public ContractCreatedEventArgs(JsonContract contract, Type objectType)
    {
        this.Contract = contract;
        this.ObjectType = objectType;
    }
}

public static class ConfigurableContractResolverExtensions
{
    public static ConfigurableContractResolver Configure(this ConfigurableContractResolver resolver, EventHandler<ContractCreatedEventArgs> handler)
    {
        if (resolver == null || handler == null)
            throw new ArgumentNullException();
        resolver.ContractCreated += handler;
        return resolver;
    }
}

那么,终于可以反序列化和序列化Candidate,如下:

var settings = new JsonSerializerSettings
{
    ContractResolver = new ConfigurableContractResolver
    {
    }.Configure((s, e) => { e.Contract.AddStringConverters(); }),
};

var candidate = JsonConvert.DeserializeObject<Candidate>(json, settings);

var json2 = JsonConvert.SerializeObject(candidate, Formatting.Indented, settings);

注意事项:

  1. 我不知道为什么包含对象的堆栈在 ReadJson() 中不可用。可能性包括:

    • 简单。
    • JSON 对象是“一组无序的名称/值对”,因此不能保证在读取属性值时尝试访问包含的 .Net 对象,因为可能尚未读取所需的信息还没有(甚至可能还没有构建父级)。
  2. 由于StringSanitizingConverter 的默认实例应用于为string 本身生成的合约,因此无需将转换器添加到JsonSerializer.SettingsConverters。这反过来可能会导致小的性能提升,因为CanConvert 将不再被调用。

  3. JsonProperty.MemberConverter 最近在 Json.NET 11.0.1 中被标记为过时,但在 Json.NET 的早期版本中必须设置为与 JsonProperty.Converter 相同的值。如果您使用的是 11.0.1 或更新的版本,您应该可以删除该设置。

  4. 您可能需要cache the contract resolver 以获得最佳性能。

  5. 要修改 中的 JsonSerializerSettings,请参阅 JsonSerializerSettings and Asp.Net CoreWeb API: Configure JSON serializer settings on action or controller levelHow to set custom JsonSerializerSettings for Json.NET in MVC 4 Web API?ASP.NET Core API JSON serializersettings per request,具体取决于您的要求和使用的框架版本。

示例工作 .Net fiddle here.

【讨论】:

  • 非常感谢您的出色回答。我能够应用此解决方案来解决我的问题。很有帮助。
  • 这正是我在星期五寻找的“相反,您可以做的是创建一个自定义合同解析器,该解析器根据对象的属性应用适当配置的 StringSanitizingConverter 实例正在生成合同。”我从dotnetfiddle.net/3mmNPj 上的代码中了解到这一点,并且能够实现我的目标。 :-)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-02-26
相关资源
最近更新 更多