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.
|
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;
});
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 { get; set; }
[ApiIgnore(IgnoreDeserializeOn = "*")]
public string Name { get; set; }
}
▋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 member, MemberSerialization memberSerialization)
{
JsonProperty property = base.CreateProperty(member, memberSerialization);
// 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.IgnoreSerializeOn, customAttr.EnableSerializeOn, httpMethod);
};
property.ShouldDeserialize = instance =>
{
return string.IsNullOrEmpty(httpMethod) ? true : !this.CheckIfIgnore(customAttr.IgnoreDeserializeOn, customAttr.EnableDeserializeOn, httpMethod);
};
}
}
return property;
}
private virtual bool CheckIfIgnore(string ignoreOn, string enableOn, string 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(method, StringComparison.OrdinalIgnoreCase))
{
return true;
}
else
{
return !enableOn.Contains(method, StringComparison.OrdinalIgnoreCase);
}
}
else
{
// Just check EnableOn
return !enableOn.Contains(method, StringComparison.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(key, contract);
}
else
{
contract = this.contractCache[key] as JsonContract;
}
return contract;
}
// Skip CreateProperty method …
}
▋Configure JsonOptions
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;
}
}
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<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<IConfigureOptions<MvcJsonOptions>, CustomJsonOptionWrapper>();
services.AddMvc();
// … skip
}
}
▋Demo
▋Model
public class UserV1
{
[ApiIgnore(EnableDeserializeOn = "POST")]
public string Id { get; set; }
[ApiIgnore(IgnoreDeserializeOn = "*")]
public string Name { get; set; }
[ApiIgnore(EnableDeserializeOn = "POST", IgnoreSerializeOn = "*")]
public string Password { get; set; }
[ApiIgnore(EnableDeserializeOn = "POST,PUT")]
public string Email { get; set; }
[ApiIgnore(IgnoreSerializeOn = "GET")]
public DateTime? Birthday { get; set; }
[ApiIgnore(IgnoreDeserializeOn = "*")]
public int? Age { get; set; }
[ApiIgnore(IgnoreDeserializeOn = "*")]
public Metadata Metadata { get; set; }
}
public class Metadata
{
[JsonIgnore]
public int Id { get; set; }
[ApiIgnore(IgnoreDeserializeOn = "*", EnableSerializeOn = "GET")]
public DateTime CreateOn { get; set; } = DateTime.Now;
[ApiIgnore(IgnoreDeserializeOn = "*", EnableSerializeOn = "DELETE")]
public string Description { get; set; }
}
(HttpGet)
(HttpPost)
▋Source code
▌Reference