Click here to Skip to main content
15,121,505 members
Articles / All Topics
Technical Blog
Posted 15 Sep 2017

Tagged as

Stats

8.4K views
3 bookmarked

Custom Model Binding in ASP.NET Core 2.0

Rate me:
Please Sign up or sign in to vote.
4.00/5 (4 votes)
15 Sep 2017CPOL2 min read
How to implement custom model binders in ASP.NET Core. Continue reading

Problem

How to implement custom model binders in ASP.NET Core.

Description:  in an earlier post I discussed how to prevent insecure object references by encrypting the internal references (e.g. table primary keys) using Data Protection API. To avoid duplication of code that encrypt/decrypt on every controller I used filters in that example. In this post I’ll use another complimentary technique: custom model binding.

What we want to achieve is:

  1. Encrypt data going out to views; using result filters.
  2. Decrypt as it comes back to controllers; using custom model binding.

Note: if you’re new to Model BindingData Protection API or Filters, please read earlier posts first.

Solution

Create a marker interface and attribute to flag properties on our model as protected i.e. require encryption/decryption:

public interface IProtectedIdAttribute { }

    public class ProtectedIdAttribute 
        : Attribute, IProtectedIdAttribute  { }

Create a custom model binder by implementing IModelBinder interface:

public class ProtectedIdModelBinder : IModelBinder
    {
        private readonly IDataProtector protector;

        public ProtectedIdModelBinder(IDataProtectionProvider provider)
        {
            this.protector = provider.CreateProtector("protect_my_query_string");
        }

        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            var valueProviderResult =
                bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

            if (valueProviderResult == ValueProviderResult.None)
                return Task.CompletedTask;

            bindingContext.ModelState.SetModelValue(
                    bindingContext.ModelName, valueProviderResult);

            var result = this.protector.Unprotect(valueProviderResult.FirstValue);

            bindingContext.Result = ModelBindingResult.Success(result);
            return Task.CompletedTask;
        }
    }

Create a custom model binder provider by implementing IModelBinderProvider interface:

public class ProtectedIdModelBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context.Metadata.IsComplexType) return null;

            var propName = context.Metadata.PropertyName;
            if (propName == null) return null;

            var propInfo = context.Metadata.ContainerType.GetProperty(propName);
            if (propInfo == null) return null;

            var attribute = propInfo.GetCustomAttributes(
                typeof(IProtectedIdAttribute), false).FirstOrDefault();
            if (attribute == null) return null;

            return new BinderTypeModelBinder(typeof(ProtectedIdModelBinder));
        }
    }

Add our custom model binder provider to MVC services in Startup class:

public void ConfigureServices(
            IServiceCollection services)
        {
            services.AddDataProtection();
            
            services.AddMvc(options =>
            {
                options.ModelBinderProviders.Insert(
                     0, new ProtectedIdModelBinderProvider());
            });
        }

Add our [ProtectedId] attribute to models:

public class MovieInputModel
    {
        [ProtectedId]
        public string Id { get; set; }
    }

    public class MovieViewModel
    {
        [ProtectedId]
        public string Id { get; set; }
        public string Title { get; set; }
        public int ReleaseYear { get; set; }
        public string Summary { get; set; }
    }

Add a controller to use the models:

public class HomeController : Controller
    {
        public IActionResult Index()
        {
            List<MovieViewModel> model = GetMovies();
            return View(model);
        }

        public IActionResult Details(MovieInputModel model)
        {
            return Content(model.Id);
        }

Views will show encrypted identifiers:

But our model binder will decrypt it:

Discussion

As we discussed in the previous post Model Binding is the mechanism through which ASP.NET Core maps HTTP request to our models. In order to achieve this mapping the framework will go through a list of providers that will indicate whether they can handle the mapping or not. If they can, they will return a binder that is responsible for actually doing the mapping.

Model Binding

In order to tell the framework that we need some bespoke mapping i.e. decryption of incoming data, we create our own provider and binder.

Model Binder Provider will decide when our custom binder is needed. In our solution the provider is simply checking the existence of our attribute/interface on the model property and if it exists, it will return our custom binder.

Note: BinderTypeModelBinder is used here since our custom binder has dependencies it needs at runtime. Otherwise you could just return an instance of your custom binder.

Model Binder will actually do the mapping (i.e. decryption in our case) of incoming data. In our solution we get the value being passed, decrypt it using Data Protection API and set the decrypted value as binding result.

Result Filter

In order to automatically encrypt the properties of our model, I wrote a simple result filter:

public class ProtectedIdResultFilter : IResultFilter
    {
        private readonly IDataProtector protector;

        public ProtectedIdResultFilter(IDataProtectionProvider provider)
        {
            this.protector = provider.CreateProtector("protect_my_query_string");
        }

        public void OnResultExecuting(ResultExecutingContext context)
        {
            var viewResult = context.Result as ViewResult;
            if (viewResult == null) return;

            if (!typeof(IEnumerable).IsAssignableFrom(viewResult.Model.GetType()))
                return;

            var model = viewResult.Model as IList;
            foreach (var item in model)
            {
                foreach (var prop in item.GetType().GetProperties())
                {
                    var attribute =
                        prop.GetCustomAttributes(
                            typeof(IProtectedIdAttribute), false).FirstOrDefault();

                    if (attribute != null)
                    {
                        var value = prop.GetValue(item);
                        var cipher = this.protector.Protect(value.ToString());
                        prop.SetValue(item, cipher);
                    }
                }
            }
        }

        public void OnResultExecuted(ResultExecutedContext context)
        {
            
        }
    }

    public class ProtectedIdResultFilterAttribute : TypeFilterAttribute
    {
        public ProtectedIdResultFilterAttribute() 
            : base(typeof(ProtectedIdResultFilter))
        { }
    }

This filter is added globally in Startup class:

public void ConfigureServices(
            IServiceCollection services)
        {
            services.AddDataProtection();
            
            services.AddMvc(options =>
            {
                options.ModelBinderProviders.Insert(
                  0, new ProtectedIdModelBinderProvider());
                options.Filters.Add(typeof(ProtectedIdResultFilter));
            });
        }

Source Code

GitHub: https://github.com/TahirNaushad/Fiver.Mvc.ModelBinding.Custom

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author


Comments and Discussions

 
GeneralMy vote of 5 Pin
E. Scott McFadden18-Sep-17 8:54
professionalE. Scott McFadden18-Sep-17 8:54 
Once again an awesome article!
GeneralRe: My vote of 5 Pin
User 104326418-Sep-17 22:56
MemberUser 104326418-Sep-17 22:56 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.