Mar 17 2017

Custom Model Binding in Asp.net Core, 3: Model Binding Interfaces

Category: MVC | Asp.net core | Asp.netFrancesco @ 01:53


Custom Model Binding in Asp.net Core Mvc, 1

Custom Model Binding in Asp.net Core, 2: Getting Time + Client Time Zone Offset

Model binding interfaces: fixing server side validation

Asp.net core: Deserializing Json with Dependency Injection

 

In this last post of the model binding series I will discuss model binders that are not specific for a single type. That is, model binders able to handle generic types, and/or categories of types. Typical examples of these model binders are the built-in model binders for all complex types, all IEnumerables, and all dictionaries. If we give a look at all these model binders, we see that they recursively call the binding process itself to bind child elements and/or properties. They do this after having created a child binding context with a call to bindingContext.EnterNestedScope that loads the newly created child binding context in the same bindingContext variable. The call is enclosed within an using block, so that the original bindingContext is restored after exiting the block.

As an example we will build a custom model binder for interfaces, that is, a model binder that binds interfaces by looking for a an implementation in the Asp.net core Dependency Injection framework, and then by calling back the binding process itself on that implementation. Interfaces model binding is not only a good example of recursive model binding but also an useful programming tool for the following reasons:

  1. Using interfaces instead of concrete ViewModels allows a better separation between business layer and user interface layer.
  2. ViewModels inheritance allows a better reuse of views and metadata (validation attributes, DisplayAttributes, etc.), and interfaces overcome the limitation of inheriting from a single father class, since each interface may inherit from multiple other interfaces.

Asp.net core Mvc already has a model binder that is able to inject objects from the Dependency Injection engine into action methods. Its usage is triggered by the FromServicesAttribute, and it just injects object instances as they are returned by the DI engine without binding their properties to the input received by the client. Our custom model binder, instead, will be invoked only on interfaces or abstract classes and only when no FromServicesAttribute is provided. Moreover, once invoked it will bind all interface/abstract class properties against the input received by the client.

Preparing the project

As a first step let create a new asp.net core project named “ModelBindingInterfaces” and let select “no authentication”. Then let create a new folder called “ViewModels”.

Now let add an Interface called “ITestInterface”  to this folder :

using System.ComponentModel.DataAnnotations;

namespace ModelBindingInterfaces.ViewModels
{
    public interface ITestInterface
    {
        [Range(1, 10)]
        int TestProperty { get; set; }
    }
}

and a class implementing this interface, called “TestViewModel”:

using Microsoft.AspNetCore.Mvc;

namespace ModelBindingInterfaces.ViewModels
{
    [ModelMetadataType(typeof(ITestInterface))]
    public class TestViewModel : ITestInterface
    {
        public int TestProperty { get; set; }
    }
}

All metadata are added to the interface, so, to be sure they are used also by its implementation, we define the interface as a “ModelMetaDataType” for its implementation.

Now let register the above pair in the dependency injection engine, by adding the last line of the code below to the “startup.cs” file:

public void ConfigureServices(IServiceCollection services)
        {
            // Add framework services.
            services.AddApplicationInsightsTelemetry(Configuration);
            services.AddTransient<ITestInterface, TestViewModel>();

Finally, we need a controller and a test view. Let modify the “Index” action method of the HomeController in “HomeController.cs” as follows:

public IActionResult Index()
{
    return View();
}
[HttpPost]
public IActionResult Index(ITestInterface model)
{
    return View(model);
}

 

And also its “Views\Home\Index.cshtml” associated view:

@model ModelBindingInterfaces.ViewModels.ITestInterface

@{
    ViewData["Title"] = "Model Binding Interfaces";
}

<h2>@ViewData["Title"]</h2>

<form asp-action="Index" asp-controller="Home">
    <div class="form-group">
        <label asp-for="TestProperty" class="control-label"></label>
        <input asp-for="TestProperty" class="form-control">
        <span asp-validation-for="TestProperty" class="text-danger"></span>
    </div>
    <button type="submit" class="btn btn-default">Submit</button>
</form>

 

As you can see both controller and view reference only the interface. The only chunk of code that mentions its actual implementation is the DI container definition.

If you run the project as it is now, and try to submit the Index form, you get the following error:

“ModelBindingInterfaces.ViewModels.ITestInterface' does not have a default constructor”

In fact, all default model binders are not able to create automatically an instance of the  “ITestInterface'” interface.

Implementing the custom model binder

Our model binder should perform a job similar to the one of the built-in ComplexTypeModelBinder. The only difference being that instead of attempting to create an interface it should invoke the the DI engine to get an instance of a class that implements the interface. The code of the ComplexTypeModelBinder is quite complex, but its kernel is the invocation of “bindingContext.EnterNestedScope” on each type property. EnterNestedScope is passed an updated "ModelName" obtained by appending ".<property name>" to the current ModelName. ModelName is used to retrieve data from the input received by the client once a leaf property is reached, as we have seen in the previous post of this series. Inside the using block associated to EnterNestedScope there is a call to the “BindProperty” method, that in turn, retrieves the right binder for the property by looking in the dictionary of all property binders that is passed in the ComplexTypeModelBinder constructor. Once the right binder is found it is invoked on the child binding context created by “bindingContext.EnterNestedScope”. The result is finally stored in a variable defined outside the using block.

All results returned by the recursive calls on all  properties are then used to populate an instance of the type created by the “CreateModel” method.

We need just to modify “CreateModel” so that it uses the DI engine to get an instance of the type. Thus, we may inherit from the ComplexTypeModelBinder  and override its CreateModel method.

Let add the new “ModelBinders” folder to our project, and then add our “InterfacesModelBinder” class to this folder:

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;

namespace ModelBindingInterfaces.ModelBinders
{
    public class InterfacesModelBinder : ComplexTypeModelBinder
    {

        public InterfacesModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinder)
            : base(propertyBinder)
        {

        }
        protected override object CreateModel(ModelBindingContext bindingContext)
        {
            return bindingContext.HttpContext
                .RequestServices.GetService(bindingContext.ModelType);
        }
    }
}

 

Implementing the provider

In the previous post of the series we have seen that model binders are selected and created by model binder providers, so now it is time to write a provider for our custom model binder. Before start coding we need an answer to two questions:

  1. When the provider must return a not null result, meaning when we are in the right situation when our custom model binder must be used.
  2. Which parameters to pass in the constructor of the model binder.

The right conditions for using our “InterfacesModelBinder” are:

  • we are going to bind either an interface or an abstract class
  • the interface is not an IEnumerable, otherwise the built-in CollectionModelBinder/DictionaryModelBinder would be more appropriate
  • no FromServicesAttribute is in place, since this attribute is used when the user needs just the injection of a type with no subsequent binding of the type against the input provided by the client.

Finally, the constructor of our “InterfacesModelBinder” needs a dictionary whose entries associate the right binder to each type property.

Now we have all information to add our InterfacesModelBinderProvider class to the ModelBinders folder:

using System;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace ModelBindingInterfaces.ModelBinders
{
    public class InterfacesModelBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (!context.Metadata.IsCollectionType &&
                (context.Metadata.ModelType.GetTypeInfo().IsInterface ||
                 context.Metadata.ModelType.GetTypeInfo().IsAbstract)  &&
                (context.BindingInfo.BindingSource == null ||
                !context.BindingInfo.BindingSource
                .CanAcceptDataFrom(BindingSource.Services)))
            {
                var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
                for (var i = 0; i < context.Metadata.Properties.Count; i++)
                {
                    var property = context.Metadata.Properties[i];
                    propertyBinders.Add(property, context.CreateBinder(property));
                }
                return new InterfacesModelBinder(propertyBinders);
            }

            return null;
        }
    }
}

The metadata of each property are taken from the MetaData.Properties array contained in the context object passed to the GetBinder method of the provider. Then, a binder for the property is created by invoking the CreateBinder method of the same context object on the property metadata.The remainder of the code is self-explicative.

The provider is installed with the same procedure of our previous post. Go to the startup.cs file and modify the ConfigureServices method as shown below:

public void ConfigureServices(IServiceCollection services)
        {
            // Add framework services.
            services.AddApplicationInsightsTelemetry(Configuration);
            services.AddTransient<ITestInterface, TestViewModel>();
            services.AddMvc(o =>
            {
                o.ModelBinderProviders.Insert(0,
                    new ModelBinders.InterfacesModelBinderProvider());
            });

As in our previous post, our provider is inserted at the top of the list of all providers to avoid that some other built-in provider might return a different model binder also when all conditions for the application of our InterfacesModelBinder are satisfied.

We have Finished! Run the project and verify how the interface is bound properly, and also how validation errors work properly.

Our series on model binding ended

but stay tuned!

We are preparing new enjoying adventures!

Francesco

Tags: , ,