15

I have a command looking like:

public interface ICommand {
    // Just a marker interface
}

public interface IUserAware {
    Guid UserId { get; set; }
}

public class CreateSomething : ICommand, IUserAware
{
    public string Title { get; set; }

    public Guid UserId { get; set; }
}

The REST request is:

PUT /create HTTP/1.1
UserId: 7da6f9ee-2bfc-70b1-f93c-10c950c8f6b0 // Possible an Auth token and not a userId like here.
Host: localhost:63079
Content-Type: application/json
Cache-Control: no-cache
{
    "title": "This is a test title"
}

I have a API controller action looking:

[HttpPut, Route("create")]
public IHttpActionResult CreateSomething([FromBody]CreateSomething command)
{
    // I would like command.UserId already binded here
}

The Title property on my model is filled out with the body of the request, but I would like to bind the command.UserId property using some values from the request headers (e.g. from a authentication token).

How can I bind the property of IUserAware from a request header value, with e.g. a model-binder, without having to create a binder for the concrete class CreateSomething?

I've tried various combinations of the IModelBinder interface in Web API, but with no real luck.

It also feels redundant to to use:

[HttpPut, Route("create")]
public IHttpActionResult CreateSomething([FromBody]CreateSomething command)
{
    command.UserId = GetUserIdFromTheRequest();
}

Or getting the UserId from a dependency on the controller and set it like the above.

How it is done in ASP.NET MVC

In ASP.NET MVC it is possible to do the following to get it work:

public class UserAwareModelBinder : DefaultModelBinder
{
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, System.Type modelType)
    {
        var baseModel = base.CreateModel(controllerContext, bindingContext, modelType);
        var commandModel = baseModel as IUserAware;
        if (commandModel != null) 
        {
             commandModel.UserId = controllerContext.HttpContext.User; // or get it from the HttpContext headers.
        }

        return baseModel;
    }
}

And wire it up at startup with:

ModelBinders.Binders.DefaultBinder = new UserAwareModelBinder();
janhartmann
  • 14,713
  • 15
  • 82
  • 138

2 Answers2

5
public class CreateSomethingModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        string key = bindingContext.ModelName;
        ValueProviderResult val = bindingContext.ValueProvider.GetValue(key);
        if (val != null)
        {
            string s = val.AttemptedValue as string;
            if (s != null)
            {
                return new CreateSomething(){Title = s; UserId = new Guid(ControllerContext.HttpContext.Request.Headers["userId"]);}
            }
        }
        return null;
    }
}

and add attribute on the type declaration

[ModelBinder(typeof(CreateSomethingModelBinder))]
public class CreateSomething  { ... }
Todd
  • 1,071
  • 8
  • 12
  • Thanks for the suggestion, is possible to create a more generic way of handling this? I would like to avoid manually binding other properties - I only want to bind properties based on the interface - others should be binding as normal. – janhartmann Aug 15 '16 at 10:56
  • ModelBinder is for binding to specific custom objects.For user authentication you should implement web api security read more on [link](http://www.asp.net/web-api/overview/security/authentication-and-authorization-in-aspnet-web-api) and use HttpContext.Current.User or create base controller where you can implement CurrentUser and separate the user from the model binding process. – Todd Aug 15 '16 at 11:18
  • The need is not only for authentication, but a general question on how to bind custom properties in WebAPI. I've updated my question on how to do this in MVC, I just dont know how to do it in WebAPI alone. – janhartmann Aug 15 '16 at 11:20
  • The binding of custom properties in MVC is like you write it: `public IHttpActionResult CreateSomething([FromBody]CreateSomething command)` but you can't expect MVC to guess from where to get the values for your object. In you case you should pass userId in the body with the title. – Todd Aug 15 '16 at 11:33
  • No, it is not. DefaultModelBinder with the overriding of the `BindModel` is MVC namespace thing: System.Web.Mvc. – janhartmann Aug 15 '16 at 11:37
  • Well I think you can use `TypeConverter` for that job – Todd Aug 15 '16 at 12:01
  • Hi Todd, seems like the method is not compiling - there is no method signature with the use of `ConvertFrom` with `Type destinationType`. Also it seem not possible to get the HttpRequest from here. :-) – janhartmann Aug 18 '16 at 06:58
  • Sorry I was taken the params from ConvertTo method, but anyway it doesn't work too(I tested it), so I am deleting it from the answer. Look [here](http://stackoverflow.com/questions/20855185/webapi2-custom-parameter-binding-to-bind-partial-parameters) for implementation with `HttpParameterBinding` or write your own custom [media formatter](http://www.asp.net/web-api/overview/formats-and-model-binding/media-formatters) – Todd Aug 18 '16 at 21:08
5

Based on @Todd last comment, the answer to the question is:

Create a HttpParameterBinding class:

public class UserAwareHttpParameterBinding : HttpParameterBinding
{
    private readonly HttpParameterBinding _paramaterBinding;
    private readonly HttpParameterDescriptor _httpParameterDescriptor;

    public UserAwareHttpParameterBinding(HttpParameterDescriptor descriptor) : base(descriptor)
    {
        _httpParameterDescriptor = descriptor;
        _paramaterBinding = new FromBodyAttribute().GetBinding(descriptor);
    }

    public override async Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken)
    {
        await _paramaterBinding.ExecuteBindingAsync(metadataProvider, actionContext, cancellationToken);

        var baseModel = actionContext.ActionArguments[_httpParameterDescriptor.ParameterName] as IUserAware;
        if (baseModel != null)
        {
            baseModel.UserId = new Guid("6ed85eb7-e55b-4049-a5de-d977003e020f"); // Or get it form the actionContext.RequestContext!
        }
    }
}

And wire it up in the HttpConfiguration:

configuration.ParameterBindingRules.Insert(0, descriptor => typeof(IUserAware).IsAssignableFrom(descriptor.ParameterType) ? new UserAwareHttpParameterBinding(descriptor) : null);

If anyone know how this is done in .NET Core MVC - please edit this post or comment.

janhartmann
  • 14,713
  • 15
  • 82
  • 138