Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
599 views
in Technique[技术] by (71.8m points)

asp.net web api - Access custom attributes of .NET class inside custom json converter

In my project, I have written a custom json converter to trim the white-spaces present in the string property.

Here is an example of the typical class we will use,

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

Here is my custom json converter

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());
    }
}

With my custom converter I am now able to format the string by trimming any white-spaces present sent to the action methods using my 'Candidate' as one of its parameter.

public void Post(ComplexType complexTypeParameter){
}

Everything worked well so far. I later wanted to enhance this json converter to format the string properties based on the attributes set to the string property in the Candidate class. for example, assume I have written my candidate class like this,

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

And if I wanted to format the string properties of a class based on the custom attribute configuration inside the json converter , I am not able to access this custom attribute and its configuration inside the ReadJson method of the custom converter.

Here is what I have tried so far but with no luck,

  1. Not present in the CustomAttributes property of the objectType
    parameter sent to the ReadJson() method.

  2. Was trying to see if I could extract the parent class of the property inside the ReadJson() method, so that I could apply reflection on the class to extract the custom attributes given to any of its property,but I could not extract that too.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

The stack of containing object(s) is not made available to JsonConverter.ReadJson(), thus you cannot do what you want inside ReadJson().

Instead, what you can do is to create a custom contract resolver that applies an appropriately configured instance of StringSanitizingConverter based on the properties of the object for which a contract is being generated.

First, let's say your data model, attribute, and JsonConverter look like the following (where I had to modify a few things to make your code compile and include some additional test cases):

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);
    }
}

Next, grab ConfigurableContractResolver from How to add metadata to describe which properties are dates in JSON.Net, and define the extension method 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;
    }
}

Then, finally you can deserialize and serialize Candidate as follows:

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);

Notes:

  1. I don't know why the stack of containing object(s) is not available in ReadJson(). Possibilities include:

    • Simplicity.
    • A JSON object is "an unordered set of name/value pairs", so trying to access the containing .Net object while reading a property value isn't guaranteed to work, since the information required might not have been read in yet (and the parent might not even have been constructed).
  2. Because a default instance of StringSanitizingConverter is applied to the contract generated for string itself, it is not necessary to add the converter to JsonSerializer.SettingsConverters. This in turn may lead to a small performance enhancement as CanConvert will no longer get called.

  3. JsonProperty.MemberConverter was recently marked obsolete in Json.NET 11.0.1 but must be set to the same value as JsonProperty.Converter in previous versions of Json.NET. If you are using 11.0.1 or a more recent version you should be able to remove the setting.

  4. You may want to cache the contract resolver for best performance.

  5. To modify JsonSerializerSettings in , see 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? or ASP.NET Core API JSON serializersettings per request, depending on your requirements and the version of the framework in use.

Sample working .Net fiddle here.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...