Reputation: 3615
I have a Blazor WASM core hosted application, which has a Blazor client app and a .Net server project. I am trying to create a file upload when the user saves an object. The client service will send the request to the server controller. Here is what it looks like:
Models:
public class MyObjectDTO : BaseModel<MyObjectDTO>
{
public int Id { get; set; }
[Required] public string MyObjectName { get; set; } = string.Empty;
public List<ItemDocumentDTO>? MyFiles { get; set; }
}
public class ItemDocumentDTO : BaseModel<ItemDocumentDTO>
{
public int Id { get; set; }
[Required(ErrorMessage = "Document Name is required")]
public string DocumentName { get; set; } = string.Empty;
public IBrowserFile? File { get; set; }
}
Client Service:
public async Task<ServiceResponse<int>> CreateNewObject(MyObjectDTO newObject)
{
using var client = await _httpClientFactory.CreateClientAsync("default");
var multipartContent = new MultipartFormDataContent();
if (newObject.MyFiles != null)
{
foreach(var item in newObject.MyFiles)
{
if(item.File != null)
{
var fileContent = new StreamContent(item.File.OpenReadStream());
multipartContent.Add(fileContent, "files", item.File.Name);
}
}
}
var result = await client.PostAsJsonAsync($"api/MyObject/create-item", new {
createObject = newObject,
filesContent = multipartContent
});
if (result != null && result.Content != null)
{
var response = await result.Content.ReadFromJsonAsync<ServiceResponse<int>>();
if (response != null)
{
return response;
}
}
return new ServiceResponse<int>
{
Success = false,
Message = "Failed to create end item.",
Data = 0
};
}
Server Controller:
[HttpPost("create-item")]
public async Task<ActionResult<ServiceResponse<int>>> CreateItem([FromForm] MyObjectDTO createObject)
{
//break point does not get hit here. fails before hitting the controller.
return Ok(await this._newObjectService.CreateNewItem(createObject));
}
When I submit, before it hits the Server controller, I get an error in the browser console saying:
{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "traceId": "00-987c028fd41bb0e6605243e6b5de5d4c-c7f26f97d41a2b54-00", "errors": { "MyObjectName": [ "The MyObjectName field is required." ] } }
Even though the value is not null and the form validates before it is sent. How should I be sending the object, and files, to my server controller?
Update
I updated my code as suggested:
public class YourRequestModelDTO
{
public ItemDTO createItem { get; set; }
public List<IFormFile>? files { get; set; }
public string? Key { get; set; }
}
public class ItemDTO : BaseModel<ItemDTO>{
public int Id { get; set; }
[Required] public string ItemName { get; set; } = string.Empty;
}
public async Task<ServiceResponse<int>> CreateNewItem(ItemDTO newItem)
{
using var client = await _httpClientFactory.CreateClientAsync("default");
using (MultipartFormDataContent content = new MultipartFormDataContent())
{
var file = newItem.ItemDocuments.First().File;
var fileContent = new StreamContent(file.OpenReadStream(1024 * 15));
fileContent.Headers.ContentType = new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "files",
fileName: file.Name);
content.Add(new StringContent(newItem.ItemName), "createItem.ItemName");
content.Add(new StringContent(newItem.EndDescription), "createItem.EndDescription");
try
{
var result = await client.PostAsync("api/MyItem/create-item", content);
if (result != null && result.Content != null)
{
var response = await result.Content.ReadFromJsonAsync<ServiceResponse<int>>();
if (response != null)
{
return response;
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
[Authorize]
[HttpPost("create-item")]
public async Task<ActionResult<ServiceResponse<int>>> CreateItem([FromForm] YourRequestModelDTO createItem)
{
return Ok(await this._itemService.CreateNewItem(createItem.createItem));
}
but once it tries to reach out to the controller, I get an error in the browser console saying "One or more validation errors occurred." and "CreateItem.createItem: the createItem field is required."
Solution What I ended up doing we, in my client service:
using (MultipartFormDataContent content = new MultipartFormDataContent())
{
var file = newItem.ItemDocuments.First().File;
var fileContent = new StreamContent(file.OpenReadStream(1024 * 15));
fileContent.Headers.ContentType = new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "files", // The name should match the parameter name in the API controller
fileName: file.Name);
var test = JsonConvert.SerializeObject(newItem);
var itemContent = new StringContent(test, Encoding.UTF8, "application/json");
content.Add(itemContent, "NewItem");
... }
Then in my controller:
[Authorize]
[HttpPost("create-item")]
public async Task<ActionResult<ServiceResponse<int>>> CreateItem(){
var streamContent = new StreamContent(HttpContext.Request.Body);
var content = new MultipartFormDataContent();
var serializedObject = HttpContext.Request.Form.FirstOrDefault().Value;
//deserialize from here.
}
Upvotes: 0
Views: 986
Reputation: 11826
We would receive the files from Request.Form instead of Request.Body
with IFormFile/IFormFileCollection on webapi side, we don't post the content as json ,if you want to send other entity with your files ,you could add key-value pairs in StringContent,a minimal example:
codes in Razor Component:
using var content = new MultipartFormDataContent();
// here e.GetMultipleFiles() would retrun IReadOnlyList<IBrowserFile>
foreach(var file in e.GetMultipleFiles(5))
{
var fileContent =
new StreamContent(file.OpenReadStream(1024 * 15));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "files",
fileName: file.Name);
}
content.Add(new StringContent("val1"), "key");
content.Add(new StringContent("val2"), "MyET.Prop1");
content.Add(new StringContent("val3"), "MyET.Prop2");
var response =
await httpClient.PostAsync("api/fileupload",
content);
Controller/Models in webapi:
[HttpPost]
public void Post([FromForm] UploadModel uploadModel)
{
}
....
public class UploadModel
{
public List<IFormFile>? files {get;set;}
public string? Key { get; set; }
public MyEntity? MyET { get; set; }
}
public class MyEntity
{
public string? Prop1 { get; set; }
public string? Prop2 { get; set; }
}
doucments related:Blazor FileUpload , Model binding
Update: if you want to serialize a complex model and send it with your file,you may create a model binder,a minimal example:
Razor component:
using var content = new MultipartFormDataContent();
foreach(var file in e.GetMultipleFiles(5))
{
var fileContent =
new StreamContent(file.OpenReadStream(1024 * 15));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "files",
fileName: file.Name);
}
content.Add(new StringContent("val1"), "key");
var MyET = new { Prop1 = "val2", Prop2 = "val3" };
var jsonstr = System.Text.Json.JsonSerializer.Serialize(MyET);
content.Add(new StringContent(jsonstr), "MyET");
var response =
await httpClient.PostAsync("api/fileupload",
content);
the model binder:
public class JsonModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult != ValueProviderResult.None)
{
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
// convert the input value
var valueAsString = valueProviderResult.FirstValue;
var result = System.Text.Json.JsonSerializer.Deserialize(valueAsString, bindingContext.ModelType);
if (result != null)
{
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}
return Task.CompletedTask;
}
}
apply the binder:
public class UploadModel
{
public List<IFormFile>? files {get;set;}
public string? Key { get; set; }
[ModelBinder(BinderType = typeof(JsonModelBinder))]
public MyEntity? MyET { get; set; }
}
Result:
Upvotes: 1