Use serializer within ASP.NET Core MVC controller? - c#

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

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

Swagger - how attach an example to particular end-point

I'm working on API where my controllers just returns JsonDocument's instead of DTO's.
I need to provide some response examples for API documentation. But examples should be dedicated for particular end-points.
I've created example provider IExampleProvider<JsonDocument> - and unfortunately the example is populated for all JsonDocument's, instead of the only one decorated by the attribute:
[SwaggerResponseExample(200, typeof(MySpecificResponseExample))]
Even if attribute mentioned above is not specified, the example documentation is included.
Startup.cs:
c.ExampleFilters();
c.OperationFilter<AddResponseHeadersFilter>();
services.AddSwaggerExamplesFromAssemblyOf<Startup>();
Example provider:
public class (MySpecificResponseExample))] : IExamplesProvider<JsonDocument>
{
public JsonDocument GetExamples()
{
return JsonDocument.Parse(#"Example JSON is here.");
}
}
Controller:
[HttpGet]
[ProducesResponseType(typeof(JsonDocument), 200)]
[SwaggerResponseExample(200, typeof(MySpecificResponseExample))]
public async Task<IActionResult> GetSomething() {}
How to include examples to end-points with specific attribute only? I have another end-point where [ProducesResponseType(typeof(JsonDocument), 200)] is specified, but I want to ommit MySpecificResponseExample.
You can not return JsonDocument over the http since it needs to be converted to json , so I don't see any sense in double parcing and unparcing you better try this and parse to JsonDocument when you get it. But I highly recommend to use JObject Or JArray instead
public JsonResult GetExamples()
{
return new JsonResult( #"Example JSON is here.");
}

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

ASP.NET Core API JSON serializersettings per request

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

Categories