WebApi Fails to Honor JsonObjectAttribute Settings - c#

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.

Related

MVC - How to deserialize a Dictionary without having brackets [0] added to the key?

my deserialize Dictionary's key results in "brand[0]" when I send in "brand" to the api.
I have a class like this:
public class SearchRequest
{
public bool Html { get; set; } = false;
public Dictionary<string, HashSet<string>> Tags { get; set; }
}
// MVC Controller
[HttpPost]
public ActionResult Index(SearchRequest searchRequest)
{
...
}
And a json request like this that I post to the controller:
{
"html": true,
"tags": {
"brand": [
"bareminerals"
]
}
}
The binding seams to work and the searchRequest object is created but the resulting dictionary dose not have the key "brand" in it but insted the key "brand[0]" how can I preserve the real values I send in?
Edit: I need tags to be able to contain multiple tags, with multiple options, this was a simpel example.
One soulution to my problem is to create a custom model bind, so this is what am using now, but I dont understand why I need to, and I feel like there should be a easyer way? But am gonna leve It here anyhow.
public class FromJsonBodyAttribute : CustomModelBinderAttribute
{
public override IModelBinder GetBinder()
{
return new JsonModelBinder();
}
private class JsonModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var stream = controllerContext.HttpContext.Request.InputStream;
stream.Position = 0;
using (var reader = new StreamReader(stream))
{
var checkoutOrderDataStr = reader.ReadToEnd();
return JsonConvert.DeserializeObject(checkoutOrderDataStr, bindingContext.ModelType);
}
}
}
}
I'm not sure what is going on with your setup. You should not need a custom binder. I still think the problem is most likely with your calling code - whatever you're using as a client.
I'm using Asp.net Core 3.1. Here's what I threw together as a quick test.
Created Asp.net Core web application template with MVC. I declared two classes - a request POCO and a result POCO. The request was your class:
public class SearchRequest
{
public bool Html { get; set; } = false;
public Dictionary<string, HashSet<string>> Tags { get; set; }
}
The result was the same thing with a datetime field added just for the heck of it:
public class SearchResult : SearchRequest
{
public SearchResult(SearchRequest r)
{
this.Html = r.Html;
this.Tags = r.Tags;
}
public DateTime RequestedAt { get; set; } = DateTime.Now;
}
I Added a simple post method on the default HomeController.
[HttpPost]
public IActionResult Index([FromBody] SearchRequest searchRequest)
{
return new ObjectResult(new SearchResult(searchRequest));
}
I added a console Application to the solution to act as a client. I copied the two class definitions into that project.
I added this as the main method. Note you can either have the camel casing options on the request or not - asp.net accepted either.
static async Task Main(string[] _)
{
var tags = new[] { new { k = "brand", tags = new string[] { "bareminerals" } } }
.ToDictionary(x => x.k, v => new HashSet<string>(v.tags));
var request = new SearchRequest() { Html = true, Tags = tags };
var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
var json = JsonSerializer.Serialize(request, options);
Console.WriteLine(json);
using (var client = new HttpClient())
{
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync("http://localhost:59276", content);
response.EnsureSuccessStatusCode();
var data = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<SearchResult>(data, options);
Console.WriteLine(data);
var keysSame = Enumerable.SequenceEqual(request.Tags.Keys, result.Tags.Keys);
var valuesSame = Enumerable.SequenceEqual(request.Tags.Values.SelectMany(x => x),
result.Tags.Values.SelectMany(x=>x));
Console.WriteLine($"Keys: {keysSame} Values: {valuesSame}");
}
}
This outputs:
{"html":true,"tags":{"brand":["bareminerals"]}}
{"requestedAt":"2020-10-30T19:22:17.8525982-04:00","html":true,"tags":{"brand":["bareminerals"]}}
Keys: True Values: True

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

complex custom model binding in aspnet core with inheritance in property's class

I am trying to do custom model binding in Core(2.2/3.1) with an inherited sub class.
I use IModelBinderProvider and IModelBinder to manipulate my model binding as MVC doesn't know whether to translate the base class Device to a Teddybear or a Legobrick.
IModelBinder's BindModelAsync method gets called for my Product class and I guess that is where I should look for the Data property and check its Kind. Then from the parameter bindingContext.Model extrude the Device data and replace the Data property's value with a Teddybear or Legobrick.
But bindingContext.Model is null; I have no data.
There is an example at towards the bottom of MSDN but in it, it is the root that is the base class.
I have a regular root but a property is a base/inherited class construct.
Somewhere I don't get the calls correctly hooked up or I haven't found the correct way to read data.
I guess my IModelBinderProvider is correct, it catches the Product type and adds binders to the sub classes Teddybear and Legobrick.
public class DeviceTypeDataContractProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
var binders = new Dictionary<Type, (ModelMetadata, IModelBinder)>();
if (context.Metadata.ModelType == typeof(Product))
{
foreach (var type in new[] { typeof(Teddybear), typeof(Legobrick) })
{
var modelMetadata = context.MetadataProvider.GetMetadataForType(type);
binders[type] = (modelMetadata, context.CreateBinder(modelMetadata));
}
}
else
{
return null;
}
return new DeviceModelBinder(binders);
}
}
The code at IModelBinder/BindModelAsync still eludes me.
public class DeviceModelBinder : IModelBinder
{
private Dictionary<Type, (ModelMetadata, IModelBinder)> binders;
public DeviceModelBinder(Dictionary<Type, (ModelMetadata, IModelBinder)> binders)
{
this.binders = binders;
}
public async Task BindModelAsync(ModelBindingContext bindingContext){
... I totally lost it here and am beginning to feel dizzy.
}
}
Over internet comes a call like:
"product": {
"id": "56-1",
"data": {
"kind": "teddy",
"name": "Tutu"
}
}
or
"product": {
"id": "66-1",
"data": {
"kind": "lego",
"studCount": 8
}
}
which Aspnet uses to populate:
public class Product{
string Id {get;set;}
Device Data{get;set;}
}
public class Device{
string Kind {get;set}
}
public class Teddybear: Device{
string Name {get;set}
}
public class Legobrick: Device{
int StudCount {get;set}
}
The controller is regular and the custom modelling is hooked up:
[HttpPost]
public async Task<IActionResult> Create([FromBody] Product product){...
services.AddMvc(options => {
...
options.Filters.Add(new AuthorizeFilter(policy));
})
.AddJsonOptions(options => {
options.ModelBinderProviders.Insert(0, new DeviceTypeDataContractProvider());
});
I think the solution provided in documentation won't work in your case because of you using json. A simple working example would be
public class DeviceTypeDataContractProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(Product))
{
return new DeviceModelBinder();
}
return null;
}
}
public class DeviceModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var reader = new JsonTextReader(new StreamReader(bindingContext.HttpContext.Request.Body));
//loading request json
var jObject = JObject.Load(reader);
JToken data = jObject["data"];
Product result = jObject.ToObject<Product>();
switch (result.Data.Kind)
{
case "teddy":
result.Data = data.ToObject<Teddybear>();
break;
case "lego":
result.Data = data.ToObject<Legobrick>();
break;
default:
bindingContext.Result = ModelBindingResult.Failed();
return Task.CompletedTask;
}
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}

How to globally handle exceptions in ASP.NET WebApi when exception is raised through JsonNetFormatter

We develop ASP.NET MVC5 app with WebApi2 and AngularJs. For serialization and deserialization we use custum JsonNetFormatter as follow:
public class JsonNetFormatter : MediaTypeFormatter
{
// other codes for formatting
public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
{
string NameOfSet = "";
ObjectWrapperWithNameOfSet obj = value as ObjectWrapperWithNameOfSet;
if (obj != null)
{
NameOfSet = obj.NameOfSet;
value = obj.WrappedObject;
}
_jsonSerializerSettings.ContractResolver = new CustomContractResolver(NameOfSet);
// Create a serializer
JsonSerializer serializer = JsonSerializer.Create(_jsonSerializerSettings);
// Create task writing the serialized content
return Task.Factory.StartNew(() =>
{
using (JsonTextWriter jsonTextWriter = new JsonTextWriter(new StreamWriter(writeStream, SupportedEncodings[0])) { CloseOutput = false })
{
serializer.Serialize(jsonTextWriter, value);
jsonTextWriter.Flush();
}
});
}
}
and WebApiConfig as follow:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.Formatters.Clear();
config.Formatters.Insert(0, new JsonNetFormatter());
}
}
The application work fine. but in some situation we get an errors when deserialization json data. My question is How can we handle these errors and send to the client side?
Example for the errors:
You can create your own custom exception handler to modify any error messages before sending it to client
public class ErrorHandler : ExceptionHandler
{
public override void Handle(ExceptionHandlerContext context)
{
context.Result = new TextPlainErrorResult()
{
Request = context.ExceptionContext.Request,
Content = "Oops! Sorry! Something went wrong." + "Please contact support so we can try to fix it."
};
}
private class TextPlainErrorResult : IHttpActionResult
{
public HttpRequestMessage Request { get; set; }
public string Content { get; set; }
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.InternalServerError, Content);
return Task.FromResult(response);
}
}
}
and then register this custom class in config class like following
config.Services.Replace(typeof(IExceptionHandler), new ErrorHandler());

Is it possible to change the MediaTypeFormatter to JSON for only one class?

I have a web api, where the global configuration is configured to use:
XmlMediaTypeFormatter
My problem is I wont to extend this web api with a new controller, that uses the JsonMediaTypeFormatter instead.
Is it possible to change the MediaTypeFormatter to JSON for only one API Controller class?
My problem is not returning JSON, I have accumplished this with returning HttpResponseMessage:
return new HttpResponseMessage
{
Content = new ObjectContent<string>("Hello world", new JsonMediaTypeFormatter()),
StatusCode = HttpStatusCode.OK
};
It's on the request I get the problem. If I have an object with two properties:
public class VMRegistrant
{
public int MerchantId { get; set; }
public string Email { get; set; }
}
And my controller action takes the VMRegistrant as argument:
public HttpResponseMessage CreateRegistrant(VMRegistrant registrant)
{
// Save registrant in db...
}
But the problem is when I call the action with JSON it fails.
You can have your controller return an IHttpActionResult and use the extension method HttpRequestMessageExtensions.CreateResponse<T> and specify the formatter you want to use:
public IHttpActionResult Foo()
{
var bar = new Bar { Message = "Hello" };
return Request.CreateResponse(HttpStatusCode.OK, bar, new MediaTypeHeaderValue("application/json"));
}
Another possibility is to use the ApiController.Content method:
public IHttpActionResult Foo()
{
var bar = new Bar { Message = "Hello" };
return Content(HttpStatusCode.OK, bar, new JsonMediaTypeFormatter(), new MediaTypeHeaderValue("application/json"));
}
Edit:
One possibility is to read and deserialize the content yourself from the Request object via reading from the stream and using a JSON parser such as Json.NET to create the object from JSON:
public async Task<IHttpActionResult> FooAsync()
{
var json = await Request.Content.ReadAsStringAsync();
var content = JsonConvert.DeserializeObject<VMRegistrant>(json);
}
Yes, it's possible to change the MediaTypeFormatters for only one class/controller. If you want to save and restore the default formatters you can follow these steps:
In the beginning of the request save old formatters
Clear the formatters collection
Add the desired formatter
At the end of the request copy back the old formatters
I think this is easily done by an ActionFilterAttribute:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class ChangeFormatterAttribute : ActionFilterAttribute
{
private IEnumerable<MediaTypeFormatter> oldFormatters;
private MediaTypeFormatter desiredFormatter;
public ChangeFormatterAttribute(Type formatterType)
{
this.desiredFormatter = Activator.CreateInstance(formatterType) as MediaTypeFormatter;
}
public override void OnActionExecuting(HttpActionContext actionContext)
{
var formatters = actionContext.ControllerContext.Configuration.Formatters;
oldFormatters = formatters.ToList();
formatters.Clear();
formatters.Add(desiredFormatter);
base.OnActionExecuting(actionContext);
}
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
var formatters = actionExecutedContext.ActionContext.ControllerContext.Configuration.Formatters;
formatters.Clear();
formatters.AddRange(oldFormatters);
base.OnActionExecuted(actionExecutedContext);
}
}
And the usage:
[ChangeFormatterAttribute(typeof(JsonMediaTypeFormatter))]
public class HomeController : ApiController
{
public string Get()
{
return "ok";
}
}
// ...
[ChangeFormatterAttribute(typeof(XmlMediaTypeFormatter))]
public class ValuesController : ApiController
{
public string Get()
{
return "ok";
}
}
Maybe you could have your media type formatter only accept the type that is handled by your controller:
public class Dog
{
public string Name { get; set; }
}
public class DogMediaTypeFormatter : JsonMediaTypeFormatter
{
public override bool CanReadType(Type type)
{
return type == typeof (Dog);
}
}
Probably not the best solution though :I

Categories