David
David

Reputation: 218827

Automatically Binding Another Parameter in WebAPI

In MVC this sort of thing is pretty trivial. Let's say I have an MVC action signature:

public ActionResult SomeAction(InjectedObject a, ConstructedObject b)

And let's say that the request from the client contains the ConstructedObject and I want to build the InjectedObject in the framework pipeline automatically. (In this example, InjectedObject is on lots of actions, maybe even all of them.) I could just create an InjectedObjectModelBinder : IModelBinder and register an instance of that binder when the application starts.

That binder would simply construct an instance of InjectedObject however I need to. (From request data, from some other source, a combination of sources, etc.) This worked really well for cross-cutting concerns.


However, is there a way to do this in WebAPI? There's a new IModelBinder, but it seems like its use assumes only a single model on the input. And my Googling so far has also pointed to that assumption. Is it even possible in WebAPI to inject something into the pipeline like this as a cross-cutting concern while still having the constructed model from the post body?

The specific use case here is that I'd like to build a custom authorization-related object, in this case from request headers. I could build it in the action, but every action would need to. I could add an extension method onto the controller, but that would hurt unit testing. The preferred method would be to simply inject it into the pipeline so that I can inject a mock when unit testing the controller actions.

Does WebAPI support this? Or perhaps is there another preferred approach?

Upvotes: 5

Views: 667

Answers (1)

petelids
petelids

Reputation: 12815

Yes, you can use an IModelBinder in the way you suggest, just ensure that the model binder handles (only) your InjectedObject. For example, the following simple model binder reads the header "your_key":

public class InjectedObjectModelBinder : IModelBinder
{
    public bool BindModel(System.Web.Http.Controllers.HttpActionContext actionContext, ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelType != typeof(InjectedObject))
        {
            return false;
        }

        IEnumerable<string> values;
        string keyValue = "";

        if (actionContext.Request.Headers.TryGetValues("your_key", out values))
        {
            keyValue = values.First();
        }

        bindingContext.Model = new InjectedObject() { Id = 789, Name = keyValue };

        return true;
    }
}

You then need to wire up the binder to the InjectedObject class. There are several ways of doing this: Firstly, you could do it on each action method:

public void Post([ModelBinder(typeof(InjectedObjectModelBinder))]InjectedObject a, ConstructedObject b)

but this feels wrong given that you want to use this in many places. Secondly you could do it by decorating the InjectedObject class with the ModelBinder attribute:

[ModelBinder(typeof(InjectedObjectModelBinder))]
public class InjectedObject
{....

Finally, you could add it to the HttpConfiguration in the Register method of the WebApiConfig class:

var provider = new SimpleModelBinderProvider(typeof(InjectedObject), new InjectedObjectModelBinder());
config.Services.Insert(typeof(ModelBinderProvider), 0, provider);

Given some simple ConstructedObject and InjectedObject implementations with the binder from above:

[ModelBinder(typeof(InjectedObjectModelBinder))]
public class InjectedObject
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class ConstructedObject
{
    public int A { get; set; }
    public int B { get; set; }
    public string C { get; set; }
}

and an action method like this:

public void Post(InjectedObject a, ConstructedObject b)
{
    //a will be populated in our model binder.
}

and finally a request from Fiddler that looks like this:

POST http://localhost:64577/api/values HTTP/1.1
Host: localhost:64577
Accept: */*
Content-Type: application/json
Connection: keep-alive
Content-Length: 51
your_key: This came from the header

{"a":1,"b":2,"c":"This was 'normal' model binding"}

The binding behaves as you would expect / hope:

Watch window with correctly bound values

A good article on model binding in Web API can be found here.

Upvotes: 5

Related Questions