Monday, December 5, 2016

ASP.NET Core Custom FromBody Model Binding

I recently had requirement where I want to populate additional values to the input passed on to the action method. I am using AngularJS as the client which posts the data to the action method. In addition to what is passed from the client, I want to have the developer certain fields present in the model.

For AngularJS HttpPost, we use FromBody and BodyModelBinder to bind the client data to the action method parameter. So I decided to enhance the BodyModelBinder and populate these additional fields. Unfortunately BodyModelBinder and its corresponding Provider does not allow extending the existing Provider and Binder. We have to completely write our own Binder. Instead of writing our own implementation of Provider and Binder, I decided to create a wrapper that internally uses the default provider and binder.

Here is my code

Custom BodyModelBinderProvider
public class NtBodyModelBinderProvider : IModelBinderProvider
{
    private readonly IList<IInputFormatter> formatters;
    private readonly IHttpRequestStreamReaderFactory readerFactory;
    private BodyModelBinderProvider defaultProvider;

    public NtBodyModelBinderProvider(IList<IInputFormatter> formatters, IHttpRequestStreamReaderFactory readerFactory)
    {
        this.formatters = formatters;
        this.readerFactory = readerFactory;
        defaultProvider = new BodyModelBinderProvider(formatters, readerFactory);
    }

    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        IModelBinder modelBinder = defaultProvider.GetBinder(context);

        // default provider returns null when there is error.So for not null setting our binder
        if (modelBinder != null)
            {
                modelBinder = new NtBodyModelBinder(this.formatters, this.readerFactory);
            }

        return modelBinder;
    }
}

Custom ModelBinder
public class NtBodyModelBinder : IModelBinder
{
    private BodyModelBinder defaultBinder;

    public NtBodyModelBinder(IList<IInputFormatter> formatters, IHttpRequestStreamReaderFactory readerFactory) // : base(formatters, readerFactory)
    {
        defaultBinder = new BodyModelBinder(formatters, readerFactory);
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        // callinng the default body binder
        await defaultBinder.BindModelAsync(bindingContext);

        if(bindingContext.Result.IsModelSet && bindingContext.Result.Model is IServiceDocument)
        {
            // populating the additional fields
            // ....
            // ....
            // ....
        }
    }
}

Startup configuration
services.AddMvc()
    .AddMvcOptions(options =>
    {
        IHttpRequestStreamReaderFactory readerFactory = services.BuildServiceProvider().GetRequiredService<IHttpRequestStreamReaderFactory>();
        options.ModelBinderProviders.Insert(0, new NtBodyModelBinderProvider(options.InputFormatters, readerFactory));
    });


3 comments:

  1. Thank you for this...i have a very similar scenario and this fits perfect!

    ReplyDelete
  2. We have to completely write our own Binder. Instead of writing our own implementation of Provider and Binder.

    ReplyDelete