Tuesday 23 April 2019

Custom Model Binding through header in DotNetCore

Now the day with Asp.net core we have facility to bind object through exact binding by using attributes [FromHeader], [FromQuery], [FromRoute], [FromForm], [FromBody] and [FromServices] etc.

In this post we are focusing on FromHeader in which we have one constrain that it doesn't not resolve Complex Type.
But we can overcome from this constrain by using ModelBinder by using which we can customize Binder in asp.net core.

Scenario
Let's suppose that we have a Complex Model

public class UserLogin
{
    public int UserId { getset; }
    public string Password { getset; }
    public DateTime? RequestTime { getset; }
}

Now we want to make a get call where we are putting JSON value from Header with key RequestModel.

Now what we are expecting that, from RequestModel header key Model should resolve there in controller where we are using FromHeader attribute.
But unfortunately it is null.

To resolve this problem we can have 2 quick solution.
1. Create Custom Binder that implement IModelBinder, and use additional hint everywhere, when binding required from header.
2. Create a Custom Binder from IModelBinder and Custom provider from IModelBinderProvider, point Binder to Provider and Use provider in ConfigureServices globally so without adding additional hint model will be resolve.  
Note: In this example we just resolve only one header key RequestModel.

Solution 1:
Create a simple HeaderComplexModelBinder.cs
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Newtonsoft.Json;
using R6.Core.Constants;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
 
namespace R6.Service.Filter
{
    [ExcludeFromCodeCoverageAttribute]
    public class R6ModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }
 
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }
 
            var headerKey = bindingContext.ModelMetadata.BinderModelName;//Resolve Name from FormHeader(Name = "Key")
            if (string.IsNullOrEmpty(headerKey))
            {
                headerKey = Constant.CUSTOM_HEADER_NAME;
            }
            var headerValue = bindingContext.HttpContext.Request.Headers[headerKey].FirstOrDefault();
            var modelType = bindingContext.ModelMetadata.ModelType;
 
            if (!string.IsNullOrEmpty(headerValue))
            {
                bindingContext.Model = JsonConvert.DeserializeObject(headerValuemodelType);
                bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
            }
            return Task.CompletedTask;
        }
    }
}

And change action like this, and you found resolved Model.
[ModelBinder(typeof(HeaderComplexModelBinder))] UserLogin userLogin
Solution 2:
We need to keep HeaderComplexModelBinder.cs and create an additional HeaderComplexModelBinderProvider.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using System;
using System.Linq;
 
namespace CoreApp.WebApi
{
    public class HeaderComplexModelBinderProvider : IModelBinderProvider
    {
 
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }
 
            if (context.Metadata.IsComplexType)
            {
                var x = context.Metadata as Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.DefaultModelMetadata;
                var headerAttributex.Attributes.Attributes.Where(a => a.GetType() == typeof(FromHeaderAttribute)).FirstOrDefault();
                if (headerAttribute != null)
                {
                    return new BinderTypeModelBinder(typeof(HeaderComplexModelBinder));
                }
                else
                {
                    return null;
                }
            }
            else
            {
                return null;
            }
        }
    }
}

And add provider inside MVC service as option in ConfigureServices.
services.AddMvcCore(options =>
{
options.ModelBinderProviders.Insert(0, new HeaderComplexModelBinderProvider());
}

Now make your action very simple like this and you found resolved Model. :-)