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());
});
}