jason
jason

Reputation: 3615

Blazor WASM MudBlazor upload file to server

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

Answers (1)

Ruikai Feng
Ruikai Feng

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

Result: enter image description here

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:

enter image description here

Upvotes: 1

Related Questions