Bind from custom headers and request body

5.1k views Asked by At

I am having a problem binding a model from both custom headers and the request body at the same time.

This is my model:

public class TestModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public HeaderModel HeaderModel { get; set; }
}
public class HeaderModel
{
    public int Version { get; set; }
    public string Test { get; set; }
}

I have a simple test page that posts the following:

$(function() {
    var go = function() {
        $.ajax({
            type: 'POST',
            url: '/api/values',
            data: { id: 1, name: 'theValue2' },
            headers: { HeaderModel: JSON.stringify({version: 1, test:"roar"}) }
        }).always();
    };
    $('input').click(go);
});

I have created the following header value provider:

public class HeaderValueProvider<T> : IValueProvider where T : class
{
    private readonly HttpRequestHeaders _headers;

    public HeaderValueProvider(HttpRequestHeaders headers)
    {
        _headers = headers;
    }

    public bool ContainsPrefix(string prefix)
    {
        var test = typeof (T).Name == prefix;
        return test;
    }

    public ValueProviderResult GetValue(string key)
    {
        if (typeof (T).Name == key)
        {
            IEnumerable<string> headerStrings;
            _headers.TryGetValues(key, out headerStrings);
            var strings = headerStrings.ToArray();
            if (headerStrings != null && strings.Any())
            {
                var value = strings.First();
                var obj = JsonConvert.DeserializeObject<T>(value,
                    new JsonSerializerSettings {NullValueHandling = NullValueHandling.Ignore});
                return new ValueProviderResult(obj, value, CultureInfo.InvariantCulture);
            }
        }
        return null;
    }
}

public class HeaderValueProviderFactory<T> : ValueProviderFactory where T : class
{
    public override IValueProvider GetValueProvider(HttpActionContext actionContext)
    {
        var headers = actionContext.ControllerContext.Request.Headers;
        return new HeaderValueProvider<T>(headers);
    }
}

And it has been registered in the config:

config.Services.Add(typeof(ValueProviderFactory), new HeaderValueProviderFactory<HeaderModel>());

If I use my controller like the following I get my model bound, but not my header. It uses a media formatter:

public IHttpActionResult Post(TestModel model)
{
    return Ok(model);
}

If I include the ModelBinder attribute I get the HeaderModel bound, but not the rest of the model from the request body:

public IHttpActionResult Post([ModelBinder]TestModel model)
{
    return Ok(model);
}

What is the cleanest way to get both to work?

2

There are 2 answers

0
TheGwa On BEST ANSWER

This was frustratingly easy to solve. As correctly stated by Badri, you cannot use the request body and headers for the same binding. You can however have two parameters and have them bound separately.

I have left the HeaderModel as its own separate class and added a ModelBinder attribute to it.

public class TestModel
{
    public int Id { get; set; }
    public string Name { get; set; }
}

[ModelBinder]
public class HeaderModel
{
    public int Version { get; set; }
    public string Test { get; set; }
}

I now have two parameters in my Post.

public IHttpActionResult Post(HeaderModel headerModel, TestModel model)
{
    return Ok();
}

I would not call this a perfect solution, but it works well.

1
Badrinarayanan Lakshmiraghavan On

Web API binds a complex type like TestModel from body by default using media formatters. By specifying ModelBinder, you ask Web API to use model binding, which works on URI, query string but since you have a value provider, it is binding from the header as well. But basically, you will not be able to get TestModel model created from both body and other places like header, query string, etc, out of box, unless you write a custom parameter binder. Check this out for an example.