.NET core custom and default binding combined
Asked Answered
K

2

16

I'm creating a custom model binder for a view model, implementing IModelBinder

I have a lot of properties in my view model, the majority of which do not need any custom binding. Rather than explicitly set all of the property values on my model individually from the ModelBindingContext, I would to be able to get the framework to bind the model for me, then I would carry out any custom binding:

public class ApplicationViewModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        // get .net core to bind values on model

        // Cary out any customization of the models properties

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

Basically I want to carry out the default model binding, then apply custom binding, similar to the approach taken in this SO post but for .NET Core, not framework.

I assumed applying the default binding would be straight forward, but haven't been able to find out how to do so. I believe the solution would involve ComplexTypeModelBinder and ComplexTypeModelBinderProvider classes, but can't seem to find out how to go about it.

I know I could just make any changes when the POST request hits my controller method, but this seem the wrong place and wrong time to do so.

Kokoruda answered 19/8, 2018 at 19:22 Comment(0)
G
8

For custom ComplexTypeModelBinder, you could inherit from ComplexTypeModelBinder.

  1. Model
    public class BinderModel
    {
       public int Id { get; set; }
       public string Name { get; set; }
       public string BinderValue { get; set; }
    }
  1. Controller Action
    [HttpPost]
    public void Post([FromForm]BinderModel value)
    {

    }
  1. CustomBinder
    public class CustomBinder : ComplexTypeModelBinder
    {
        private readonly IDictionary<ModelMetadata, IModelBinder> _propertyBinders;
        public CustomBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders)
        : base(propertyBinders)
        {
            _propertyBinders = propertyBinders;
        }
        protected override Task BindProperty(ModelBindingContext bindingContext)
        {
            if (bindingContext.FieldName == "BinderValue")
            {
                bindingContext.Result = ModelBindingResult.Success("BinderValueTest");
                return Task.CompletedTask;
            }
            else
            {
                return base.BindProperty(bindingContext);
            }
        }
        protected override void SetProperty(ModelBindingContext bindingContext, string modelName, ModelMetadata propertyMetadata, ModelBindingResult result)
        {
            base.SetProperty(bindingContext, modelName, propertyMetadata, result);
        }
    }
  1. CustomBinderProvider
    public class CustomBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (context.Metadata.IsComplexType && !context.Metadata.IsCollectionType)
            {
                var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
                for (var i = 0; i < context.Metadata.Properties.Count; i++)
                {
                    var property = context.Metadata.Properties[i];
                    propertyBinders.Add(property, context.CreateBinder(property));
                }

                //var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
                //return new ComplexTypeModelBinder(propertyBinders, loggerFactory);
                return new CustomBinder(propertyBinders);
            }

            return null;
        }

    }
  1. Inject provider
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc(options => {
            options.ModelBinderProviders.Insert(0, new CustomBinderProvider());
        });
    }
Gyrostat answered 20/8, 2018 at 4:5 Comment(1)
ComplexTypeModelBinder does not have a single-parameter constructor. IModelBinder worked for me instead.Broadside
J
3

ComplexTypeModelBinder has unfortunately been deprecated in .Net 5.0, and it's counterpart, ComplexObjectModelBinder, is sealed, so you can't extend from it.

But, you can work around that. ComplexObjectModelBinderProvider is public, and you can use that to create a ComplexObjectModelBinder. Thus, if you make your own custom IModelBinderProvider, you can have the constructor accept a ComplexObjectModelBinderProvider argument, and make use of that to make a ComplexObjectModelBinder. Then, you can pass that to your custom IModelBinder, where it'll happily do its custom work before falling back to the ComplexObjectModelBinder you supplied.

Here's an example. First, your IModelBinder. This example shows that you can use DI if you want to. (In this example, say we needed a DbContext.)

public class MyCustomModelBinder : IModelBinder
{
    private readonly IModelBinder _defaultBinder;
    private readonly DbContext _dbContext;
    public MyCustomModelBinder(IModelBinder defaultBinder, DbContext dbContext)
    {
        _defaultBinder = defaultBinder;
        _dbContext = dbContext;
    }

    public override Task BindModelAsync(ModelBindingContext bindingContext)
    {
        // -do custom work here-

        return _defaultBinder.BindModelAsync(bindingContext);
    }
}

However, in order to use DI on your custom model binder, you'll need a helper class. The problem is, when IModelBinderProvider is called, it won't have access to all the services in a typical request, like your DbContext for example. But this class will help:

internal class DIModelBinder : IModelBinder
{
    private readonly IModelBinder _rootBinder;
    private readonly ObjectFactory _factory;

    public DIModelBinder(Type binderType, IModelBinder rootBinder)
    {
        if (!typeof(IModelBinder).IsAssignableFrom(binderType))
        {
            throw new ArgumentException($"Your binderType must derive from IModelBinder.");
        }
        
        _factory = ActivatorUtilities.CreateFactory(binderType, new[] { typeof(IModelBinder) });
        _rootBinder = rootBinder;
    }

    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var requestServices = bindingContext.HttpContext.RequestServices;
        var binder = (IModelBinder)_factory(requestServices, new[] { _rootBinder });
        return binder.BindModelAsync(bindingContext);
    }
}

Now you're ready to write the custom IModelBinderProvider:

public class MyCustomModelBinderProvider : IModelBinderProvider
{
    private readonly IModelBinderProvider _rootProvider;
    public MyCustomModelBinderProvider(IModelBinderProvider rootProvider)
    {
        _rootProvider = rootProvider;
    }
    
    public IModelBinder? GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType == typeof(MyModel))
        {
            var rootBinder = _rootProvider.GetBinder(context)
                ?? throw new InvalidOperationException($"Root {_rootProvider.GetType()} did not provide an IModelBinder for MyModel.");

            return new DIModelBinder(typeof(MyCustomModelBinder), rootBinder);
        }

        return null;
    }
}

Finally, in your startup file where you configure services, you can grab the ComplexObjectModelBinderProvider instance, use that to create a new instance of your MyCustomModelBinderProvider, and insert that into the ModelBinderProviders.

services.AddMvc(options =>
{
    var fallbackProvider = options.ModelBinderProviders
        .First(p => p is ComplexObjectModelBinderProvider);
    var myProvider = new MyCustomModelBinderProvider(fallbackProvider);
    options.ModelBinderProviders.Insert(0, myProvider);
})
Jephum answered 26/8, 2022 at 18:54 Comment(1)
Thanks for the detailed answer. I've found that running the default binder after my custom binding work will override that. I can run the default binder before but it means it will explode if it tries to parse a required field that it cant handle because of the custom binding I need to do on it. Is there some way to prevent the default binder overriding the values I set?Welldone

© 2022 - 2024 — McMap. All rights reserved.