I can give you a starting point to get going.
In the same spirit as the article you linked, let's define some pet-related types:
public interface IPet
{
string Name { get; }
}
public class Cat : IPet
{
public string Name => "Cat";
public bool HasTail { get; set; }
}
public class Dog : IPet
{
public string Name => "Dog";
public bool HasTail { get; set; }
}
public class Fish : IPet
{
public string Name => "Fish";
public bool HasFins { get; set; }
}
And in a view, define the following form that we can play around with:
<form asp-action="BindPet" method="post">
<input type="hidden" name="PetType" value="Fish" />
<input type="hidden" name="pet.HasTail" value="true" />
<input type="hidden" name="pet.HasFins" value="true" />
<input type="submit" />
</form>
And, finally, a simple controller action that takes an IPet instance as an argument:
public IActionResult BindPet(IPet pet)
{
return RedirectToAction("Index");
}
Now, there are 3 parts to creating a polymorphic binder like this:
- Creating a model binder, implementing
IModelBinder
- Creating a type that implements
IModelBinderProvider, which will be used to create instances of our IModelBinder
- Registering our
IModelBinderProvider type so it can be used
An implementation of our binder could look as follows (I've added comments, as it's doing a fair bit):
public class PetModelBinder : IModelBinder
{
private readonly IDictionary<Type, (ModelMetadata, IModelBinder)> _binders;
public PetModelBinder(IDictionary<Type, (ModelMetadata, IModelBinder)> binders)
{
_binders = binders;
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
// Read our expected type from a form,
// and convert to its .NET type.
var petType = bindingContext.ActionContext.HttpContext.Request.Form["PetType"];
var actualType = TypeFrom(petType);
// No point continuing if a valid type isn't found.
if (actualType == null)
{
bindingContext.Result = ModelBindingResult.Failed();
return;
}
// This will become clearer once we see how _binders
// is populated in our IModelBinderProvider.
var (modelMetadata, modelBinder) = _binders[actualType];
// Create a new binding context, as we have provided
// type information the framework didn't know was available.
var newBindingContext = DefaultModelBindingContext.CreateBindingContext(
bindingContext.ActionContext,
bindingContext.ValueProvider,
modelMetadata,
bindingInfo: null,
bindingContext.ModelName);
// This tries to bind the actual model using our
// context, setting its Result property to the bound model.
await modelBinder.BindModelAsync(newBindingContext);
bindingContext.Result = newBindingContext.Result;
// Sets up model validation.
if (newBindingContext.Result.IsModelSet)
{
bindingContext.ValidationState[newBindingContext.Result] = new ValidationStateEntry
{
Metadata = modelMetadata,
};
}
}
private static Type? TypeFrom(string name)
{
return name switch
{
"Cat" => typeof(Cat),
"Dog" => typeof(Dog),
"Fish" => typeof(Fish),
_ => null
};
}
}
Next, let's implement IModelBinderProvider:
public class PetModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType != typeof(IPet))
{
return null;
}
var pets = new[] { typeof(Cat), typeof(Dog), typeof(Fish) };
var binders = new Dictionary<Type, (ModelMetadata, IModelBinder)>();
foreach (var type in pets)
{
var modelMetadata = context.MetadataProvider.GetMetadataForType(type);
binders[type] = (modelMetadata, context.CreateBinder(modelMetadata));
}
return new PetModelBinder(binders);
}
}
As you can see, that is much simpler than the binder itself, and is little more than a glorified factory. It queries the metadata for each concrete type, and also creates a binder that could handle each of those types, and passes those to our binder.
Finally, in Startup, we need to register the IModelBinderProvider for use:
services.AddControllersWithViews(options =>
{
options.ModelBinderProviders.Insert(0, new PetModelBinderProvider());
});
The 0 indicates the priority the model binder has. This ensures our binder will be checked first. If we didn't do this, another binder would attempt, and fail, to bind the type.
Now that that's done, start the debugger, place a breakpoint in the action method we created, and try submitting the form. Inspect the instance of IPet and you should see the HasFins property being set for Fish. Edit the PetType element to be Dog, repeat the above, and you should see HasTail being set.
