Search code examples
c#file-uploadasp.net-core.net-core

Upload files and JSON in ASP.NET Core Web API


How can I upload a list of files (images) and json data to ASP.NET Core Web API controller using multipart upload?

I can successfully receive a list of files, uploaded with multipart/form-data content type like that:

public async Task<IActionResult> Upload(IList<IFormFile> files)

And of course I can successfully receive HTTP request body formatted to my object using default JSON formatter like that:

public void Post([FromBody]SomeObject value)

But how can I combine these two in a single controller action? How can I upload both images and JSON data and have them bind to my objects?


Solution

  • 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());
    });