Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVC 6 Custom Model Binder with Dependency Injection

Right now my ViewModel looks like this:

public class MyViewModel
{
    private readonly IMyService myService;

    public ClaimantSearchViewModel(IMyService myService)
    {
        this.myService = myService;
    }
}

My Controller that consumes this ViewModel looks like this:

public class MyController : Controller
{
    private readonly IMyService myService;
    public HomeController(IMyService myService)
    {
        this.myService = myService;
    }

    public IActionResult Index()
    {
        var model = new MyViewModel(myService);

        return View(model);
    }

    [HttpPost]
    public async Task<IActionResult> Find()
    {
        var model = new MyViewModel(myService);
        await TryUpdateModelAsync(model);

        return View("Index", model);
    }
}

What I need is my Controller to look like is this:

public class MyController : Controller
{
    private readonly IServiceProvider servicePovider;
    public MyController(IServiceProvider servicePovider)
    {
        this.servicePovider = servicePovider;
    }

    public IActionResult Index()
    {
        var model = servicePovider.GetService(typeof(MyViewModel));

        return View(model);
    }

    [HttpPost]
    public IActionResult Index(MyViewModel model)
    {
        return View(model);
    }
}

Right now, calling the first Index method works fine (with

builder.RegisterSource(new AnyConcreteTypeNotAlreadyRegisteredSource(x => x.Name.Contains("ViewModel")));

in my Startup class) but doing the POST to Index(MyViewModel model) gives you a No parameterless constructor defined for this object exception. I realize that a custom model binder that can use my DI will be the most likely solution... but I'm not able to find any help on how to even get started here. Please help me with this, especially for Autofac in MVC 6.

like image 652
Serj Sagan Avatar asked Feb 25 '16 00:02

Serj Sagan


1 Answers

We got the answer here: https://github.com/aspnet/Mvc/issues/4167

And the answer is to use: [FromServices]

My Model ends up looking like this:

public class MyViewModel
{
    [FromServices]
    public IMyService myService { get; set; }

    public ClaimantSearchViewModel(IMyService myService)
    {
        this.myService = myService;
    }
}

Although it's sad to make that property public, it's much less sad than having to use a custom model binder.

Also, supposedly you should be able to pass [FromServices] as part of the param in the Action method, it does resolve the class, but that breaks the model binding... ie none of my properties got mapped. It looks like this: (but again, THIS DOES NOT WORK so use the above example)

public class MyController : Controller
{
    ... same as in OP

    [HttpPost]
    public IActionResult Index([FromServices]MyViewModel model)
    {
        return View(model);
    }
}

UPDATE 1

After working with the [FromServices] attribute we decided that property injection in all of our ViewModels was not the way we wanted to go, especially when thinking about long term maintenance with testing. SO we decided to remove the [FromServices] attributes and got our custom model binder working:

public class IoCModelBinder : IModelBinder
{
    public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
    {
        var serviceProvider = bindingContext.OperationBindingContext.HttpContext.RequestServices;

        var model = serviceProvider.GetService(bindingContext.ModelType);
        bindingContext.Model = model;

        var binder = new GenericModelBinder();
        return binder.BindModelAsync(bindingContext);
    }
}

It's registered like this in the Startup ConfigureServices method:

        services.AddMvc().AddMvcOptions(options =>
        {
            options.ModelBinders.Clear();
            options.ModelBinders.Add(new IoCModelBinder());

        });

And that's it. (Not even sure that options.ModelBinders.Clear(); is needed.)

UPDATE 2 After going through various iterations of getting this to work (with help https://github.com/aspnet/Mvc/issues/4196), here is the final result:

public class IoCModelBinder : IModelBinder
{
    public async Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
    {   // For reference: https://github.com/aspnet/Mvc/issues/4196
        if (bindingContext == null)
            throw new ArgumentNullException(nameof(bindingContext));

        if (bindingContext.Model == null && // This binder only constructs viewmodels, avoid infinite recursion.
                (
                    (bindingContext.ModelType.Namespace.StartsWith("OUR.SOLUTION.Web.ViewModels") && bindingContext.ModelType.IsClass)
                        ||
                    (bindingContext.ModelType.IsInterface)
                )
            )
        {
            var serviceProvider = bindingContext.OperationBindingContext.HttpContext.RequestServices;
            var model = serviceProvider.GetRequiredService(bindingContext.ModelType);

            // Call model binding recursively to set properties
            bindingContext.Model = model;
            var result = await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(bindingContext);

            bindingContext.ValidationState[model] = new ValidationStateEntry() { SuppressValidation = true };

            return result;
        }

        return await ModelBindingResult.NoResultAsync;
    }
}

You'd obviously want to replace OUR.SOLUTION... with whatever the namespace is for your ViewModels Our registration:

        services.AddMvc().AddMvcOptions(options =>
        {
            options.ModelBinders.Insert(0, new IoCModelBinder());
        });

UPDATE 3: This is the latest iteration of the Model Binder and its Provider that works with ASP.NET Core 2.X:

public class IocModelBinder : ComplexTypeModelBinder
{
    public IocModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders, ILoggerFactory loggerFactory) : base(propertyBinders, loggerFactory)
    {
    }

    protected override object CreateModel(ModelBindingContext bindingContext)
    {
        object model = bindingContext.HttpContext.RequestServices.GetService(bindingContext.ModelType) ?? base.CreateModel(bindingContext);

        if (bindingContext.HttpContext.Request.Method == "GET")
            bindingContext.ValidationState[model] = new ValidationStateEntry { SuppressValidation = true };
        return model;
    }
}

public class IocModelBinderProvider : IModelBinderProvider
{
    private readonly ILoggerFactory loggerFactory;

    public IocModelBinderProvider(ILoggerFactory loggerFactory)
    {
        this.loggerFactory = loggerFactory;
    }

    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (!context.Metadata.IsComplexType || context.Metadata.IsCollectionType) return null;

        var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
        foreach (ModelMetadata property in context.Metadata.Properties)
        {
            propertyBinders.Add(property, context.CreateBinder(property));
        }
        return new IocModelBinder(propertyBinders, loggerFactory);
    }
}

Then in Startup:

services.AddMvc(options =>
{
    // add IoC model binder.
    IModelBinderProvider complexBinder = options.ModelBinderProviders.FirstOrDefault(x => x.GetType() == typeof(ComplexTypeModelBinderProvider));
    int complexBinderIndex = options.ModelBinderProviders.IndexOf(complexBinder);
    options.ModelBinderProviders.RemoveAt(complexBinderIndex);
    options.ModelBinderProviders.Insert(complexBinderIndex, new IocModelBinderProvider(loggerFactory));
like image 180
Serj Sagan Avatar answered Oct 15 '22 21:10

Serj Sagan