How to bind multiple file uploads with model in web api? - .net-core

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.

Related

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

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

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

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?

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.

Uploading a file to a webservice without saving to server, database, or locally

I am creating a web service that will let the user upload a file and then I will make some changes to the file and return the file. But I want to get that file content without saving it to the server, database, or locally. Instead I want to save the content into a stream or something similar so that I can then run modifications on its content and return the new file.
I have tried doing the following but can only figure out how to get it to upload and save locally. Currently the file information is collected from the form data and the file headers.
namespace FileConverter.Controllers
{
[RoutePrefix("api/test")]
public class FileUploadController : ApiController
{
private static readonly string ServerUploadFolder = "C:\\Temp"; //Path.GetTempPath();
[Route("files")]
[HttpPost]
[ValidateMimeMultipartContentFilter]
public async Task<FileResult> UploadSingleFile()
{
var streamProvider = new MultipartFormDataStreamProvider(ServerUploadFolder);
await Request.Content.ReadAsMultipartAsync(streamProvider);
return new FileResult
{
FileNames = streamProvider.FileData.Select(entry => entry.LocalFileName),
Names = streamProvider.FileData.Select(entry => entry.Headers.ContentDisposition.FileName),
ContentTypes = streamProvider.FileData.Select(entry => entry.Headers.ContentType.MediaType),
};
}
}
}
I use a ValidateMimeMultipartContentFilter attribute to check if the correct MIME type is sent.
namespace FileConverter.Controllers
{
public class ValidateMimeMultipartContentFilter : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (!actionContext.Request.Content.IsMimeMultipartContent())
{
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}
}
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
}
}
}
I then have a model set up to store some data.
namespace FileConverter.Controllers
{
public class FileResult
{
public IEnumerable<string> FileNames { get; set; }
public IEnumerable<string> ContentTypes { get; set; }
public IEnumerable<string> Names { get; set; }
}
}
Is there any way I can get the content of the file in a Stream/byte[] without saving the file locally and then reading its content? If so how would I go about doing that?
By the time your form action is invoked, ASP.NET has already done a lot of magic to interpret the file upload, so you can't really do much within the action itself. However, you can capture the byte streams in multipart forms as they come across the wire using an HttpModule or HttpHandler.
This example seems like it would be a good starting point.
I don't know whether an HttpRequest can begin returning data until after the payload has been received, so this is likely going to require some crazy cross-request magic if you literally want to "stream" the filtering process. On the other hand, if you were intending to keep the whole file in memory while you process it and then send it back afterward, I'd argue that you're better off allowing MVC to save the file as a temp file: you'd potentially get worse performance by keeping the entire uploaded file in memory while it's being uploaded.

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