Upload Files and JSON in ASP.NET Core Web API

Upload files and JSON in ASP.NET Core Web API

Apparently there is no built in way to do what I want. So I ended up writing my own ModelBinder to handle this situation. I didn't find any official documentation on custom model binding but I used this post as a reference.

Custom ModelBinder will search for properties decorated with FromJson attribute and deserialize string that came from multipart request to JSON. I wrap my model inside another class (wrapper) that has model and IFormFile properties.

IJsonAttribute.cs:

public interface IJsonAttribute
{
object TryConvert(string modelValue, Type targertType, out bool success);
}

FromJsonAttribute.cs:

using Newtonsoft.Json;
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class FromJsonAttribute : Attribute, IJsonAttribute
{
public object TryConvert(string modelValue, Type targetType, out bool success)
{
var value = JsonConvert.DeserializeObject(modelValue, targetType);
success = value != null;
return value;
}
}

JsonModelBinderProvider.cs:

public class JsonModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null) throw new ArgumentNullException(nameof(context));

if (context.Metadata.IsComplexType)
{
var propName = context.Metadata.PropertyName;
var propInfo = context.Metadata.ContainerType?.GetProperty(propName);
if(propName == null || propInfo == null)
return null;
// Look for FromJson attributes
var attribute = propInfo.GetCustomAttributes(typeof(FromJsonAttribute), false).FirstOrDefault();
if (attribute != null)
return new JsonModelBinder(context.Metadata.ModelType, attribute as IJsonAttribute);
}
return null;
}
}

JsonModelBinder.cs:

public class JsonModelBinder : IModelBinder
{
private IJsonAttribute _attribute;
private Type _targetType;

public JsonModelBinder(Type type, IJsonAttribute attribute)
{
if (type == null) throw new ArgumentNullException(nameof(type));
_attribute = attribute as IJsonAttribute;
_targetType = type;
}

public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext));
// Check the value sent in
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult != ValueProviderResult.None)
{
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
// Attempt to convert the input value
var valueAsString = valueProviderResult.FirstValue;
bool success;
var result = _attribute.TryConvert(valueAsString, _targetType, out success);
if (success)
{
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}
return Task.CompletedTask;
}
}

Usage:

public class MyModelWrapper
{
public IList<IFormFile> Files { get; set; }
[FromJson]
public MyModel Model { get; set; } // <-- JSON will be deserialized to this object
}

// Controller action:
public async Task<IActionResult> Upload(MyModelWrapper modelWrapper)
{
}

// Add custom binder provider in Startup.cs ConfigureServices
services.AddMvc(properties =>
{
properties.ModelBinderProviders.Insert(0, new JsonModelBinderProvider());
});

Is it impossible to mix fileupload and raw json in asp core web api?

In case anyone is still wondering about this question, I followed the suggestion i got from @IhorKostrov and created a multipart/form-data request. The solution can be found: using jsonData as a key value to send mixed data in asp: Does not update the database with the values.

Upload file into ASP.NET Core Web API

In dotnet core controller you can use IFormFile Interface to get files,

[HttpPost("upload-file")]
public async Task<IActionResult> UploadFile([FromQuery] IFormFile file){

if(file.Length > 0){
// Do whatever you want with your file here
// e.g.: upload it to somewhere like Azure blob or AWS S3
}

//TODO: Save file description and image URL etc to database.
}

In Flutter you need to send a Multipart POST request to include files with binary content (images, various documents, etc.), in addition to the regular text values.

import 'package:http/http.dart' as http;

Future<String> uploadImage(filename, url) async {
var request = http.MultipartRequest('POST', Uri.parse(url));
request.files.add(
http.MultipartFile.fromBytes(
'file',
File(filename).readAsBytesSync(),
filename: filename.split("/").last
)
);
var res = await request.send();
return res;
}

How do I do file upload using ASP.NET Core 6 minimal api?

Currently out of the box support for binding in Minimal APIs is quite limited. Supported binding sources:

  • Route values
  • Query string
  • Header
  • Body (as JSON)
  • Services provided by dependency injection
  • Custom

NOTE: Binding from forms is not natively supported in .NET 6

You can either leverage custom binding or use special types handling:

app.MapPost("/upload", (HttpRequest request) =>
{
//Do something with the file
var files = request.Form.Files;
return Results.Ok();
})
.Accepts("multipart/form-data")
.Produces(200);

upload file on server webfolder and post json data at the same using asp.net web api

Uploading a file from web form to server requires a different approach specially when you want to upload file with other model data.

Files selected on the client can not be submitted as JSON to the server. So we need to submit the form as a Collection to the server. We also can not use application\json as content-type.

Consider following example. The example is demonstrated with ASP.NET MVC application example.

First thing you need to do is add the File object as part of the model class.

public class ClientDocument
{
public int Id {get;set;}
public int ClientId {get;set;}
public string Title {get;set;}
// Below property is used for getting file from client to the server.
public IFormFile File {get;set;}
}

Then you need to create a form in the View. Assume that the view has Model type set to the above class.

<form id="someForm" method="post">
@Html.HiddenFor(m => m.ClientId)
<table>
<tr>
<td>
@Html.LabelFor(m => m.Title)
</td>
<td>
@Html.TextFor(m => m.Title)
</td>
</tr>
<tr>
<td>
@Html.LabelFor(m => m.File)
</td>
<td>
//Following line of code will render file control, where user can select file from the local machine.
@Html.TextBoxFor(m => m.File, new { Type = "file" })
</td>
</tr>
<tr>
<td colspan="2">
<button type="button" onclick="SubmitForm();">Submit</button>
</td>
</tr>
</table>
</form>

Following is the JavaScript code for submitting the form. SubmitForm function in the JavaScript will convert the form to a FormData object.

FormData is a JavaScript object which represents data as a colleciton of Key/value pairs. Once the FormData object is created, we will post it to the server. As I mentioned above we can not use application\json content-type, we need to use multipart/form-data as encodingtype of the payload.

<script>
function SubmitForm() {
if ($("#someForm").valid()) {
var form = $("#someForm")[0];
var data = new FormData(form);

$.ajax({
type: "POST",
enctype: 'multipart/form-data',
url: "http://www.somedomain.com/api/ClientDocument/Save",
data: data,
processData: false,
contentType: false,
cache: false,
beforeSend: function () {
// show waiting icon.
},
success: function (data) {
//Hiding waiting icon;
//Display the response in the UI.
},
error: function (xhr, status, errorThrown) {
console.log('ERROR : ' + xhr.responseText);
}
});
}
}
</script>

Now at the server end the controller action would look like following.

public IActionResult Save(ClientDocument model)
{
try
{
//Access File Property to save it on server or upload to some storage
var fileObject = model.File;
var tartgetLocation = "D:\\ClientDocuments\\" + fileObject.FileName;

using (var stream = new FileStream(tartgetLocation , FileMode.Create))
{
fileObject.CopyTo(stream);
}

// Write code to save information to database.
}
catch (Exception ex)
{
//Handle Exception
return Json(new {Status= "Failed"});
}

return Json( new {Status= "Success"});
}

I know that this is not the exact answer you might be looking for. But this should give you an idea about how to approach the problem and apply the same technique if applicable in your use case.

consuming asp.net core web api from asp.net core mvc controller for file upload

My suggestion is that:

the webapi isn't in the same project with mvc,You need to set cors in mvc startup:

public void ConfigureServices(IServiceCollection services)

{
......
services.AddCors(options =>
{
options.AddPolicy("any", builder =>
{
builder.WithOrigins("https://localhost:44320/WeatherForecast", "https://localhost:44393/WeatherForecast/PostFile")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

{

app.UseCors("cors");
app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
endpoints.MapHub<ChatHub>("/chatHub");
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}

Codes in controller:

 public IActionResult AddPost(PostViewModel model)

{
TestViewModel testViewModel = new TestViewModel()
{
Description = model.Description,
File = model.File,
};
var url = "https://localhost:44320/WeatherForecast";
HttpClient hc = new HttpClient();
var formContent = new MultipartFormDataContent();
formContent.Add(new StringContent(testViewModel.Description), "Description");
formContent.Add(new StreamContent(testViewModel.File.OpenReadStream()), "File", Path.GetFileName(testViewModel.File.FileName));
var response = hc.PostAsync(url,formContent).Result;
return Ok();
}

Sample Image

Multipart/form-data images upload with JSON asp.net core api

You can use built-in class IFormFile to easily work with file upload. To use it together with JSON you may create custom model binder and combine it together in DTO object:

public class ViewModel
{
[ModelBinder(BinderType = typeof(FormDataJsonBinder))]
public DataModel Data { get;set; }

public List<IFormFile> Images { get; set; }
}

public class FormDataJsonBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if(bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}

string fieldName = bindingContext.FieldName;
var valueProviderResult = bindingContext.ValueProvider.GetValue(fieldName);

if(valueProviderResult == ValueProviderResult.None)
{
return Task.CompletedTask;
}
else
{
bindingContext.ModelState.SetModelValue(fieldName, valueProviderResult);
}

string value = valueProviderResult.FirstValue;
if(string.IsNullOrEmpty(value))
{
return Task.CompletedTask;
}

try
{
object result = JsonConvert.DeserializeObject(value, bindingContext.ModelType);
bindingContext.Result = ModelBindingResult.Success(result);
}
catch(JsonException)
{
bindingContext.Result = ModelBindingResult.Failed();
}

return Task.CompletedTask;
}
}

Then you can work with it in your controller:

[HttpPost]
public IActionResult Create(ViewModel vm)
{
var data = vm.Data;

if (vm.Images != null)
{
foreach(var image in vm.Images)
{
byte[] fileData = null;

// read file to byte array
using (var binaryReader = new BinaryReader(image.OpenReadStream()))
{
fileData = binaryReader.ReadBytes((int)image.Length);
}
}
}
}


Related Topics



Leave a reply



Submit