Search code examples
asp.net-coreblazorblazor-server-sideblazor-webassemblymudblazor

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

Solution

  • 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