ASP.NET Core - Model Binding of abstract classes

 
5/13/2018
.NET Core, C#, MVC
4 Comments

Last week I updated my old blog post to ASP.NET Core. Since the DefaultModelBinder class does not exist any more, I had to rewrite my model binding code.

My controller action takes a collection of an abstract class as input:

public ActionResult Index(ICollection<AbstractSearch> searchCriteria)

To get the correct instances of AbstractSearch, I'm storing the type information in a hidden field in the view.
During model binding this information is used to instantiate the correct classes.

To customize model binding in ASP.NET Core you have to take the following steps:

Create an IModelBinderProvider

The IModelBinderProvider is responsible of creating the correct IModelBinder.
In my case several concrete classes, which derive from AbstractSearch, have to be constructible. Foreach concrete type a ComplexTypeModelBinder is added to a dictionary.

public class AbstractSearchModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType == typeof(AbstractSearch))
        {
            var assembly = typeof(AbstractSearch).Assembly;
            var abstractSearchClasses = assembly.GetExportedTypes()
                .Where(t => t.BaseType.Equals(typeof(AbstractSearch)))
                .Where(t => !t.IsAbstract)
                .ToList();

            var modelBuilderByType = new Dictionary<Type, ComplexTypeModelBinder>();

            foreach (var type in abstractSearchClasses)
            {
                var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
                var metadata = context.MetadataProvider.GetMetadataForType(type);

                foreach (var property in metadata.Properties)
                {
                    propertyBinders.Add(property, context.CreateBinder(property));
                }

                modelBuilderByType.Add(type, new ComplexTypeModelBinder(propertyBinders));
            }

            return new AbstractSearchModelBinder(modelBuilderByType, context.MetadataProvider);
        }

        return null;
    }
}

Create an IModelBinder

The IModelBinder takes the dictionary of ComplexTypeModelBinders.
During model binding the type information from the hidden field is used to select the correct ComplexTypeModelBinder from the dictionary. This class creates the desired concrete instance of my AbstractSearch class:

public class AbstractSearchModelBinder : IModelBinder
{
    private readonly IDictionary<Type, ComplexTypeModelBinder> modelBuilderByType;

    private readonly IModelMetadataProvider modelMetadataProvider;

    public AbstractSearchModelBinder(IDictionary<Type, ComplexTypeModelBinder> modelBuilderByType, IModelMetadataProvider modelMetadataProvider)
    {
        this.modelBuilderByType = modelBuilderByType ?? throw new ArgumentNullException(nameof(modelBuilderByType));
        this.modelMetadataProvider = modelMetadataProvider ?? throw new ArgumentNullException(nameof(modelMetadataProvider));
    }

    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var modelTypeValue = bindingContext.ValueProvider.GetValue(ModelNames.CreatePropertyModelName(bindingContext.ModelName, "ModelTypeName"));

        if (modelTypeValue != null && modelTypeValue.FirstValue != null)
        {
            Type modelType = Type.GetType(modelTypeValue.FirstValue);
            if (this.modelBuilderByType.TryGetValue(modelType, out var modelBinder))
            {
                ModelBindingContext innerModelBindingContext = DefaultModelBindingContext.CreateBindingContext(
                    bindingContext.ActionContext,
                    bindingContext.ValueProvider,
                    this.modelMetadataProvider.GetMetadataForType(modelType),
                    null,
                    bindingContext.ModelName);

                modelBinder.BindModelAsync(innerModelBindingContext);

                bindingContext.Result = innerModelBindingContext.Result;
                return Task.CompletedTask;
            }
        }

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

Register the IModelBinderProvider

As a last step the IModelBinderProvider has to be registered in Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
        // add custom binder to beginning of collection
        options.ModelBinderProviders.Insert(0, new AbstractSearchModelBinderProvider());
    });
}

Feedly Feedly Tweet


Related posts


Comments


H Khanam

H Khanam

9/21/2019

Great Post.


Dmitry Dirin

Dmitry Dirin

8/31/2019

Awesome! Also should check t.BaseType for null while scanning assembly.


Nigel Atwell

Nigel Atwell

11/15/2018

Worked perfectly! Just what I was looking for. Worth adding an example into your post, that you need to add the 'ModelTypeName' field to the abstract base model class and render this out into the HTML as hidden field. That was the only part that wasn't clear from the example. Many Thanks.


Vasiliy

Vasiliy

6/9/2018

Thanks, very interesting.