5

I'm developing a web application using ASP.NET Core MVC 3.1. I'm implementing a system where people can request things, which then goes into a generic 'request' flow. Because I don't want to create dozens of controllers and views which are 80% similar, I am looking at dynamically binding the model and using partial views for the things which are different.

For this purpose, I want to override the model binder behaviour so that it can bind from the correct type at run-time. I have found some guidance[1] on how to do this with 'classic' ASP.NET MVC, but this doesn't seem to work in ASP.NET Core MVC as this has all been reengineered.

I've found out the ComplexModelTypeBinder is probably what I need, but inheriting and overriding from this doesn't get me much further as quite a number of properties on the BindingContext are now read-only.

How can I achieve the same goal in ASP.NET Core MVC?

[1] ASP.NET MVC - How To Dynamically Bind Models At Run-time

1 Answer 1

8

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:

  1. Creating a model binder, implementing IModelBinder
  2. Creating a type that implements IModelBinderProvider, which will be used to create instances of our IModelBinder
  3. 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.

Dog model binding Fish model binding

Sign up to request clarification or add additional context in comments.

2 Comments

The IModelBinderProvider was the thing which I couldn't wrap my head around, but now I have been able to create what I needed thanks to your excellent examples. Thanks for your help!
Do you know if it would be possible to implement this same concept if the interface was a property on a parent class? For example, if the viewmodel was a concrete class named Animal, and IPet was a property on it? You would then be posting an Animal object to your action method. I'm curious if there's a way to modify the ModelBinder code so that it could handle the (nested) interface property?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.