skip to Main Content

Say I have a ViewModel like this coming from a form submit in ASP.NET MVC:

public class EditProfileViewModel
{
    public string DisplayName { get; set; }
    public IFormFile Photo { get; set; }
}

I can receive this easily enough inside a controller:

public async Task<IActionResult> OnPost([FromForm] EditProfileViewModel edits)
{
    // edits contains form data, great
}

But now I would like to forward this to a backend API, also using ASP.NET. Including the file upload. Easiest way to do that seems to be creating a new form POST with HttpClient, using MultipartFormDataContent. But as far as I can tell, while the conversion from form content to model classes happens transparently when receiving requests, creating MultipartFormDataContent requires hard-coding the key-value pairs.

I know it is possible to do something like this:

var form = new MultipartFormDataContent()
form.Add(new StringContent(edits.DisplayName), "displayName")
var content = new StreamContent(edits.Photo.OpenReadStream());
content.Headers.ContentType = MediaTypeHeaderValue.Parse(file.ContentType)
form.Add(content, "photo") // Is this the right capitalization ?? See how error prone this is?

var result = await client.PostAsync("some-api", form);

But this is verbose and error prone with duplicate declarations. Also, to use the same model class to deserialize on the receiving end requires knowledge of the conversion magic.

Is there a better way to convert model classes back into MultipartFormDataContent? Or, in case this is an XY problem, a better way to forward this form model to a backend ASP.NET API altogether?

2

Answers


  1. requires knowledge of the conversion magic

    That’s the point – MultipartFormDataContent can be used to post a form to any web application, and the code doesn’t know the underlying parsing magic because it needs to be told how the receiving end expects its format.

    One web framework can be case-sensitive, while the other requires lowercase or exactly matching case. Or for arrays, one can support repetition of field names (name="foo", name="foo", …) the other may require a [] suffix (name="foo[]", name="foo[]", …) and yet another may require indices (name="foo[0]", name="foo[1]", …).

    Let alone for multiple levels of properties: name="foo.bar", name="foo_bar", …

    So you’ll have to write your own property-to-fieldname-mapping code, knowing what framework you POST to.

    Login or Signup to reply.
  2. I solved ‘hard-coding the key-value pairs’ with some extension methods and reflection :

        public static class MultiContentExtensionMethods
        {
            public static void AddDto<TDto>(this MultipartFormDataContent multiContent, TDto dto)
            {
                List<PropertyInfo> propertyInfos = dto.GetType().GetProperties().ToList();
    
                foreach(var propertyInfo in propertyInfos)
                {
                    if (propertyInfo.PropertyType != typeof(IFormFile) &&
                       !propertyInfo.PropertyType.Name.Contains("List")) 
                    {
                        if(propertyInfo.GetValue(dto) is not null)
                            multiContent.Add(new StringContent(propertyInfo.GetValue(dto).ToString()), propertyInfo.Name);
                    }
                    else if(propertyInfo.PropertyType != typeof(IFormFile) &&
                        propertyInfo.PropertyType.Name.Contains("List"))
                    {
                        var list = (IList)propertyInfo.GetValue(dto, null);
    
                        if(list is not null)
                        foreach (var item in list) 
                        {
                            multiContent.Add(new StringContent(item.ToString()), propertyInfo.Name);
                        }
                    }
                }
            }
    
    
           public static void AddFile(this MultipartFormDataContent multiContent, IFormFile file, string name)
           {
                byte[] data;
                using (var br = new BinaryReader(file.OpenReadStream()))
                {
                    data = br.ReadBytes((int)file.OpenReadStream().Length);
                }
    
                ByteArrayContent bytes = new(data);
    
                multiContent.Add(bytes, name, file.FileName);
            }
        }
    

    Usage:

    In this example , there is no diffrence between ViewModel or Dto , So you can use ViewModel

    public class EditProfileDto
    {
        public string DisplayName { get; set; }
        public IFormFile Photo { get; set; }
    }
    
    EditProfileDto dto = new()
    {
        DisplayName = edits.DisplayName
    };
    
    MultipartFormDataContent form = new();
    form.AddDto(dto);
    form.AddFile(edits.Photo, nameof(dto.Photo));
    
    var result = await client.PostAsync("some-api", form);
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search