在自定义json转换器中访问.NET类的自定义属性

时间:2018-04-23 22:58:05

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

在我的项目中,我编写了一个自定义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类中设置为string属性的属性来格式化字符串属性。例如,假设我已经编写了这样的候选类,

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

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

这是我到目前为止所尝试的但没有运气,

  1. 不在CustomAttributes的{​​{1}}属性中 参数发送到objectType方法。

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

1 个答案:

答案 0 :(得分:0)

JsonConverter.ReadJson()无法使用包含对象的堆栈,因此您无法在ReadJson()内执行所需操作。

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

首先,让我们说您的数据模型,属性和JsonConverter如下所示(我必须修改一些内容以使您的代码编译并包含一些其他测试用例):< / p>

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对象是&#34;一组无序的名称/值对&#34;,因此在读取属性值时尝试访问包含的.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 Core Web API: Configure JSON serializer settings on action or controller level How to set custom JsonSerializerSettings for Json.NET in MVC 4 Web API? ASP.NET Core API JSON serializersettings per request ,具体取决于您的要求和正在使用的框架版本。

  6. 示例工作.Net小提琴here