.NET 6 fails parsing form-data JSON text into entity - asp.net

I want to pass multiple files and additional data in a single POST request. Currently I am using "multipart/form-data" with 2 keys: "files" (images can be uploaded here) and "data" (JSON text to be parsed into an entity). Problem is that "files" inside controller has correct data and "data" is null.
Postman POST request (JSON text in "data" is correct, double checked it, headers are automatically generated by Postman and left unchanged):
Code of helper class to allow usage of generic types (as I have multiple entities to be created from different JSON texts, also tried to pass the type of my entity here instead of T - still null is being returned in the controller):
public class MultipleFilesGeneric<T> where T : class
{
public T? Data { get; set; }
public List<IFormFile>? Files { get; set; }
}
Controller code:
[HttpPost]
public async Task Create([FromForm] MultipleFilesGeneric<GamePostDto> gameFormData)
{
// Here gameFormData.Files has correct information and gameFormData.Data is null
}
Entity used in MultipleFilesGeneric<T> class:
public record GamePostDto(
[Required, StringLength(63)] string Title,
[Required, StringLength(511)] string Description,
[Required, Range(1, 128)] int MinPlayers,
[Required, Range(1, 128)] int MaxPlayers,
[Required, StringLength(511)] string Rules,
[Required, Range(1, 5)] int Difficulty
);
When I tried to change Data property type in MultipleFilesGeneric<T> class to string the JSON text is sent to server correctly but I would like to keep ModelState validation (and because passing data as string ignores validation it is not ideal for my usage).
Any ideas why "data" becomes null? Is there another approach to pass both files and additional data as JSON to be parsed on server side? Is it better to use 2 requests (POST and PUT/PATCH) instead of trying to upload files and create entities in a single POST request?

Related

How to bind multiple file uploads with model in web api?

I can't see what's missing here.
I have a model that looks like this:
public class ModelDto
{
public string X { get; set; }
// ...
public IList<IFormFile> Attachments { get; set; }
}
It is used for a POST endpoint in Web Api, like this:
[HttpPost()]
public async Task<ActionResult<ResponseModel>> PostEndpoint([FromForm] ModelDto modelDto)
{
// ...
}
When I POST with Postman, with Form-Data, and fields, including Attachment[0] as a file, I receive a model, complete with all other fields, but without any file. But I do receive the file in the request. If I look at this.Request.Form.Files, it's there, but it was not loaded as part of the model.
So, obviously, I can manually re-attach the list of files to the appropriate part of the model. But why doesn't the binding work?
Thanks
Well, I finally got it, so the problem was on the Postman side of how I wrote the request.
For arrays of text keys, the [] notation is used. It must not be used for arrays of files.
So instead of sending Attachments[0] or Attachments[], I just had to send Attachments as the key to each file. Then the binding works fine.

.NET Core WebApi Action is executed even with missing properties in the request body

My .NET Core 3.1 WebApi controller PUT action is found and execute even if it should not. I have request data object with 2 properties in [FromBody] parameter. But if I call this route with absolutely different properties in the request Body, or no properties at all - it stil seems to be OK and Action is executed, just properties have default values for its types. I expected 400 Bad Request.
public class UpdateLogRequestData
{
[Required]
public int Records { get; set; }
[Required]
public int LogStatusId { get; set; }
}
Controller Action:
[HttpPut]
[Route("{logId:int}")]
public IActionResult UpdateLog(
[FromRoute] int logId,
[FromBody] UpdateLogRequestData requestData)
{
if (!this.ModelState.IsValid)
{
return this.BadRequest(this.ModelState);
}
...
}
I tried to add [Required] attributes and ModelState validation later when I noticed the Action is executed even by "bad request" - ie request with incorrectly named properties. Properties in UpdateLogRequestData just have 0 as their values (int default value).
It is dangerous behavior since I update records in the DB. And now, if someone sends the request without Records and LogStatusId properties, database will be updated with zeroes.
Why controller doesn't check it? It's the first time I see something like this. Why no Bad Request happens in that case?
So after deep digging in the Microsoft documentation I have found that validation in Web API has been changed since version .NET Core 3. For those with the same problem here is how ti works.
Web API controllers don't have to check ModelState.IsValid if they
have the [ApiController] attribute. In that case, an automatic HTTP
400 response containing error details is returned when model state is
invalid. For more information, see Automatic HTTP 400 responses.
[Required] validation on the server. On the server, a required value is
considered missing if the property is null. A non-nullable field is
always valid, and the [Required] attribute's error message is never
displayed.
However, model binding for a non-nullable property may fail, resulting
in an error message such as The value '' is invalid. To specify a
custom error message for server-side validation of non-nullable types,
you have the following options:
Make the field nullable (for example, decimal? instead of decimal).
Nullable value types are treated like standard nullable types.
OR
Specify the default error message to be used by model binding, as
shown in the following example:
services.AddRazorPages()
.AddMvcOptions(options =>
{
options.MaxModelValidationErrors = 50;
options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
_ => "The field is required.");
});
services.AddSingleton<IValidationAttributeAdapterProvider,
CustomValidationAttributeAdapterProvider>();
See the line
options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
_ => "The field is required.");
Make those value types nullable so that they do not default to the non-null default values when omitted in the request body.
public class UpdateLogRequestData {
[Required]
public int? Records { get; set; }
[Required]
public int? LogStatusId { get; set; }
}

MediaFormatter or ModelBinder for web api PUT method

I have a PUT method in web api which accepts a JSON data and a route data as follows.
[Route("api/v1/Orders/{orderId}/active")]
public HttpResponseMessage Put(Guid? orderId,List<ActiveRequest> activeRequests)
{
}
public class ActiveRequest
{
public int Id { get; set; }
public bool IsActive { get; set; }
}
Now is it possible to simplify the method signature as:
[Route("api/v1/Orders/{orderId}/active")]
public HttpResponseMessage Put(ActiveRequestModel model)
{
}
public class ActiveRequestModel
{
public Guid OrderId { get; set; }
public List<ActiveRequest> ActiveRequests {get; set;}
}
I tried writing a custom ModelBinder by implementing the System.Web.Http.ModelBinding.IModelBinder interface but could'nt find a way to read the JSON data that is coming inside the Request object.
I doubt that is there a way by which I can bind my model with data coming from three different places i.e. from route data, json & form.
You cannot simplify the parameter as described.
Unlike MVC model binding, beacuse of how the Web API formatter works, in Web API you only can have a single parameter that is deserialized from the payload, and a number of simple type parameters coming from route parameters or url query string. The reason is that the creation of the parameter coming from the payload is done in a single pass deserialization of the payload.
So, for your example you need the two parameters in your original version, i.e.:
public HttpResponseMessage Put(Guid? orderId, List<ActiveRequest> activeRequests)
If you want to use the ActiveRequestModel you need to include a payload which has exactly the same structure, so you should include the orderId in the payload, because it will not be recovered from the url (even if the name matches).
Please, read this article which explains how parameter binding works in Web API:
Parameter Binding in ASP.NET Web API
If you read it thoroughly you'll see that you can create and register your own model binder to make it work the same way that an MVC controller, but I think it's not worth the effort (so I include it only in this last paragraph), and it's not the standard way of working.

Proper way to send json object to HttpGet endpoint in WebAPI 2

I am developing web api as an facade which will encapsulated request to underlying systems.
So, lets assume I have cars endpoint:
api/v1/cars
Now I want my api to get parameters which will determine calls to underlying systems.
Like:
{
provider: 'service_1'.
access_token: 'token_2',
info: 'some_info'
},
{
provider: 'service_2'.
access_token: 'token_2',
info: 'some_info'
}
Besides that api will take standard parameters like startdate, enddate, offset and others.
public async Task<Result<Cars>> Get([FromUri] RequestParams requestParams);
public class RequestParams
{
public RequestParams()
{
Limit = 50;
Offset = 0;
StartDate = DateTime.Now;
EndDate = DateTime.Now;
}
public string UserId { get; set; }
public int Limit { get; set; }
public int Offset { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
}
It's easy to map standard params from uri, but I do know how to properly pass json collection. Any ideas?
By definition, a GET request doesn't have payload (i.e. data in the body). So, the only way to pass data to a GET request is in the url, i.e. using route data or query string parameters.
If you need to pass a JSON object you need to use a different request method, usualy POST. This includes passing collections.
If you use POST, Web API will automatically load the parameter object with the JSON posted object. If the paramerter is a collection (for example a List<T>) it will also be correctly populated.
There is only one thing that you must take into account: the method can only have one parameter loaded from the body. I.e. you cannot receive several parameters in a Web API action from the body. (You can, however, have data coming from the URL and data coming from the body).
So, you need to change your request and your method to use POST or any other method with payload. Besides, you must choose one of thesse two options:
create a class that includes all the standard parameters, and the collection, and use it as parameter, and post al lthe data in a single object, with the same structure.
pass the standard parameters in the query string, or using route data, and the collection as JSON. In this case, yourmethod must have several parameters: onw for the collection posted as JSON, and one for each other parameters postes in the query string or route data
Posting a collection in the querystring, as proposed in kapsi's answer, is not possible, unless you make some kind of serialization of the parameter on the client side and deserialization when receiving it on the server side. That's overkill, just use POST or any other method with body, as explained above.
If you for example use jQuery, you can use the ajax method to address this:
$.ajax({
url: "./",
type: "GET",
data: {
UserId: 1,
Limit: 2,
Offset: 2,
StartDate: "02/15/2015",
EndDate: "05/15/2015"
}
});
jQuery takes action and following GET is made:
?UserId=1&Limit=2&Offset=2&StartDate=02%2F15%2F2015&EndDate=05%2F15%2F2015&_=1423137376902

ViewModel type architecture from Controller to View

I have a fairly complex class of Policies, of which I display a checkbox list of them, the user checks which one they want, and returns back to the server via ajax. The class is fairly complex:
public class Policy {
public int PolicyId { get; set; }
public string PolicyName { get; set; }
... another 15 properties ...
}
To display the list of checkboxes I really only need the Id and Name, so I've created a lightweight class PolicyViewModel that is simply:
public class PolicyViewModel {
public int PolicyId { get; set; }
public string PolicyName { get; set; }
}
So I then pass a List to the View and get a List back containing the selected Policies.
Another developer on my team said that he doesn't necessarily want to translate from the ViewModel to the Policy class on the Ajax call to save the selected policies, but I'm resistant to send a List of policies due to how heavy they are to send to the view, retrieving all the properties, etc.
EDIT: For clarification, on the Ajax save method, to persist to the DB, the call needs a list of the full Policy class.
What is the best way to display this list and get back the values? Is there a better way than I am proposing?
Usually, you wouldn't need a separate model when serializing to json. Simply pluck out what you need from the domain object into an anonymous object.
return policies.Select(x => new { PolicyId = x.PolicyId, Name = x.PolicyName});
on the return trip, you shouldn't have to send anything more than the Ids of the policies that the user selected. Those can be easily mapped back to your policy objects.
public Whatever PostPolicyChoices(IEnumerable<int> ids)
{
var checked = _context.Policies.Where(x => returnIds.Contains(x.PolicyId));
// snip
boom. done.
I will recommend you not to work with Domain objects in your mvc application . You must work just with ViewModels, I think this is best practice for mvc projects. Take a look at Automapper and use it in your project, this will simplify your work, so this should look something like this :
in your [HttpGet] method you will have :
var model =Mapper.Map<IList<Policy>,IList<VmSysPolicy>>(yourlist)
And in your [HttpPost] method you will have :
var domainList=Mapper.Map<IList<VmSysPolicy>,IList<Policy>>(modelList);
And in your mapping configuration you will do :
Mapper.CreateMap<Policy,PolicyVmSysPolicy,>()
.ForMemeber()//Your mapping here
and
Mapper.CreateMap<VmSysPolicy,Policy>()
.ForMemeber//mapping here

Resources