Tuesday, November 7, 2023

Bind complex view model objects in ASP.Net Core using ComplexObjectModelBinder

The way how view model objects are bound has been changed in ASP.Net Core. Now we need to create custom model binder class which inherits IModelBinder interface and implement binding logic there (in ASP.Net Core there is no DefaultModelBinder base class anymore which we may inherit in custom binders). However for most cases binding logic itself will be the same still - only custom logic will differ (like validation specific to view model class). Instead of implementing this binding logic for each view model class by ourselves we may use builtin ComplexObjectModelBinder which will do actual work for us. In this post I will show how to do that.

Let's say we have RequestModel view model class which can be used for asking contact information (name, email, phone):

public class RequestModel
{
    public string Name { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }
}

For binding this model we need to implement some infrastructural classes. First of all we need custom model binder provider which inherits IModelBinderProvider. But instead of creating own binder provider for each view model class let's create generic ComplexModelBinderProvider class which can be used with any view model:

public class ComplexModelBinderProvider<TModel, TBinder> : IModelBinderProvider
{
    private ComplexObjectModelBinderProvider complexObjectBinderProvider;

    public ComplexModelBinderProvider(ComplexObjectModelBinderProvider complexObjectBinderProvider)
    {
        this.complexObjectBinderProvider = complexObjectBinderProvider;
    }

    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType == typeof(TModel))
        {
            return (IModelBinder)Activator.CreateInstance(typeof(TBinder),
                new object[] { complexObjectBinderProvider.GetBinder(context) });
        }
        return null;
    }
}

It use 2 generic types: one for view model class (TModel) and another for its model binder which will be used for binding objects of TModel. Note that when it creates model binder it passes instance of ComplexObjectModelBinder to constructor (complexObjectBinderProvider.GetBinder(context)).

We also need custom model binder class RequestModelBinder for our view model - but instead of implementing binding logic there we will just call ComplexObjectModelBinder which was injected in constructor and which will do all complex work (get values from form values, query string params, etc):

public class RequestModelBinder : IModelBinder
{
    protected IModelBinder complexObjectBinder;
	
    public RequestModelBinder(IModelBinder complexObjectBinder)
    {
	    this.complexObjectBinder = complexObjectBinder;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var model = new RequestModel();
        bindingContext.Model = model;
        await this.complexObjectBinder.BindModelAsync(bindingContext);
		
		// add custom logic there if needed
    }
}

Now we just need to link all of that together in Program.cs:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews(
    o =>
    {
        var complexObjectBinderProvider = o.ModelBinderProviders.First(p => p is ComplexObjectModelBinderProvider) as ComplexObjectModelBinderProvider;
        o.ModelBinderProviders.Insert(0, new ComplexModelBinderProvider<RequestModel, RequestModelBinder>(complexObjectBinderProvider));
        // add more ComplexModelBinderProvider instances for other view models
        ...
    });

Here we added our generic ComplexModelBinderProvider for RequestModel view model which during binding will create instance of RequestModelBinder which in turn will bind model by delegating work to ComplexObjectModelBinder. The more view models we have in the code the more advantages this approach will bring since we will just reuse the same binding logic for all models.

No comments:

Post a Comment