ASP.NET Core API JSON serializersettings per request - c#

Based on some value in the request (header or in the url) I want to change the serialization of my DTO objects.
Why? Well I've applied the [JsonProperty("A")] to my DTO's but depending on the client (website or mobile app) it want to use that property or not.
I started with
services
.AddMvc()
.AddJsonOptions(opt =>
{
#if DEBUG
opt.SerializerSettings.ContractResolver = new NoJsonPropertyNameContractResolver();
#endif
}
So while debugging I get JSON with full propertynames. I use the JsonProperty attribute to shorten the response JSON, which works fine with the mobile app (Xamarin) which deserialize back to the same DTO's.
But now I have a website which uses the the same API to get data via jQuery, but in there I want to deal with the full property names of the DTO's, not the name given in the JsonProperty attribute.
Website and WebApi are on the same server so it's no problem if the response is a little bigger.
I started with a middleware class to react on a customer header value, which works, but now I don't know how to get to the JSON SerializerSettings. Searched the web but cannot find it.
While searching I've read about InputFormatters and OutputFormatters, and also content negotiation, but I don't know which direction I must go.
I don't want to deploy the same API twice with different settings.
I'am able to change things like the routesconfig if that would help.
Update
Not only the JSON response had to be serialized in 2 different ways, also the deserializing had to be done in 2 different ways.

Here are two options:
1. Manual formatting
Options you set by services.AddMvc().AddJsonOptions() are registered in DI and you can inject it into your controllers and services:
public HomeController(IOptions<MvcJsonOptions> optionsAccessor)
{
JsonSerializerSettings jsonSettings = optionsAccessor.Value.SerializerSettings;
}
To per-request override these serialization settings, you could use Json method or create JsonResult instance:
public IActionResult Get()
{
return Json(data, new JsonSerializerSettings());
return new JsonResult(data, new JsonSerializerSettings());
}
2. Result filter to replace JSON output
public class ModifyResultFilter : IAsyncResultFilter
{
public ModifyResultFilter(IOptions<MvcJsonOptions> optionsAccessor)
{
_globalSettings = optionsAccessor.Value.SerializerSettings;
}
public async Task OnResultExecutionAsync(
ResultExecutingContext context,
ResultExecutionDelegate next)
{
var originResult = context.Result as JsonResult;
context.Result = new JsonResult(originResult.Value, customSettings);
await next();
}
}
Use it on action/controller:
[ServiceFilter(typeof(ModifyResultFilter ))]
public IActionResult Index() {}
Or create a custom attribute as described in documentation:
[ModifyResultAttribute]
public IActionResult Index() {}
Don't forget to register the filter in DI.

Thanks for the comments and answers. I found a solution with Input and outputformatters. With thanks to http://rovani.net/Explicit-Model-Constructor/ to point me in the right direction.
I've created my own input and outputformatters, which inherit from JsonInputFormatter to keep as much functionality the same.
In the constructor I set the supported mediatype (used some that looks like the existing one for JSON).
Also must override CreateJsonSerializer to set the ContractResolver to the desired one (could implement singleton).
Must do it this way, because changing the serializerSettings in the constructor would change the serializersettings for all input/outputformatters, meaning the default JSON formatters will also use the new contract resolver.
Also doing it this way means you can setup some default JSON options via AddMvc().AddJsonOption()
Example inputformatter, outputformatter uses the same principle:
static MediaTypeHeaderValue protoMediaType = MediaTypeHeaderValue.Parse("application/jsonfull");
public JsonFullInputFormatter(ILogger logger, JsonSerializerSettings serializerSettings, ArrayPool<char> charPool, ObjectPoolProvider objectPoolProvider)
: base(logger, serializerSettings, charPool, objectPoolProvider)
{
this.SupportedMediaTypes.Clear();
this.SupportedMediaTypes.Add(protoMediaType);
}
protected override JsonSerializer CreateJsonSerializer()
{
var serializer = base.CreateJsonSerializer();
serializer.ContractResolver = new NoJsonPropertyNameContractResolver();
return serializer;
}
As per the mentioned URL above the setup class:
public class YourMvcOptionsSetup : IConfigureOptions<MvcOptions>
{
private readonly ILoggerFactory _loggerFactory;
private readonly JsonSerializerSettings _jsonSerializerSettings;
private readonly ArrayPool<char> _charPool;
private readonly ObjectPoolProvider _objectPoolProvider;
public YourMvcOptionsSetup(ILoggerFactory loggerFactory, IOptions<MvcJsonOptions> jsonOptions, ArrayPool<char> charPool, ObjectPoolProvider objectPoolProvider)
{
//Validate parameters and set fields
}
public void Configure(MvcOptions options)
{
var jsonFullInputFormatter = new JsonFullInputFormatter(
_loggerFactory.CreateLogger<JsonFullInputFormatter>(),
_jsonSerializerSettings,
_charPool,
_objectPoolProvider
);
options.InputFormatters.Add(jsonFullInputFormatter);
options.OutputFormatters.Add(new JsonFullOutputFormatter(
_jsonSerializerSettings,
_charPool
));
}
And then an extension method to register it:
public static class MvcBuilderExtensions
{
public static IMvcBuilder AddJsonFullFormatters(this IMvcBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
ServiceDescriptor descriptor = ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, YourMvcOptionsSetup>();
builder.Services.TryAddEnumerable(descriptor);
return builder;
}
}
Call it in ConfigureServices:
services.AddMvc(config =>
{
config.RespectBrowserAcceptHeader = true; // To use the JsonFullFormatters if clients asks about it via Accept Header
})
.AddJsonFullFormatters() //Add our own JSON Formatters
.AddJsonOptions(opt =>
{
//Set up some default options all JSON formatters must use (if any)
});
Now our Xamarin App can access the webapi and receive JSON with (short) property names set via JsonProperty attribute.
And in the website we can get the full JSON property names by adding an Accept (get calls) and ContentType (post/put calls) header. Which we do once via jQuery's $.ajaxSetup(.
$.ajaxSetup({
contentType: "application/jsonfull; charset=utf-8",
headers: { 'Accept': 'application/jsonfull' }
});

Related

How to inject JSONConvert into .NET Core 3.1 service class?

I need to serialize some classes in .NET Core 3.1 service class (not controller).
I can simply use JsonConvert.SerializeObject which could do the job for me:
private void LogEvent(object properties, LogLevel logLevel)
{
var message = JsonConvert.SerializeObject(properties);
_logger.Log(logLevel, message);
}
But I have to use Newtonsoft.Json.Converters.StringEnumConverter as we need the enums as string.
I can use below code to achieve this:
var message = JsonConvert.SerializeObject(properties, Formatting.None, new JsonSerializerSettings
{
Converters = { new Newtonsoft.Json.Converters.StringEnumConverter() },
NullValueHandling = NullValueHandling.Ignore
});
As you can see this will create a new object every time this method gets hit. Not sure if this is the right approach.
Can we define JSONConvert in startup class and inject it on service class where I can use Serialize object?
Something like:
.AddJsonOptions(options =>
{
options.SerializerSettings.Converters.Add(new StringEnumConverter());
});
You can not inject JSONConvert because it is static class.
But you can do something like this
public interface IProxyJSONConvert {
public string SerializeObject(object obj);
}
public class ProxyJSONConvert : IProxyJSONConvert {
public string SerializeObject(object obj)
{
return JsonConvert.SerializeObject(properties);
}
}
And In Startup
services.AddScoped<IProxyJSONConvert, ProxyJSONConvert>();
Now in controller you can pass this in controller and then service
You can modify according to your need

Use serializer within ASP.NET Core MVC controller?

ASP.NET Core MVC serializes responses returned from my Microsoft.AspNetCore.Mvc.ControllerBase controller methods.
So the following will serialize a MyDTO object to JSON:
[Produces("application/json")]
[HttpGet]
public MyDTO test()
{
return new MyDTO();
}
However, I want to use the exact same serializer within the method itself. Is that possible? Something like
[Produces("application/json")]
[HttpGet]
public MyDTO test()
{
var result = new MyDTO();
string serialized = this.<SOMETHING GOES HERE>(result);
Console.WriteLine($"I'm about to return {serialized}");
return result;
}
It's important that it's the same serializer, including any options set in the stack.
Don't think that's possible because it's hidden away within IActionResultExecutor<JsonResult>. Default will be the SystemTextJsonResultExecutor but could be overwritten on startup when using e.g. .AddNewtonsoftJson(...)...
You could set the Json Format Options explicitly in startup.cs and then just use the same options wherever else you need to serialize. That way all serializations will be done exactly the same way.
e.g. If you are using JsonSerializer:
public static class JsonSerializerExtensions
{
public static JsonOptions ConfigureJsonOptions(this JsonOptions options)
{
// your configuration
options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.WriteAsString;
return options;
}
}
startup.cs
services
.AddControllers()
.AddJsonOptions(options => options.ConfigureJsonOptions());
Conroller.cs
string serialized =JsonSerializer.Serialize(new MyDTO(), options: new JsonOptions().ConfigureJsonOptions().JsonSerializerOptions);

Change System.Text.Json Serialization Options of a single ASP.NET Core API controller or single Action in ASP.NET Core 3

I'm having two controller controllers: ControllerA and ControllerB. The base class of each controller is ControllerBase.
The ControllerA needs to deserialize JSON in the default option
JsonSerializerOptions.IgnoreNullValues = false;
The ControllerB needs to deserialize JSON with option
JsonSerializerOptions.IgnoreNullValues = true;
I know how to set this option global in Startup.cs
services.AddControllers().AddJsonOptions( options => options.JsonSerializerOptions.IgnoreNullValues = true);
But how to set specific deserialize options to Controller or Action? (ASP.NET Core 3 API)
As suggested by Fei Han, the straight-forward answer is to use on ControllerB the attribute NullValuesJsonOutput :
public class NullValuesJsonOutputAttribute : ActionFilterAttribute
{
private static readonly SystemTextJsonOutputFormatter Formatter = new SystemTextJsonOutputFormatter(new JsonSerializerOptions
{
IgnoreNullValues = true
});
public override void OnActionExecuted(ActionExecutedContext context)
{
if (context.Result is ObjectResult objectResult)
objectResult.Formatters.Add(Formatter);
}
}

How to configure two JSON serializers and select the correct one based on the route

I have an ASP.NET Core Web API project using .Net Framework 4.7 and I'm upgrading to .Net Core 3.1. One of the reasons that I'm upgrading is to use the new System.Text.Json serializer.
Currently, I have some versions of the API based on the route, like:
/api/v1/controller
/api/v2/controller
And I will create a new one (v3) to use the new serializer. But here is the problem: I want to keep using JSON.Net on the older routes, to avoid any possible formating problem with the integrated clients.
Is there an easy way to configure Asp.Net Core to automatically select the correct JSON serializer based on the route?
You could create your own super InputFormatter/OutputFormatter so that it checks the condition at runtime and then make a decision to use System.Text.Json or use Newtonsoft.Json dynamically.
For example, we can check the current action method ( or controller class):
if it has a custom attribute of [UseSystemTextJsonAttribute], then use System.Text.Json
if it has a custom attribute of [UseNewtonsoftJsonAttribute], then use Newtonsoft.Json.
I create a custom InputFormatter for your reference:
// the custom attribute
internal abstract class UseJsonAttribute : Attribute, IAsyncActionFilter
{
public Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) => next();
}
internal class UseSystemTextJsonAttribute : UseJsonAttribute { }
internal class UseNewtonsoftJsonAttribute : UseJsonAttribute { }
// Our Super Input Formatter
internal class MySuperJsonInputFormatter : TextInputFormatter
{
public MySuperJsonInputFormatter()
{
SupportedEncodings.Add(UTF8EncodingWithoutBOM);
SupportedEncodings.Add(UTF16EncodingLittleEndian);
SupportedMediaTypes.Add("application/json");
}
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
{
var mvcOpt= context.HttpContext.RequestServices.GetRequiredService<IOptions<MvcOptions>>().Value;
var formatters = mvcOpt.InputFormatters;
TextInputFormatter formatter =null; // the real formatter : SystemTextJsonInput or Newtonsoft
Endpoint endpoint = context.HttpContext.GetEndpoint();
if(endpoint.Metadata.GetMetadata<UseSystemTextJsonAttribute>()!= null)
{
formatter= formatters.OfType<SystemTextJsonInputFormatter>().FirstOrDefault();
//formatter = formatter ?? SystemTextJsonInputFormatter
}
else if( endpoint.Metadata.GetMetadata<UseNewtonsoftJsonAttribute>() != null){
// don't use `Of<NewtonsoftJsonInputFormatter>` here because there's a NewtonsoftJsonPatchInputFormatter
formatter= (NewtonsoftJsonInputFormatter)(formatters
.Where(f =>typeof(NewtonsoftJsonInputFormatter) == f.GetType())
.FirstOrDefault());
}
else{
throw new Exception("This formatter is only used for System.Text.Json InputFormatter or NewtonsoftJson InputFormatter");
}
var result = await formatter.ReadRequestBodyAsync(context,encoding);
return result;
}
}
internal class MySuperJsonOutputFormatter : TextOutputFormatter
{
... // similar to MySuperJsonInputFormatter, omitted for brevity
}
And then configure the Json settings/options in the startup:
services.AddControllers(opts =>{ })
.AddNewtonsoftJson(opts =>{ /**/ })
.AddJsonOptions(opts =>{ /**/ });
Note AddNewtonsoftJson() will remove the builtin SystemTextJsonInputFormatters. So we need configure the MvcOptions manually :
services.AddOptions<MvcOptions>()
.PostConfigure<IOptions<JsonOptions>, IOptions<MvcNewtonsoftJsonOptions>,ArrayPool<char>, ObjectPoolProvider,ILoggerFactory>((opts, jsonOpts, newtonJsonOpts, charPool, objectPoolProvider, loggerFactory )=>{
// configure System.Text.Json formatters
if(opts.InputFormatters.OfType<SystemTextJsonInputFormatter>().Count() ==0){
var systemInputlogger = loggerFactory.CreateLogger<SystemTextJsonInputFormatter>();
opts.InputFormatters.Add(new SystemTextJsonInputFormatter(jsonOpts.Value, systemInputlogger));
}
if(opts.OutputFormatters.OfType<SystemTextJsonOutputFormatter>().Count() ==0){
opts.OutputFormatters.Add(new SystemTextJsonOutputFormatter(jsonOpts.Value.JsonSerializerOptions));
}
// configure Newtonjson formatters
if(opts.InputFormatters.OfType<NewtonsoftJsonInputFormatter>().Count() ==0){
var inputLogger= loggerFactory.CreateLogger<NewtonsoftJsonInputFormatter>();
opts.InputFormatters.Add(new NewtonsoftJsonInputFormatter(
inputLogger, newtonJsonOpts.Value.SerializerSettings, charPool, objectPoolProvider, opts, newtonJsonOpts.Value
));
}
if(opts.OutputFormatters.OfType<NewtonsoftJsonOutputFormatter>().Count()==0){
opts.OutputFormatters.Add(new NewtonsoftJsonOutputFormatter(newtonJsonOpts.Value.SerializerSettings, charPool, opts));
}
opts.InputFormatters.Insert(0, new MySuperJsonInputFormatter());
opts.OutputFormatters.Insert(0, new MySuperJsonOutputFormatter());
});
Now it should work fine.
I've used this approach (create a "super" formatter which then decides which real formatter to use) to allow different routes to have different JSON formatting (camelCase or not in our scenario). This works well and allows us to use a third-party add-in which requires a specific set of formatting rules which differ from our standard...
As the third-party add-in is fully compiled we couldn't use a custom attribute on their controllers; and didn't one to add one to each of ours (!); so instead we examine the route and selected the formatter based on that.
Real life saver in this case :)

Customizing ASP Web API Json serialization by which Action is invoked

I am looking at converting an existing JSON api from a hacky MVC3 implementation to the latest MVC4 Web Api. The MVC3 implementation uses JSON.NET to do all the serialization which will make the upgrade nice and smooth.
I am stuck on customizing how the results of some action get serialized. For instance I want some actions to return only a few properties of the outputted objects, whilst others may do rather deep serialization. In my current implementation, an action can add a bunch of serialization overrides by setting appropriate settings in the HttpContext. These are later picked up for custom serialization through a class derived from JsonResult. The main use of adding custom JsonConverters is to control and reduce the number of key/values getting serialized, and vary the parameters to serialize depending on the action (certain actions should return more object parameters than others).
Condensed example of a controller and the class controlling json serialization in my current MVC3 implementation:
public class TestController : JsonController {
public JsonResult Persons() {
ControllerContext.HttpContext.Items[typeof(IEnumerable<JsonConverter>)] = new JsonConverter[] {
new InterfaceExtractorJsonConverter<IPersonForList>(),
new StringEnumConverter()
};
ControllerContext.HttpContext.Items[typeof(IContractResolver)] = new SpecialCamelCasePropertyNamesContractResolver();
}
}
public class JsonNetResult : JsonResult {
public override void ExecuteResult(ControllerContext context) {
var response = context.HttpContext.Response;
var additionalConverters = context.HttpContext.Items[typeof(IEnumerable<JsonConverter>)] as IEnumerable<JsonConverter> ?? Enumerable.Empty<JsonConverter>();
var contractResolver = context.HttpContext.Items[typeof(IContractResolver)] as IContractResolver ?? new JsonContractResolver();
var typeNameHandling = TypeNameHandling.None;
if (context.HttpContext.Items.Contains(typeof(TypeNameHandling)))
typeNameHandling = (TypeNameHandling)context.HttpContext.Items[typeof(TypeNameHandling)];
response.Write(JsonConvert.SerializeObject(Data, Formatting.Indented, new JsonSerializerSettings {
ContractResolver = contractResolver,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
Converters = additionalConverters,
TypeNameHandling = typeNameHandling
}));
}
}
In Web Api I see that I can get the Json formatter from the configuration and alter the serializations globally.
var config = new HttpSelfHostConfiguration("http://localhost:8080");
var jsonFormatter = config.Formatters.OfType<JsonMediaTypeFormatter>().Single();
jsonFormatter.SerializerSettings.ContractResolver = new SpecialCamelCasePropertyNamesContractResolver();
jsonFormatter.SerializerSettings.Converters = new[] { new InterfaceExtractorJsonConverter<ITesting>() };
However, I was planning on controlling the serialization of actions on an individual basis (or per controller) by adding some attributes to specify which JsonConverter's to use. Thus I would like the Json serializer to find the relevant attributes given to the invoked action/controller and alter serialization accordingly. I am not sure where and how to do this. Should I inherit from JsonMediaTypeFormatter and do the work there somehow? What other options do I have?
I've never seen anyone want to control the serialization in that way. But to achieve your goal with the minimal amount of rework, I would return all of that information from your method directly:
class JsonNetResponse {
public IContractResolver ContractResolver { get;set; }
// Other Json.Net bits
public object Value { get; set; }
}
I would then create a custom Formatter than can handle those objects:
class JsonNetFormatter : MediaTypeFormatter {
public override bool CanWriteType(Type t) {
return typeof(JsonNetResponse).IsAssignableFrom(t);
}
// TODO WriteToStreamAsync which is basically a copy of your original JsonNetResult
}

Categories