Change JSON response to Pascal case using web API custom attribute - c#

My Custom Action Filter Attribute to convert a JSON response of MVC core webApi from "camelCase" to "pascalCase" is not working.
Tried using:
services.AddMvc()
.AddJsonOptions(options =>
options.SerializerSettings.ContractResolver = new DefaultContractResolver());
However, this kind of global setting changes all response to Pascal case. I want to change only a limited API response to Pascal case.
Custom ActionFilterAttribute:
public class CancelCamelCaseResolverConfigurationAttribute : ActionFilterAttribute
{
public override void OnResultExecuted(ResultExecutedContext context)
{
base.OnResultExecuted(context);
var objectResult = context.Result as ObjectResult;
if (objectResult != null)
{
objectResult.Formatters.Clear();
objectResult.Formatters.Add(new JsonOutputFormatter(
new JsonSerializerSettings()
{
Formatting = Formatting.None,
ContractResolver = new DefaultContractResolver()
}, ArrayPool<char>.Shared));
}
}
}
And use in the webApi controller:
[CancelCamelCaseResolverConfiguration]
public class FrmMainSearchController : AtlasApiController<FrmMainSearchController>
{
/*Api*/
}
Expected result:
searchCriteria = [{Key: "xx", Value: "yy"}]
Actual result:
searchCriteria = [{key: "xx", value: "yy"}]

You're almost there: You need override the OnActionExecuted() method instead of the OnResultExecuted().
It's too late to change the formatters when the OnResultExecuted() filter method is invoked.
How to fix:
Override the OnResultExecuted method so that the formatter is changed before the result execution:
public override void OnResultExecuted(ResultExecutedContext context)
public override void OnActionExecuted(ActionExecutedContext context)
{
...
}
As a side note, you didn't check for type JsonResult. To make it work with Json() or JsonResult(), you need check the result type dynamically:
public class CancelCamelCaseResolverConfigurationAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext context)
{
base.OnActionExecuted(context);
switch(context.Result){
case JsonResult j:
var result = new ObjectResult(j.Value);
context.Result = result;
ChangeFormatting(result);
break;
case ObjectResult o:
ChangeFormatting(o);
break;
default:
return;
}
}
private void ChangeFormatting(ObjectResult result){
if (result == null){ return; }
result.Formatters.Clear();
result.Formatters.Add(new JsonOutputFormatter(
new JsonSerializerSettings()
{
Formatting = Formatting.None,
ContractResolver = new DefaultContractResolver()
}, ArrayPool<char>.Shared)
);
}
}

Related

How to catch a result from IActionFilter

Consider having filter with type of IActionFilter
// example from https://learn.microsoft.com/en-us/aspnet/core/web-api/handle-errors?view=aspnetcore-7.0#use-exceptions-to-modify-the-response
public class HttpResponseExceptionFilter : IActionFilter, IOrderedFilter
{
public int Order => int.MaxValue - 10;
public void OnActionExecuting(ActionExecutingContext context) { }
public void OnActionExecuted(ActionExecutedContext context)
{
if (context.Exception is HttpResponseException httpResponseException)
{
context.Result = new ObjectResult(httpResponseException.Value)
{
StatusCode = httpResponseException.StatusCode
};
context.ExceptionHandled = true;
}
}
}
My question is how could i catch this context.Result property to use it like a return type?
For example, i have a controller method that should normally return (for example) response 200, but under some exceptions that can be handled inside my filter, the filter itself could return 404. So back to the question, is it possible to catch this context.Result? And if so, what is the best practices of doing it?
I think that you can try using ExceptionFilterAttribute.
For example:
public class UnhandledExceptionFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(ExceptionContext context)
{
// Customize this object to fit your needs
var result = new ObjectResult(new
{
context.Exception.Message, // Or a different generic message
context.Exception.Source,
ExceptionType = context.Exception.GetType().FullName,
})
{
StatusCode = (int)HttpStatusCode.InternalServerError
};
// Set the result
context.Result = result;
}
}
And in Program.cs(global):
builder.Services.AddControllers(options => options.Filters.Add(new UnhandledExceptionFilterAttribute()));
Or apply it to a controller or action:
[ServiceFilter(typeof(UnhandledExceptionFilterAttribute))]
public class SomeController : ControllerBase
{
}
At this point in Program.cs:
builder.Services.AddScoped<UnhandledExceptionFilterAttribute>();
Hope this can help you.

Change the JSON deserialization/serialization policy for single ASP.NET Core controller

I have a controller that I use for a third party API, which uses a snake case naming convention. The rest of my controllers are used for my own app, which uses a camelcase JSON convention. I'd like to automatically deserialize and serialize my models from/to snake case for the API in that one controller. This question explains how to use a snake case naming policy for JSON in the entire app, but is there a way that I can specify to use the naming policy only for that single controller?
I've seen Change the JSON serialization settings of a single ASP.NET Core controller which suggests using an ActionFilter, but that only helps for ensuring that outgoing JSON is serialized properly. How can I get the incoming JSON deserialized to my models properly as well? I know that I can use [JsonPropertyName] on the model property names but I'd prefer to be able to set something at the controller level, not at the model level.
The solution on the shared link in your question is OK for serialization (implemented by IOutputFormatter) although we may have another approach by extending it via other extensibility points.
Here I would like to focus on the missing direction (the deserializing direction which is implemented by IInputFormatter). You can implement a custom IModelBinder but it requires you to reimplement the BodyModelBinder and BodyModelBinderProvider which is not easy. Unless you accept to clone all the source code of them and modify the way you want. That's not very friendly to maintainability and getting up-to-date to what changed by the framework.
After researching through the source code, I've found that it's not easy to find a point where you can customize the deserializing behavior based on different controllers (or actions). Basically the default implementation uses a one-time init IInputFormatter for json (default by JsonInputFormatter for asp.net core < 3.0). That in chain will share one instance of JsonSerializerSettings. In your scenario, actually you need multiple instances of that settings (for each controller or action). The easiest point I think is to customize an IInputFormatter (extending the default JsonInputFormatter). It becomes more complicated when the default implementation uses ObjectPool for the instance of JsonSerializer (which is associated with a JsonSerializerSettings). To follow that style of pooling the objects (for better performance), you need a list of object pools (we will use a dictionary here) instead of just one object pool for the shared JsonSerializer as well as the associated JsonSerializerSettings (as implemented by the default JsonInputFormatter).
The point here is to based on the current InputFormatterContext, you need to build the corresponding JsonSerializerSettings as well as the JsonSerializer to be used. That sounds simple but once it comes to a full implementation (with fairly complete design), the code is not short at all. I've designed it into multiple classes. If you really want to see it working, just be patient to copy the code carefully (of course reading it through to understand is recommended). Here's all the code:
public abstract class ContextAwareSerializerJsonInputFormatter : JsonInputFormatter
{
public ContextAwareSerializerJsonInputFormatter(ILogger logger,
JsonSerializerSettings serializerSettings,
ArrayPool<char> charPool, ObjectPoolProvider objectPoolProvider, MvcOptions options, MvcJsonOptions jsonOptions) : base(logger, serializerSettings, charPool, objectPoolProvider, options, jsonOptions)
{
PoolProvider = objectPoolProvider;
}
readonly AsyncLocal<InputFormatterContext> _currentContextAsyncLocal = new AsyncLocal<InputFormatterContext>();
readonly AsyncLocal<ActionContext> _currentActionAsyncLocal = new AsyncLocal<ActionContext>();
protected InputFormatterContext CurrentContext => _currentContextAsyncLocal.Value;
protected ActionContext CurrentAction => _currentActionAsyncLocal.Value;
protected ObjectPoolProvider PoolProvider { get; }
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
{
_currentContextAsyncLocal.Value = context;
_currentActionAsyncLocal.Value = context.HttpContext.RequestServices.GetRequiredService<IActionContextAccessor>().ActionContext;
return base.ReadRequestBodyAsync(context, encoding);
}
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
_currentContextAsyncLocal.Value = context;
_currentActionAsyncLocal.Value = context.HttpContext.RequestServices.GetRequiredService<IActionContextAccessor>().ActionContext;
return base.ReadRequestBodyAsync(context);
}
protected virtual JsonSerializer CreateJsonSerializer(InputFormatterContext context) => null;
protected override JsonSerializer CreateJsonSerializer()
{
var context = CurrentContext;
return (context == null ? null : CreateJsonSerializer(context)) ?? base.CreateJsonSerializer();
}
}
public abstract class ContextAwareMultiPooledSerializerJsonInputFormatter : ContextAwareSerializerJsonInputFormatter
{
public ContextAwareMultiPooledSerializerJsonInputFormatter(ILogger logger, JsonSerializerSettings serializerSettings, ArrayPool<char> charPool, ObjectPoolProvider objectPoolProvider, MvcOptions options, MvcJsonOptions jsonOptions)
: base(logger, serializerSettings, charPool, objectPoolProvider, options, jsonOptions)
{
}
readonly IDictionary<object, ObjectPool<JsonSerializer>> _serializerPools = new ConcurrentDictionary<object, ObjectPool<JsonSerializer>>();
readonly AsyncLocal<object> _currentPoolKeyAsyncLocal = new AsyncLocal<object>();
protected object CurrentPoolKey => _currentPoolKeyAsyncLocal.Value;
protected abstract object GetSerializerPoolKey(InputFormatterContext context);
protected override JsonSerializer CreateJsonSerializer(InputFormatterContext context)
{
object poolKey = GetSerializerPoolKey(context) ?? "";
if(!_serializerPools.TryGetValue(poolKey, out var pool))
{
//clone the settings
var serializerSettings = new JsonSerializerSettings();
foreach(var prop in typeof(JsonSerializerSettings).GetProperties().Where(e => e.CanWrite))
{
prop.SetValue(serializerSettings, prop.GetValue(SerializerSettings));
}
ConfigureSerializerSettings(serializerSettings, poolKey, context);
pool = PoolProvider.Create(new JsonSerializerPooledPolicy(serializerSettings));
_serializerPools[poolKey] = pool;
}
_currentPoolKeyAsyncLocal.Value = poolKey;
return pool.Get();
}
protected override void ReleaseJsonSerializer(JsonSerializer serializer)
{
if(_serializerPools.TryGetValue(CurrentPoolKey ?? "", out var pool))
{
pool.Return(serializer);
}
}
protected virtual void ConfigureSerializerSettings(JsonSerializerSettings serializerSettings, object poolKey, InputFormatterContext context) { }
}
//there is a similar class like this implemented by the framework
//but it's a pity that it's internal
//So we define our own class here (which is exactly the same from the source code)
//It's quite simple like this
public class JsonSerializerPooledPolicy : IPooledObjectPolicy<JsonSerializer>
{
private readonly JsonSerializerSettings _serializerSettings;
public JsonSerializerPooledPolicy(JsonSerializerSettings serializerSettings)
{
_serializerSettings = serializerSettings;
}
public JsonSerializer Create() => JsonSerializer.Create(_serializerSettings);
public bool Return(JsonSerializer serializer) => true;
}
public class ControllerBasedJsonInputFormatter : ContextAwareMultiPooledSerializerJsonInputFormatter,
IControllerBasedJsonSerializerSettingsBuilder
{
public ControllerBasedJsonInputFormatter(ILogger logger, JsonSerializerSettings serializerSettings, ArrayPool<char> charPool, ObjectPoolProvider objectPoolProvider, MvcOptions options, MvcJsonOptions jsonOptions) : base(logger, serializerSettings, charPool, objectPoolProvider, options, jsonOptions)
{
}
readonly IDictionary<object, Action<JsonSerializerSettings>> _configureSerializerSettings
= new Dictionary<object, Action<JsonSerializerSettings>>();
readonly HashSet<object> _beingAppliedConfigurationKeys = new HashSet<object>();
protected override object GetSerializerPoolKey(InputFormatterContext context)
{
var routeValues = context.HttpContext.GetRouteData()?.Values;
var controllerName = routeValues == null ? null : routeValues["controller"]?.ToString();
if(controllerName != null && _configureSerializerSettings.ContainsKey(controllerName))
{
return controllerName;
}
var actionContext = CurrentAction;
if (actionContext != null && actionContext.ActionDescriptor is ControllerActionDescriptor actionDesc)
{
foreach (var attr in actionDesc.MethodInfo.GetCustomAttributes(true)
.Concat(actionDesc.ControllerTypeInfo.GetCustomAttributes(true)))
{
var key = attr.GetType();
if (_configureSerializerSettings.ContainsKey(key))
{
return key;
}
}
}
return null;
}
public IControllerBasedJsonSerializerSettingsBuilder ForControllers(params string[] controllerNames)
{
foreach(var controllerName in controllerNames ?? Enumerable.Empty<string>())
{
_beingAppliedConfigurationKeys.Add((controllerName ?? "").ToLowerInvariant());
}
return this;
}
public IControllerBasedJsonSerializerSettingsBuilder ForControllersWithAttribute<T>()
{
_beingAppliedConfigurationKeys.Add(typeof(T));
return this;
}
public IControllerBasedJsonSerializerSettingsBuilder ForActionsWithAttribute<T>()
{
_beingAppliedConfigurationKeys.Add(typeof(T));
return this;
}
ControllerBasedJsonInputFormatter IControllerBasedJsonSerializerSettingsBuilder.WithSerializerSettingsConfigurer(Action<JsonSerializerSettings> configurer)
{
if (configurer == null) throw new ArgumentNullException(nameof(configurer));
foreach(var key in _beingAppliedConfigurationKeys)
{
_configureSerializerSettings[key] = configurer;
}
_beingAppliedConfigurationKeys.Clear();
return this;
}
protected override void ConfigureSerializerSettings(JsonSerializerSettings serializerSettings, object poolKey, InputFormatterContext context)
{
if (_configureSerializerSettings.TryGetValue(poolKey, out var configurer))
{
configurer.Invoke(serializerSettings);
}
}
}
public interface IControllerBasedJsonSerializerSettingsBuilder
{
ControllerBasedJsonInputFormatter WithSerializerSettingsConfigurer(Action<JsonSerializerSettings> configurer);
IControllerBasedJsonSerializerSettingsBuilder ForControllers(params string[] controllerNames);
IControllerBasedJsonSerializerSettingsBuilder ForControllersWithAttribute<T>();
IControllerBasedJsonSerializerSettingsBuilder ForActionsWithAttribute<T>();
}
To help conveniently configure the services to replace the default JsonInputFormatter, we have the following code:
public class ControllerBasedJsonInputFormatterMvcOptionsSetup : IConfigureOptions<MvcOptions>
{
private readonly ILoggerFactory _loggerFactory;
private readonly MvcJsonOptions _jsonOptions;
private readonly ArrayPool<char> _charPool;
private readonly ObjectPoolProvider _objectPoolProvider;
public ControllerBasedJsonInputFormatterMvcOptionsSetup(
ILoggerFactory loggerFactory,
IOptions<MvcJsonOptions> jsonOptions,
ArrayPool<char> charPool,
ObjectPoolProvider objectPoolProvider)
{
if (loggerFactory == null)
{
throw new ArgumentNullException(nameof(loggerFactory));
}
if (jsonOptions == null)
{
throw new ArgumentNullException(nameof(jsonOptions));
}
if (charPool == null)
{
throw new ArgumentNullException(nameof(charPool));
}
if (objectPoolProvider == null)
{
throw new ArgumentNullException(nameof(objectPoolProvider));
}
_loggerFactory = loggerFactory;
_jsonOptions = jsonOptions.Value;
_charPool = charPool;
_objectPoolProvider = objectPoolProvider;
}
public void Configure(MvcOptions options)
{
//remove the default
options.InputFormatters.RemoveType<JsonInputFormatter>();
//add our own
var jsonInputLogger = _loggerFactory.CreateLogger<ControllerBasedJsonInputFormatter>();
options.InputFormatters.Add(new ControllerBasedJsonInputFormatter(
jsonInputLogger,
_jsonOptions.SerializerSettings,
_charPool,
_objectPoolProvider,
options,
_jsonOptions));
}
}
public static class ControllerBasedJsonInputFormatterServiceCollectionExtensions
{
public static IServiceCollection AddControllerBasedJsonInputFormatter(this IServiceCollection services,
Action<ControllerBasedJsonInputFormatter> configureFormatter)
{
if(configureFormatter == null)
{
throw new ArgumentNullException(nameof(configureFormatter));
}
services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();
return services.ConfigureOptions<ControllerBasedJsonInputFormatterMvcOptionsSetup>()
.PostConfigure<MvcOptions>(o => {
var jsonInputFormatter = o.InputFormatters.OfType<ControllerBasedJsonInputFormatter>().FirstOrDefault();
if(jsonInputFormatter != null)
{
configureFormatter(jsonInputFormatter);
}
});
}
}
//This attribute is used as a marker to decorate any controllers
//or actions that you want to apply your custom input formatter
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class UseSnakeCaseJsonInputFormatterAttribute : Attribute
{
}
Finally here's a sample configuration code:
//inside Startup.ConfigureServices
services.AddControllerBasedJsonInputFormatter(formatter => {
formatter.ForControllersWithAttribute<UseSnakeCaseJsonInputFormatterAttribute>()
.ForActionsWithAttribute<UseSnakeCaseJsonInputFormatterAttribute>()
.WithSerializerSettingsConfigurer(settings => {
var contractResolver = settings.ContractResolver as DefaultContractResolver ?? new DefaultContractResolver();
contractResolver.NamingStrategy = new SnakeCaseNamingStrategy();
settings.ContractResolver = contractResolver;
});
});
Now you can use the marker attribute UseSnakeCaseJsonInputFormatterAttribute on any controllers (or action methods) that you want to apply the snake-case json input formatter, like this:
[UseSnakeCaseJsonInputFormatter]
public class YourController : Controller {
//...
}
Note that the code above uses asp.net core 2.2, for asp.net core 3.0+, you can replace the JsonInputFormatter with NewtonsoftJsonInputFormatter and MvcJsonOptions with MvcNewtonsoftJsonOptions.
Warning: this doesn't work
I've been trying to create an attribute which would re-read the body of the request and return a bad result:
public class OnlyValidParametersAttribute : ActionFilterAttribute
{
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (context.HttpContext.Request.ContentType.Contains("application/json"))
{
context.HttpContext.Request.EnableBuffering();
StreamReader stream = new StreamReader(context.HttpContext.Request.Body);
string body = await stream.ReadToEndAsync(); // Always set as "".
try
{
JsonConvert.DeserializeObject(
body,
context.ActionDescriptor.Parameters.FirstOrDefault()!.ParameterType,
new JsonSerializerSettings
{
MissingMemberHandling = MissingMemberHandling.Error,
}
);
}
catch (MissingMemberException e) // Unsure if this is the right exception to catch.
{
context.Result = new UnprocessableEntityResult();
}
base.OnActionExecuting(context);
}
await next();
}
}
However, in my .NET Core 3.1 application, body is always an empty string.
I'd return Content with json serialised with desired settings:
var serializeOptions = new JsonSerializerOptions
{
...
};
return Content(JsonSerializer.Serialize(data, options), "application/json");
For multiple methods I'd create a helper method.

Lowercase Json result in ASP.NET MVC 5

When my json gets returned from the server, it is in CamelCase, but I need lowercase. I have seen a lot of solutions for ASP.NET Web API and Core, but nothing for ASP.NET MVC 5.
[HttpGet]
public JsonResult Method()
{
var vms = new List<MyViewModel>()
{
new MyViewModel()
{
Name = "John Smith",
}
};
return Json(new { results = vms }, JsonRequestBehavior.AllowGet);
}
I want "Names" to be lowercase.
The best solution I have for this is to override the default Json method to use Newtonsoft.Json and set it to use camelcase by default.
First thing is you need to make a base controller if you don't have one already and make your controllers inherit that.
public class BaseController : Controller {
}
Next you create a JsonResult class that will use Newtonsoft.Json :
public class JsonCamelcaseResult : JsonResult
{
private static readonly JsonSerializerSettings _settings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
Converters = new List<JsonConverter> { new StringEnumConverter() }
};
public override void ExecuteResult(ControllerContext context)
{
HttpResponseBase response = context.HttpContext.Response;
response.ContentType = !String.IsNullOrEmpty(this.ContentType) ? this.ContentType : "application/json";
response.ContentEncoding = this.ContentEncoding ?? response.ContentEncoding;
if (this.Data == null)
return;
response.Write(JsonConvert.SerializeObject(this.Data, _settings));
}
}
Then in your BaseController you override the Json method :
protected new JsonResult Json(object data)
{
return new JsonCamelcaseResult
{
Data = data,
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
}
So in the end in your original action returning JSON you just keep it the same and the properties will be camelcased (propertyName) instead of pascalcased (PropertyName) :
[HttpGet]
public JsonResult Method()
{
var vms = new List<MyViewModel>()
{
new MyViewModel()
{
Name = "John Smith",
}
};
return Json(new { results = vms });
}

Asp WebApi: Prettify ActionResult JSON output

I'm working on REST service based on ASP.NET Core Web API and want to add a parameter 'prettify' to my endpoint so that response json will be formatted with indentation and readable in web browser.
My question - how can I change JSON formatting per controller method in ASP.WEB API Core application?
Appreciate you help.
Thank to #Nkosi comment, I've found the solution. Below is a code of the action filter that looks for 'prettify' parameter and adds indentation to output JSON. If the parameter omitted, indentation is also added.
public class OutputFormatActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
}
public void OnActionExecuted(ActionExecutedContext context)
{
var actionResult = context.Result as ObjectResult;
if (actionResult == null) return;
var paramObj = context.HttpContext.Request.Query["prettify"];
var isPrettify = string.IsNullOrEmpty(paramObj) || bool.Parse(paramObj);
if (!isPrettify) return;
var settings = new JsonSerializerSettings { Formatting = Formatting.Indented };
actionResult.Formatters.Add(new JsonOutputFormatter(settings, ArrayPool<char>.Shared));
}
}
Create an action filter that can be used on which ever action you want to add that functionality. I had achieved the same in Asp.Net Web API 2 (not core) using a DelegatingHandler that would inspect the request and update the json formatter's indent based on prettify=true query parameter in the URL
Here is how it was done with the delegating handler
/// <summary>
/// Custom handler to allow pretty print json results.
/// </summary>
public class PrettyPrintJsonHandler : DelegatingHandler {
const string prettyPrintConstant = "pretty";
MediaTypeHeaderValue contentType = MediaTypeHeaderValue.Parse("application/json;charset=utf-8");
private System.Web.Http.HttpConfiguration httpConfig;
/// <summary>
/// Initializes a new instance of the <seealso cref="PrettyPrintJsonHandler"/> class with an HTTP Configuration.
/// </summary>
/// <param name="config"></param>
public PrettyPrintJsonHandler(System.Web.Http.HttpConfiguration config) {
this.httpConfig = config;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) {
var canPrettyPrint = checkQuery(request.RequestUri.Query);
var jsonFormatter = httpConfig.Formatters.JsonFormatter;
jsonFormatter.Indent = canPrettyPrint;
var response = await base.SendAsync(request, cancellationToken);
if (canPrettyPrint && response.Content != null) {
response.Content.Headers.ContentType = contentType;
}
return response;
}
private bool checkQuery(string queryString) {
var canPrettyPrint = false;
if (!string.IsNullOrWhiteSpace(queryString)) {
var prettyPrint = QueryString.Parse(queryString)[prettyPrintConstant];
canPrettyPrint = !string.IsNullOrWhiteSpace(prettyPrint) && Boolean.TryParse(prettyPrint, out canPrettyPrint) && canPrettyPrint;
}
return canPrettyPrint;
}
}
which was added as a global message handler during setup.
The same concept could be applied to an action filter.
Based on #Valentine's answer, and for Newtonsoft Json.NET (and .NET Core), I came up with this action filter implementation, which formats the response for content values except where the content value type is string:
public class OutputFormatActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
}
public void OnActionExecuted(ActionExecutedContext context)
{
var objectResult = context.Result as ObjectResult;
if (objectResult?.Value == null) return;
if (objectResult.Value is string) return; // ignore string values - it would add extra quotes around the string in the response
var jsonFormatter = new NewtonsoftJsonOutputFormatter(
new JsonSerializerSettings
{
// set custom json settings here
},
ArrayPool<char>.Shared,
new MvcOptions(),
new MvcNewtonsoftJsonOptions()
);
objectResult.Formatters.Add(jsonFormatter);
}
}
Register the action filter in the Startup.ConfigureServices to apply it to all MVC/Web API controllers:
services.AddControllersWithViews(options =>
{
options.Filters.Add(new OutputFormatActionFilter());
});

WebApi Fails to Honor JsonObjectAttribute Settings

So I have an ApiController...
public class MetaDataController : ApiController
{
[HttpPost]
public HttpResponseMessage Test(TestModel model)
{
//Do Stuff
return new HttpResponseMessage(HttpStatusCode.OK);
}
}
That accepts a model...
[JsonObject(ItemRequired = Required.Always)]
public class TestModel
{
public int Id { get; set; }
public IEnumerable<SubModel> List { get; set; }
}
public class SubModel
{
public int Id { get; set; }
}
In the form of Json...
{ "Id": 1, "List": [{ "Id": 11 }, { "Id": 12 } ] }
When posting to this controller action, the attribute on TestModel should make Json.Net throw a JsonSerializationException when the Json is missing a property. I wrote unit tests around ensuring this behavior works as expected.
[Test]
public void Test()
{
var goodJson = #"{ 'Id': 1,
'List': [ {'Id': 11}, {'Id': 12} ]
}";
Assert.DoesNotThrow(() => JsonConvert.DeserializeObject<TestModel>(goodJson));
var badJson = #"{ 'Id': 1 }";
Assert.That(()=>JsonConvert.DeserializeObject<TestModel>(badJson),
Throws.InstanceOf<JsonSerializationException>().
And.Message.Contains("Required property 'List' not found in JSON."));
}
When posting well-formed Json to the controlelr action, everything works fine. But if that json is missing a required property, no exception is thrown. The members of TestModel that map to the missing properties are null.
Why does JsonConvert work as expected, but the automatic Json deserialization through the WebApiController fail to honor the attributes on TestModel?
For giggles, I decided to be EXTRA sure that my application was using Json.Net for json deserialization. So I wrote a MediaTypeFormatter
public class JsonTextFormatter : MediaTypeFormatter
{
public readonly JsonSerializerSettings JsonSerializerSettings;
private readonly UTF8Encoding _encoding;
public JsonTextFormatter(JsonSerializerSettings jsonSerializerSettings = null)
{
JsonSerializerSettings = jsonSerializerSettings ?? new JsonSerializerSettings();
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
_encoding = new UTF8Encoding(false, true);
SupportedEncodings.Add(_encoding);
}
public override bool CanReadType(Type type)
{
if (type == null)
{
throw new ArgumentNullException();
}
return true;
}
public override bool CanWriteType(Type type)
{
return true;
}
public override Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
{
var serializer = JsonSerializer.Create(JsonSerializerSettings);
return Task.Factory.StartNew(() =>
{
using (var streamReader = new StreamReader(readStream, _encoding))
{
using (var jsonTextReader = new JsonTextReader(streamReader))
{
return serializer.Deserialize(jsonTextReader, type);
}
}
});
}
public override Task WriteToStreamAsync(Type type, Object value, Stream writeStream, HttpContent content, TransportContext transportContext)
{
var serializer = JsonSerializer.Create(JsonSerializerSettings);
return Task.Factory.StartNew(() =>
{
using (
var jsonTextWriter = new JsonTextWriter(new StreamWriter(writeStream, _encoding))
{
CloseOutput = false
})
{
serializer.Serialize(jsonTextWriter, value);
jsonTextWriter.Flush();
}
});
}
}
And modified my WebApiConfig to use it instead of the default.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Filters.Add(new HandleSerializationErrorAttribute());
config.Formatters.RemoveAt(0);
var serializerSettings = new JsonSerializerSettings
{
MissingMemberHandling = MissingMemberHandling.Error
};
config.Formatters.Insert(0, new JsonTextFormatter(serializerSettings));
}
}
I also added a ExceptionFilterAttribute to catch serialization errors and return relevant information about what was what was wrong.
public class HandleSerializationErrorAttribute : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
if (context.Exception is JsonSerializationException)
{
var responseMessage = new HttpResponseMessage(HttpStatusCode.BadRequest);
responseMessage.Content = new StringContent(JsonConvert.SerializeObject(context.Exception.Message));
context.Response = responseMessage;
}
}
}
So there it is: .net MVC 4 WebApi SAYS that it uses Json.Net, but the default JsonFormatter refused to obey Json attributes that decorated my models. Explicitly setting the formatter manually fixes the problem.

Categories