i want to change the incoming requests deserializing format just for one of my controllers. so i added this in to my Global.asax and it works just fine:
HttpConfiguration config = GlobalConfiguration.Configuration;
config.Formatters.JsonFormatter.SerializerSettings = new Newtonsoft.Json.JsonSerializerSettings
{
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new SnakeCaseNamingStrategy()
}
};
but it apply the changes to all of the controllers. i just want apply it for one of my controllers. i also found this answer and i wrote this code according to that:
public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor)
{
var formatter = controllerSettings.Formatters.OfType<JsonMediaTypeFormatter>().Single();
controllerSettings.Formatters.Remove(formatter);
formatter = new JsonMediaTypeFormatter
{
SerializerSettings = { ContractResolver = new DefaultContractResolver { NamingStrategy = new SnakeCaseNamingStrategy() } }
};
controllerSettings.Formatters.Add(formatter);
}
but unfortunately it works just for serializing the outputs. is there a way to define it for deserializing inputs?
You can do what you need with a tricky media type formatter. Usually custom formatter overrides methods CanReadType() / CanWriteType() and ReadFromStreamAsync() / WriteToStreamAsync(). CanWriteType() in your case should always return false since you are not intersted in customizing serialization. As regards deserialization you could use standard JsonMediaTypeFormatter (through inheritance or aggregation) and set its SerializerSettings to use SnakeCaseNamingStrategy:
public class SnakeCaseJsonFormatter : JsonMediaTypeFormatter
{
public SnakeCaseJsonFormatter()
{
SerializerSettings = new JsonSerializerSettings
{
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new SnakeCaseNamingStrategy()
}
};
}
public override bool CanWriteType(Type type)
{
return false;
}
public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
{
throw new NotImplementedException();
}
}
Remaining part is applying of such custom formatter on controller level. You could do this with a custom attribute implementing IControllerConfiguration interface. In Initialize() method set your custom formatter at first position so that it takes precedence over standard JsonMediaTypeFormatter. You should not remove standard JsonMediaTypeFormatter because it will handle data serializing:
public sealed class SnakeCaseNamingAttribute : Attribute, IControllerConfiguration
{
public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor)
{
controllerSettings.Formatters.Insert(0, new SnakeCaseJsonFormatter());
}
}
Now just apply this attribute on controller you want and voila:
[SnakeCaseNaming]
public class ValuesController : ApiController
Related
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.
Any idea how to generate json key as snake_case with RestSharp?
With Newtonsoft.Json, I can set json output something like this
DefaultContractResolver contractResolver = new DefaultContractResolver
{
NamingStrategy = new SnakeCaseNamingStrategy()
};
string json = JsonConvert.SerializeObject(requestData, new JsonSerializerSettings
{
ContractResolver = contractResolver,
Formatting = Formatting.Indented
});
But I not sure how can be done with RestSharp
var client = new RestClient(getService.MstUrl);
client.AddDefaultHeader("Authorization", string.Format("Bearer {0}", token));
var request = new RestRequest(Method.POST).AddJsonBody(requestData);
var response = await client.ExecuteAsync(request);
It keep generate as camelCase. Is there any configuration like Newtonsoft.Json?
Using RestSharp, does not mean you can't use the Newtonsoft serializer as well.
From the restsharp documentation:
RestSharp support Json.Net serializer via a separate package. You can install it from NuGet.
client.UseNewtonsoftJson();
And keep on going with what you did:
DefaultContractResolver contractResolver = new DefaultContractResolver
{
NamingStrategy = new SnakeCaseNamingStrategy()
};
You should be able to create your own IRestSerializer implementation and supply that in client.UseSerializer
Given
public class SimpleJsonSerializer : IRestSerializer
{
private readonly DefaultContractResolver _contractResolver;
public SimpleJsonSerializer()
{
_contractResolver = new DefaultContractResolver
{
NamingStrategy = new SnakeCaseNamingStrategy()
};
}
public string Serialize(object obj) => JsonConvert.SerializeObject(obj, new JsonSerializerSettings
{
ContractResolver = _contractResolver,
Formatting = Formatting.Indented
});
public string Serialize(Parameter bodyParameter) => Serialize(bodyParameter.Value);
public T Deserialize<T>(IRestResponse response) => JsonConvert.DeserializeObject<T>(response.Content);
public string[] SupportedContentTypes { get; } =
{
"application/json", "text/json", "text/x-json", "text/javascript", "*+json"
};
public string ContentType { get; set; } = "application/json";
public DataFormat DataFormat { get; } = DataFormat.Json;
}
Usage
var client = new RestClient(getService.MstUrl);
client.AddDefaultHeader("Authorization", string.Format("Bearer {0}", token));
client.UseSerializer(() => new SimpleJsonSerializer(){});
var request = new RestRequest(Method.POST).AddJsonBody(requestData);
var response = await client.ExecuteAsync(request);
Disclaimer : I haven't used RestSharp in years and I never intend to use it again. Further more, I really do suggest to anyone thinking of using it to switch back to HttpClient, or even better IHttpClientFactory. More so, this is completely untested, and only given as an example. You may need to modify it to it your needs
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)
);
}
}
Basically, I'm trying to make my WebApi more generic:
Model:
public abstract class A {}
public class B : A { }
Client:
using (var client = new HttpClient())
{
client.BaseAddress = new Uri("http://localhost:49611/");
var aList = List<A> { new B() };
var response = await client.PostAsJsonAsync("api/a", aList);
}
WebApi controller:
public class AController : ApiController
{
public void Post([FromBody]List<A> aList)
{
// ...
}
}
Trying to execute this code, I get an empty list in AController (because A is abstract).
If I remove "abstract" for class A, then I get an item in aList, the type is A, not B as I would expect.
From what I googled, I thought, TypeNameHandling.Auto should be the solution as
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
//...
config.Formatters.JsonFormatter.SerializerSettings.TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Auto;
}
}
However, it doesn't work for me.
However, as a test, I do see TypeNameHandling.Auto working if I use JsonConverter directly:
var aList = List<A> { new B() };
var settings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto };
var str = JsonConvert.SerializeObject(aList, Formatting.Indented, settings);
var obj = JsonConvert.DeserializeObject<List<A>>(str, settings);
In this case, I do get a list of B (instead of A) in obj.
So, why it doesn't work for my WebApi?
I found this post, which is more than two years old, saying "Web API does not use this overload" of public void Serialize(JsonWriter jsonWriter, object value, Type objectType);. I can't believe it's still true after two years. Anyway, in the accepted answer of that post, Kiran provided a custom json formatter, which should fix this problem. However, when I try to plug it into my WebApiConfig, it doesn't work for me. In fact, the overridden WriteToStreamAsync doesn't get called at all - really strange.
Here is Kiran's code:
public class CustomJsonFormatter : JsonMediaTypeFormatter
{
public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
{
try
{
Encoding effectiveEncoding = SelectCharacterEncoding(content == null ? null : content.Headers);
if (!UseDataContractJsonSerializer)
{
using (JsonTextWriter jsonTextWriter = new JsonTextWriter(new StreamWriter(writeStream, effectiveEncoding)) { CloseOutput = false })
{
if (Indent)
{
jsonTextWriter.Formatting = Newtonsoft.Json.Formatting.Indented;
}
JsonSerializer jsonSerializer = JsonSerializer.Create(this.SerializerSettings);
jsonSerializer.Serialize(jsonTextWriter, value, type); //NOTE: passing in 'type' here
jsonTextWriter.Flush();
}
}
else
{
return base.WriteToStreamAsync(type, value, writeStream, content, transportContext);
}
return TaskHelpers.Completed();
}
catch (Exception e)
{
return TaskHelpers.FromError(e);
}
}
}
internal class TaskHelpers
{
private static readonly Task _defaultCompleted = FromResult<AsyncVoid>(default(AsyncVoid));
/// <summary>
/// Used as the T in a "conversion" of a Task into a Task{T}
/// </summary>
private struct AsyncVoid
{
}
internal static Task<TResult> FromResult<TResult>(TResult result)
{
TaskCompletionSource<TResult> tcs = new TaskCompletionSource<TResult>();
tcs.SetResult(result);
return tcs.Task;
}
/// <summary>
/// Returns an error task. The task is Completed, IsCanceled = False, IsFaulted = True
/// </summary>
internal static Task FromError(Exception exception)
{
return FromError<AsyncVoid>(exception);
}
/// <summary>
/// Returns an error task of the given type. The task is Completed, IsCanceled = False, IsFaulted = True
/// </summary>
/// <typeparam name="TResult"></typeparam>
internal static Task<TResult> FromError<TResult>(Exception exception)
{
TaskCompletionSource<TResult> tcs = new TaskCompletionSource<TResult>();
tcs.SetException(exception);
return tcs.Task;
}
/// <summary>
/// Returns a completed task that has no result.
/// </summary>
internal static Task Completed()
{
return _defaultCompleted;
}
}
Here is my WebApiConfig:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.formatters.clear();
config.Formatters.Add(new CustomJsonFormatter() { SerializerSettings = new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.Auto } });
}
}
Anybody has a clue?
UPDATE and SOLUTION:
Okay, after some struggle, I figured it out myself. The problem is in client request using clien.PostAsJsonAsync. I have to use another function PostAsync with formatter parameter to add TypeNameHandling.Auto, as follows.
var formatter = new JsonMediaTypeFormatter() { SerializerSettings = new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.Auto } };
var response = await client.PostAsync("api/a", aList, formatter);
With this and TypeNameHandling.Auto in WebApiConfig.cs together, derived classes are correctly deserialized and serialized.
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.