2019年9月10日 星期二

[Json.NET] Advanced JsonIgnore and ContractResolver


 Json.NET   ASP.NET Core   JsonIgnore    ContractResolver  

Introduction



Json.NET is one of the dependencies of Microsoft.AspNetCore.Mvc.Formatters.Json in ASP.NET Core 2.X.
In Json.NET, the IContractResolver interface provides a way to customize how the JsonSerializer serializes and deserializes .NET objects to JSON without placing attributes on your classes.



We are going to create an Advanced ContractRevolver and JsonIgnoreAttribute to IGNORE property’s serialization OR deserialization AT RUN TTIME. But first let us have a look at how to use the basic ContractResolver and JsonIgnoreAttribute.




The most useful implementation of IContractResolver are



We can apply the ContractResolver by setting Serialization Settings as following (in Startup.cs: Configuration)

services.AddMvc()
        .AddJsonOptions(options =>
        {
            options.SerializerSettings.ContractResolver = new Newtonsoft.Json.Serialization.DefaultContractResolver();
        });






Or

services.AddMvc()
   .AddJsonOptions(options =>
   {
        options.SerializerSettings.ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();
        options.SerializerSettings.Formatting = Formatting.Indented;
        options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
   });





And use JsonIgnoreAttribute to ignore serialization/deserialization for a property on an object.

 public class UserV1
 {
        public string Id { get; set; }
        public string Name { get; set; }
        [JsonIgnore]
        public string Password { get; set; }
        public string Email { get; set; }
        public DateTime? Birthday { get; set; }
        public int? Age { get; set; }
        [JsonIgnore]
        public Metadata Metadata { get; set; }
 }



 



Environment

.NET Core 2.2.203
Newtonsoft.Json 11.0.2



Implement


This article will show how to create an Advanced ContractRevolver and JsonIgnoreAttribute to IGNORE property’s serialization OR deserialization AT RUN TTIME.

Title
Class name
Description
JSON Ignore attribute
ApiIgnoreAttribute
Define the property should be or should not be serialized/deserialized on what Http Methods.
Custom contract resolver
ApiContractResolver
Inherit CamelCasePropertyNamesContractResolver and override the method for creating property by checking the ApiIgnoreAttribute of an object.



Advanced JSON Ignore attribute

Let’s create the class to replace JsonIgnoreAttribute for defining if a property should be/should not be serialized/deserialized at run time.
PS. Notice that the IgnoreDeserializeOn/IgnoreSerializeOn priority are higher than EnableDeserializeOn/EnableSerializeOn. (The logic will be implemented later on our custom ContractResolver)

ApiIgnoreAttribute.cs

public class ApiIgnoreAttribute : Attribute
    {
        /// Enable the property on what Http methods when doing deserialization
        public string EnableDeserializeOn { get; set; } = "*";
       
        /// Ignore the property on what Http methods when doing deserialization
        public string IgnoreDeserializeOn { get; set; }

        /// Enable the property on what Http methods when doing deserialization
        public string EnableSerializeOn { get; set; } = "*";

        /// Ignore the property on what Http methods when doing serialization
        public string IgnoreSerializeOn { get; set; }
    }

This attribute is used for defining a property can be serialized/deserialized on what http method(s).
For example, the following model indicates that:
·       Id should not be serialized (for Http response) when receiving a HttpGet request.
·       Name should not be deserialized (model binding) on all Http request. 

 public class TestModel
 {
        [ApiIgnore(IgnoreSerializeOn = "GET")]
        public string Id { getset; }

        [ApiIgnore(IgnoreDeserializeOn = "*")]
        public string Name { getset; }
 }


Custom contract resolver

Now we create a custom ContractResolver which inherits CamelCasePropertyNamesContractResolver (or DefaultContractResolver if you don’t want to convert the property as Camel case on seriliazation).


ApiContractResolver.cs (non-completed version!)

We have to override CreateProperty function to check both [JsonIgnore] and [ApiIgnore] attributes, and decide the property should be serialize/deserialize or not.

Since we have to know what Http method that current Http request is, the IServiceProvider object (Service container) must be injected into our custom ContractRevolver and then we use the injected IHttpContextAccessor object to get the Http request.

PS. The IHttpContextAccessor will be injected on Startup.cs later, you don’t have to worry about it now.

 public class ApiContractResolver : CamelCasePropertyNamesContractResolver
 {
        private readonly IServiceProvider serviceProvider = null;

        public ApiContractResolver(IServiceProvider serviceProvider)
            : base()
        {
            this.serviceProvider = serviceProvider;
        }

        protected override JsonProperty CreateProperty(MemberInfo memberMemberSerialization memberSerialization)
        {
            JsonProperty property = base.CreateProperty(membermemberSerialization);

            // Check [JsonIgnore]
            if (member.GetCustomAttributes(typeof(JsonIgnoreAttribute), true).Length > 0)
            {
                property.ShouldSerialize = instance => { return false; };
                property.ShouldDeserialize = instance => { return false; };
            }
            else
            {
                // Check [ApiJsonIgnore]
                var customAttr = member.GetCustomAttributes(typeof(ApiIgnoreAttribute), true).FirstOrDefault() as ApiIgnoreAttribute;
                if (customAttr != null)
                {
                    var contextAccessor = this.serviceProvider.GetRequiredService<IHttpContextAccessor>();
                    var httpMethod = contextAccessor?.HttpContext?.Request?.Method;

                    property.ShouldSerialize = instance =>
                    {
                        return string.IsNullOrEmpty(httpMethod) ? true : !this.CheckIfIgnore(customAttr.IgnoreSerializeOncustomAttr.EnableSerializeOnhttpMethod);
                    };

                    property.ShouldDeserialize = instance =>
                    {
                        return string.IsNullOrEmpty(httpMethod) ? true : !this.CheckIfIgnore(customAttr.IgnoreDeserializeOncustomAttr.EnableDeserializeOnhttpMethod);
                    };
                }
            }

            return property;
        }

        private virtual bool CheckIfIgnore(string ignoreOnstring enableOnstring method)
        {
            if (string.IsNullOrEmpty(ignoreOn) && string.IsNullOrEmpty(enableOn))
            {
                // If set nothing, ignore it
                return true;
            }
            else if (string.IsNullOrEmpty(ignoreOn) && enableOn.Equals("*"))
            {
                // Enable: * and Ignore nothing, so dont ignore it
                return false;
            }
            else if (!string.IsNullOrEmpty(ignoreOn) && (string.IsNullOrEmpty(enableOn) || enableOn.Equals("*")))
            {
                // IgnoreOn had been set, but not with EnableOn or EnableOn is *
                return ignoreOn.Equals("*") || ignoreOn.Contains(method);
            }
            else if (!string.IsNullOrEmpty(ignoreOn) && !string.IsNullOrEmpty(enableOn))
            {
                // Check IgnoreOn first
                if (ignoreOn.Equals("*") || ignoreOn.Contains(methodStringComparison.OrdinalIgnoreCase))
                {
                    return true;
                }
                else
                {
                    return !enableOn.Contains(methodStringComparison.OrdinalIgnoreCase);
                }
            }
            else
            {
                // Just check EnableOn
                return !enableOn.Contains(methodStringComparison.OrdinalIgnoreCase);
            }
        }
 }

In my logic, the ignore settings have higher priority than the enable settings. You can modify the logic to suit your usage cases 😊


Update contract resolver’s cache policy

Json.NET caches type serialization information inside its IContractResolver classes: DefaultContractResolver and CamelCasePropertyNamesContractResolver.
The cache policy will cache the contract when sending the FIRST request of certain API model, so we cannot check the Http method on every request excepts for the first one!

We can disable caching by overriding ResolveContract(Type type) in ApiContractResolver as following,

 public override JsonContract ResolveContract(Type type)
 {
     JsonContract contract = this.CreateContract(type);
     return contract;
 }



Thus the contract will be created and can checks the Http method on every request.
However, resolving contract is expensive for performance, so I still cache contracts by Type and HttpMethod.

ApiContractResolver.cs (updated)

 public class ApiContractResolver : CamelCasePropertyNamesContractResolver
 {
        private readonly Hashtable contractCache = null;
        private readonly IServiceProvider serviceProvider = null;

        public ApiContractResolver(IServiceProvider serviceProvider)
            : base()
        {
            this.contractCache = new Hashtable();
            this.serviceProvider = serviceProvider;
        }

        public override JsonContract ResolveContract(Type type)
        {
            JsonContract contract = null;
            var contextAccessor = this.serviceProvider.GetRequiredService<IHttpContextAccessor>();
            var httpMethod = contextAccessor?.HttpContext?.Request?.Method;

            string key = string.Format("{0}-{1}"type.ToString(), httpMethod);

            if (!this.contractCache.ContainsKey(key))
            {
                    contract = this.CreateContract(type);
                    this.contractCache.Add(keycontract);
            }
            else
            {
                    contract = this.contractCache[keyas JsonContract;                
            }

            return contract;
        }

        // Skip CreateProperty method …
 }


The final version of ApiContractResolver is here.



Configure JsonOptions

We need to configure the MvcJsonOptions with IServiceProvider injected.
The default way of configuration cannot inject IServiceProvider:

services.AddMvc()
        .AddJsonOptions(options =>
        {
            options.SerializerSettings.ContractResolver = new Newtonsoft.Json.Serialization.DefaultContractResolver();
        });


So remove the codes and create CustomJsonOptionWrapper which implement IConfigureOptions<MvcJsonOptions> and has IServiceProvider injected.

public class CustomJsonOptionWrapper : IConfigureOptions<MvcJsonOptions>
    {
        private readonly IServiceProvider serviceProvider = null;

        public CustomJsonOptionWrapper(IServiceProvider serviceProvider)
        {
            this.serviceProvider = serviceProvider;
        }

        public void Configure(MvcJsonOptions options)
        {
            options.SerializerSettings.ContractResolver = new ApiContractResolver(this.serviceProvider);
            options.SerializerSettings.Formatting = Formatting.None;
            options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
        }
    }


And then we can add it to IServiceCollection like following,
PS. We have to inject IHttpContextAccessor as well, since the ApiContractResolver use it from IServiceProvider.

Startup.cs

public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<IHttpContextAccessorHttpContextAccessor>();
            services.AddTransient<IConfigureOptions<MvcJsonOptions>, CustomJsonOptionWrapper>();

            services.AddMvc();

            // … skip
        }
    }


Thaz all of it! I have some test code on my Github for your reference.



Demo

Model

public class UserV1
    {
        [ApiIgnore(EnableDeserializeOn = "POST")]
        public string Id { getset; }

        [ApiIgnore(IgnoreDeserializeOn = "*")]
        public string Name { getset; }

        [ApiIgnore(EnableDeserializeOn = "POST"IgnoreSerializeOn = "*")]
        public string Password { getset; }

        [ApiIgnore(EnableDeserializeOn = "POST,PUT")]
        public string Email { getset; }

        [ApiIgnore(IgnoreSerializeOn = "GET")]
        public DateTimeBirthday { getset; }

        [ApiIgnore(IgnoreDeserializeOn = "*")]
        public intAge { getset; }

        [ApiIgnore(IgnoreDeserializeOn = "*")]
        public Metadata Metadata { getset; }
    }

public class Metadata
    {
        [JsonIgnore]
        public int Id { getset; }

        [ApiIgnore(IgnoreDeserializeOn = "*"EnableSerializeOn = "GET")]
        public DateTime CreateOn { getset; } = DateTime.Now;

        [ApiIgnore(IgnoreDeserializeOn = "*"EnableSerializeOn = "DELETE")]
        public string Description { getset; }
    }



(HttpGet)


(HttpPost)





Source code




Reference