Recently, for one of my pet projects I’ve create a web API that allows to upload small files with extra metadata. During the testing file upload worked as expected, but metadata object represented by Dictionary<,> type did not go though, the dictionary always was empty. I guess, it is worth mentioning that for testing I was using Swagger .

Problem statement

So, lets quickly reproduce the problem. To upload data to the server via HTTP we need to use multipart request . The ASP.NET documentation suggests two ways of uploading files buffering and streaming. In this article we are going to focus on the buffering approach, because only in this scenario this problem arise.

Here is our data model and controller

public class UploadRequest
{
    public IFormFile File { get; set; }
    public IDictionary<string, string> Metadata { get; set; }
}

[HttpPost("multipart")]
public async Task<IActionResult> MultipartAsync([FromForm] UploadRequest request, CancellationToken cancellationToken)
{
    // ...trimmed...
}

The swagger UI for this request looks like this
Swagger

As you can see Swagger represents the Metadata property as a JSON object.

Now, if we click execute button on Swagger UI and set a breakpoint in Visual Studio we see that Metadata property is empty.
Metadata Empty

Solutions

After reading model binding documentation and trying many different variations of passing dictionary to the API, it was clear that dictionary binding does not work only in this particular scenario.

Option 1

For the full picture, I guess it is worth to mention that probably the easiest and obvious solution would be just replace Dictionary<string, string> with a string type and parse it later in the code something like this:

public class UploadRequest
{
    public IFormFile File { get; set; }
    public string Metadata { get; set; }
    
    internal IDictionary<string, string> ParseMetadata() 
        => JsonSerializer.Deserialize<Dictionary<string, string>>(Metadata ?? string.Empty);
}

But we do not seek easy ways 😄.

Option 2

Likely, ASP.NET allows us to create our own model bindings . So, lets try to implement one. But before jumping into the code, it worth to look at how an original dictionary binder is implemented, and how we can benefit from it.

APS.NET using Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DictionaryModelBinderProvider provider to create Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DictionaryModelBinder<TKey, TValue> model binder which is actually do whole binding magic.

Now, when we familiar with original implementation we ready to implement our own logic. We will try to reuse original implementation as much as possible, because it works well, except our scenario.

If we examine DictionaryModelBinderProvider provider, it is clear that there is no “legal” ways to inherit or extend it to inject our binder. But likely we can simply instantiate it inside of our provider and reuse it’s logic that analyses context and if current model type is Dictionary<,>, it creates binder, if not it just returns null. Our version will look something like this:

public class EnhancedDictionaryBinderProvider : IModelBinderProvider
{
    private readonly DictionaryModelBinderProvider _provider;

    public EnhancedDictionaryBinderProvider()
    {
        _provider = new DictionaryModelBinderProvider();
    }

    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        var binder = _provider.GetBinder(context);
        if (binder is not null)
        {
            var binderType = typeof(EnhancedDictionaryModelBinder<,>).MakeGenericType(binder.GetType().GenericTypeArguments);
            return (IModelBinder)Activator.CreateInstance(binderType, binder);
        }

        return null;
    }
}

Note: when we create our binder Activator.CreateInstance(binderType, binder) we pass original one into our binder constructor.

Now lets create our binder, and lets try to reuse original binder as much as possible.

When BindModelAsync method is called we redirect call to the original binder. Then check bindingContext.Result.Model property. If model is not empty, then job is done. But, if it is empty, then we run our logic. As we know from the testing scenario, our dictionary represented by JSON object. Everything what we need to do, is just to parse it.

public class EnhancedDictionaryModelBinder<TKey, TValue> : IModelBinder
{
    private readonly DictionaryModelBinder<TKey, TValue> _binder;

    public EnhancedDictionaryModelBinder(DictionaryModelBinder<TKey, TValue> binder)
    {
        _binder = binder;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        await _binder.BindModelAsync(bindingContext);

        var model = (IDictionary<TKey, TValue>)bindingContext.Result.Model;
        if (model != null && model.Count > 0)
        {
            return;
        }

        var values = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        foreach (var val in values)
        {
            var dictionary = JsonSerializer.Deserialize<Dictionary<TKey, TValue>>(val);
            foreach (var kv in dictionary)
            {
                if (!model.ContainsKey(kv.Key))
                {
                    model[kv.Key] = kv.Value;
                }
            }
        }
    }
}

One more thing left, we need to register our binder.

// .NET 6 style
builder.Services.AddControllers(options =>
{
    options.ModelBinderProviders.Insert(0, new EnhancedDictionaryBinderProvider());
});

Now lets run our solution and check if problem is solved.

Metadata not empty

Source Code

The sample application for this article you can find here .