.Net Web Api : consistent Json deserialization strategy between the API and a file upload? - asp.net

I have a Asp.Net 6+ Web Api that has two endpoints doing almost exactly the same thing :
- the first one gets its parameters automagically from Asp.Net . I didn't give it a second thought: it accepts parameters from the POST's body and it's Asp.Net that does the deserialization, via System.Text.Json internally.
[HttpPost]
[Route("public/v1/myRoute/")]
public async Task<ActionResult> Import(IEnumerable<JsonItemModel> items) {
// the items are already ready to use.
FooProcessItems(items);
}
- the second one receives an IFormFile in a form data (the end-user uploads a file by using a button in the UI), gets the stream, and deserializes it "manually" using System.Text.JsonSerializer.DeserializeAsync.
[HttpPost]
[Route("public/v1/myRouteWithFile/")]
public async Task<ActionResult<Guid>> ImportWithFile([FromForm] MyFormData formData
) {
var stream = formaData.File.OpenReadStream();
var items = await JsonSerializer.DeserializeAsync<IEnumerable<JsonItemModel>>(file);
FooProcessItems(items);
}
My question :
I want to customize the deserialization process (to add some constraints such as "this field cannot be null", etc.) and I want both methods to produce exactly the same result.
How do I do that?
Is it simply a case of adding Json decorators in the model and letting .Net do the rest?
public class JsonItemModel {
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] // <-- Some custom constraint that will be picked up both by Deserialize and the POST endpoint.
public int SomeField { get; init; } = 0;
...
}

Related

Custom action filter attribute performs poorly when compared to RequestSizeLimit attribute

I have a use-case when I need to limit the allowed request size for the file upload WebAPI endpoint. Normally I would use the in-built attribute RequestSizeLimit:
[HttpPost]
[RequestSizeLimit(104_857_600)] // 100mb limit
public async Task<IActionResult> Upload([FromForm] IFormFile file) {
}
However, per requirements I need to make the request size limit to be configurable and shared with rest of the application (for example with SPA application which uses the same value for it's own validation before user uploads the file, also with unit-tests etc..). So to cover this use-case I have implemented my own action filter:
public class CustomRequestSizeValidatorAttribute : ActionFilterAttribute
{
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var request = context.HttpContext.Request;
var contentLength = request.ContentLength;
if (contentLength > SharedConsts.MaximumFileUploadSize)
{
var errorMessage = "Request content length is too large";
context.Result = new BadRequestObjectResult(errorMessage);
}
else
{
await next();
}
}
}
And:
public static class SharedConsts
{
public static readonly long MaximumFileUploadSize = 104_857_600; // 100 mb
}
And use it as:
[HttpPost]
[CustomRequestSizeValidator]
public async Task<IActionResult> Upload([FromForm] IFormFile file) {
}
And here comes the problem. I upload the 180mb file via Postman and here are the service response times:
without any of the 2 attributes ~ 2.5 - 3 seconds
with [CustomRequestSizeValidator] ~ same time (2.5 - 3 seconds)
with [RequestSizeLimit] ~ 22ms !
Why is my custom attribute [CustomRequestSizeValidator] so slow, as well as how to make the request execute just as fast as with the native attribute from MS [RequestSizeLimit]?
From the diagnostic tools I see that when [CustomRequestSizeValidator] attribute is used - memory consumption grows every time the request is made. With [RequestSizeLimit] attribute - memory never grows. So the problem is probably the Order in which logic is executed by the framework. My custom attribute is most likely executed as the very last step in the pipeline.. But I still have no idea how to fix it so any advices are welcomed.

NET Core: Filtering with HttpGet

I would like to filter a list of vehicles, by their makeId using httpGet. The URL I would expect to use is:
https://localhost:5001/api/vehicle?makeId=2
Below, I will define the DTO and controller methods I used for this task:
FilterDto
public class FilterDTO
{
public int? MakeId { get; set; }
}
Below are the 2 HTTPGet methods in my controller class. I expect the first method to be called.
[HttpGet]
public async Task<IEnumerable<VehicleDTO>> Get(FilterDTO filterDto)
{
var filter = _mapper.Map<Filter>(filterDto);
var vehicles = await _vehicleRepository.GetAll(filter);
return _mapper.Map<IEnumerable<VehicleDTO>>(vehicles);
}
[HttpGet("{id}")]
public async Task<ActionResult<VehicleDTO>> Get(long id)
{
var vehicle = await _vehicleRepository.GetWithRelated(id);
if (vehicle == default)
{
return BadRequest("Vehicle not found");
}
var result = _mapper.Map<VehicleDTO>(vehicle);
return Ok(result);
}
With the above code, when I call the URL above, in Postman I get a 400 Error, saying "The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true. Path: $ | LineNumber: 0 | BytePositionInLine: 0."
I get the same result for https://localhost:5001/api/vehicle
If I change the first Get method like below, I am able to get a response:
[HttpGet]
public async Task<IEnumerable<VehicleDTO>> Get(int? makeId)
{
var filter = new Filter { MakeId = makeId};
var vehicles = await _vehicleRepository.GetAll(filter);
return _mapper.Map<IEnumerable<VehicleDTO>>(vehicles);
}
After this (lengthy) introduction, my questions are:
Why does HttpGet support 'int?' but not the data transfer object 'FilterDto'?
Should I be using a different verb instead of HttpGet?
I might have to filter in the future for some other types (say customerId). Is there any way I can change the method to support custom objects, like FilterDto, ideally without changing the verb?
Change your code as follow:
[HttpGet]
public async Task<IEnumerable<VehicleDTO>> Get([FromQuery] FilterDTO filterDto)
{
var filter = _mapper.Map<Filter>(filterDto);
var vehicles = await _vehicleRepository.GetAll(filter);
return _mapper.Map<IEnumerable<VehicleDTO>>(vehicles);
}
and call it like:
baseUrl/Controller/Get?MarkId=1
Take a look at the docs.
Basically the primitive types are supported, but the controller has no idea how to convert your web request data into C# object. You need to explicitly tell it how you want this custom object to be created out of web request.
You may have in mind that HttpGet methods are only able to receive primitiveTypes (string, int, short, datetime -using a specific format-) because the arguments are being sent through query string, for example:
myAddres.com/api/mymethod?id=5&filter1=value1&filter2=value2
Having this consideration in mind you'll notice there's no way to send any object because you need to use a json or another notation, remember querystring has a limit and because of that is better using "argument=value" notation.
On the other hand PUT and POST are able to send their data through a "body" property where you may use a json notation and this way you may create almost any object on your Backend side.
If you need to use an object as an argument it is a better idea using POST or PUT (better POST than PUT).

Return a data object with a BadRequestResult / BadRequestErrorMessageResult

I'd like to return a data object that contains the details of the error with a BadRequestErrorMessageResult or BadRequestErrorMessageResult object like so:
public IHttpActionResult Action(Model model)
{
var validationResult = model.Validate();
if (validationResult.Successful)
{
// this one's okay; it supports sending data with a 200
return Ok(validationResult);
}
else
{
// However, how do I return a custom data object here
// like so?
// No such overload, I wish there was
// return BadRequest(validationResult);
}
}
The only three overloads of the ApiController.BadRequest() method are:
1. BadRequest();
2. BadRequest(string message);
3. BadRequest(ModelStateDictionary modelState);
Even with #3, a model state dictionary is ultimate a deep collection with one layer upon another, at the bottom of which, though, is a bunch of KeyValuePair<string, ModelError> where each ModelError also only has either a string or an Exception object.
Therefore, even with #3, we are only able to pack a string to send and not a custom object like I want to.
I am really not asking how I may go about working a hack or a kludge around the situation. My question is: is there an overload or another way baked into the .NET API to send an object to the client with a Bad Request HTTP status code?
I am using ASP.NET Web API version 5.2.4 targeting .NET Framework version 4.6.1.
You can use the Content<T>(...) method to do this. It returns a NegotiatedContentResult, which is serialized depending on the request headers (e.g. json, xml), and allows you to specify a HttpStatusCode.
You can use it like this:
return Content(HttpStatusCode.BadRequest, myObject);
If you wanted to, you could create your own BadRequest<T>(T obj) method in the controller as a wrapper, so then you could call it as you wanted:
public IHttpActionResult BadRequest<T>(T obj)
{
return Content(HttpStatusCode.BadRequest, obj);
}
public IHttpActionResult Action()
{
// do whatever validation here.
var validationResult = Validate();
// then return a bad request
return BadRequest(validationResult);
}
You can build/format the string in JSON format, pass it as string in the BadRequest() parameter and convert it to JSON again or any object on the caller's backend.
Haven't tried that but that should work.

asp.net web API HTTP PUT method

I have some resource- UserProfile
public UserProfile
{
public string Email{get;set;}
public string Password{get;set;}
}
I want to change Email and Password separatly (only one for user at same time). I have web api controller for example /api/user/123 that handle requests in RESTful style. Follow the RESTful style i should have one method PUT which update the resource, but i have two task that update the same resource api/user/123. I need to add some feature to PUT request body like
{email:'test#domain.com', changeType:'email'} or {password:'12345678',changeType:'password' } to write some if in my PUT method ? Or there is some other way to update my resource in RESTful style ?
[HttpPut]
public HttpResponseMessage PutProduct(Product p)
{
Product pro = _products.Find(pr => pr.Id == p.Id);
if (pro == null)
return new HttpResponseMessage(HttpStatusCode.NotFound);
pro.Id = p.Id;
pro.Name = p.Name;
pro.Description = p.Description;
return new HttpResponseMessage(HttpStatusCode.OK);
}
You have two options for updating email and password separately.
A) Don't use PUT, use POST
B) Create child resources for updating the individual elements, e.g.
PUT /api/user/123/email
And
PUT /api/user/123/password

Front end ASP.NET MVC4 as one project, and an ASP.NET Web API as another project in same solution - how to call WebAPI from front end?

I have an ASP.NET MVC4 front end as one project in my solution, and a separate ASP.NET Web API as another project in the same solution. The Web API will contain all of my CRUD operations.
2 questions
How do I call my Web API from my front end to perform CRUD operations? I have my entity data model defined in my Web API project, and I will need to bind my front end views to it, how would I do that?
Once this is deployed to my web servers, the front end will reside on one server, and the Web API will reside on another server (the server that holds most of our web services). So, I guess along the same lines, how would I call the Web API from my front end once deployed? I understand Web API's are simply called with an HTTP request, but in terms of passing my models (which are defined in my Web API project) into my Views (in my front end project), how can I do this?
While Kevin is right, I did this the non-Ajax way. Keep in mind that I am working with JSON data, so this is centered around JSON.
In your controller page, remove anything that has to do with DbContext, Entity Framework, etc. The reason is by default, the controller will want to perform CRUD operations by calling the DbContext, and we don't want this. We want to call the WebAPI instead to do this.
First and foremost, declare some member variables in your controller. The rest of your controller will utilize these:
HttpClient client = new HttpClient();
HttpResponseMessage response = new HttpResponseMessage();
Uri contactUri = null;
In your controller, create a constructor for your controller, as such:
public ContactController()
{
// set base address of WebAPI depending on your current environment
client.BaseAddress = new Uri("http://server/YourAPI/");
// Add an Accept header for JSON format.
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
}
Replace the Index action's code with something like the following. Note that the only relevant pieces are the client.GetAsync() call and the var contacts assignment. Everything else is not necessary for the context of this problem. The value inside the client.GetAsync() should be the name of your controller, prepended by any custom routing you set up in your WebApiConfig.cs - in my case, I added the api part in my route to distinguish between API calls and normal calls:
public ActionResult Index()
{
response = client.GetAsync("api/contact").Result;
if (response.IsSuccessStatusCode)
{
var contacts = response.Content.ReadAsAsync<IEnumerable<Contact>>().Result;
return View(contacts);
}
else
{
// add something here to tell the user hey, something went wrong
return RedirectToAction("Index");
}
}
Replace the Create action (the HttpPost action) with something like the following. Again, the only important piece is the client.PostAsJsonAsync() part - this is what calls the WebAPI's POST action which takes care of, in my case, inserting a new record into the database:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Contact contact)
{
// Create a new product
response = client.PostAsJsonAsync("api/contact", contact).Result;
if (response.IsSuccessStatusCode)
{
return RedirectToAction("Index");
}
else
{
// add something here to tell the user hey, something went wrong
return RedirectToAction("Index");
}
}
Replace the Edit action (the non-HttpPost action) with something like the following. This was a little tricky because in order to edit, you had to retrieve the record first, so basically, the HttpPost version of Edit will contain somewhat similar code, with an additional line of code that performs the edit POST (PUT). Below, we're getting the response from the WebAPI by passing it a specific record ID. So, just like for Index (GET), we are doing the same thing only passing in the ID so we only get back one record. Then, we cast the response to an actual object that can be operated on in the View:
public ActionResult Edit(int id = 0)
{
response = client.GetAsync(string.Format("api/contact/{0}", id)).Result;
Contact contact = response.Content.ReadAsAsync<Contact>().Result;
if (contact == null)
{
return HttpNotFound();
}
return View(contact);
}
Replace the Edit action (the HttpPost action) with something like the following. Below, we're getting the record to be edited by calling client.GetAsync() and passing in the primary key as a parameter (contact_id). Then, we're getting the RequestUri from that response and saving it. Then, we're calling client.PutAsJsonAsync() and passing in the Uri.PathAndQuery (what we just saved) as well as the object to be edited.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(Contact contact)
{
response = client.GetAsync(string.Format("api/contact/{0}", contact.contact_id)).Result;
contactUri = response.RequestMessage.RequestUri;
response = client.PutAsJsonAsync(contactUri.PathAndQuery, contact).Result;
if (response.IsSuccessStatusCode)
{
return RedirectToAction("Index");
}
else
{
// add something here to tell the user hey, something went wrong
return RedirectToAction("Index");
}
}
Replace the Delete action (the non-HttpPost action) with something like the following. So again, we're getting the record from the database by simply calling client.GetAsync() and casting it to an actual object my app knows of.
public ActionResult Delete(int id = 0)
{
response = client.GetAsync(string.Format("api/contact/{0}", id)).Result;
Contact contact = response.Content.ReadAsAsync<Contact>().Result;
if (contact == null)
{
return HttpNotFound();
}
return View(contact);
}
Finally, replace the Delete action (the HttpPost action) with something like the following. Again, we're doing something similar to that of the Edit action. We are getting the record to be deleted, casting it to an object, and then passing that object into a client.DeleteAsync() call, as shown below.
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
response = client.GetAsync(string.Format("api/contact/{0}", id)).Result;
contactUri = response.RequestMessage.RequestUri;
response = client.DeleteAsync(contactUri).Result;
return RedirectToAction("Index");
}
You can call your Web API from the client using jQuery ajax method. But since you are calling from a site other than where the Web API is deployed you will have to use JSONP, instead of JSON. Take a look at this QA to see how you use JSONP with Web API. Your models will be passed as JSON which you will have to render on the client side, instead of using Razor to render it on the server side. I would use something like Knockout to create a View Model on the client that will bind your model to the HTML elements on the client.

Resources