Связывание пользовательской модели MVC 6 с внедрением зависимостей

Сейчас мой ViewModel выглядит так:

public class MyViewModel
{
    private readonly IMyService myService;

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

Мой Controller, потребляющий этот ViewModel, выглядит так:

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);
    }
}

Мне нужно, чтобы мой Controller выглядел так:

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);
    }
}

Прямо сейчас вызов первого метода Index работает нормально (с

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

в моем Startup class), но выполнение от POST до Index(MyViewModel model) приводит к исключению No parameterless constructor defined for this object. Я понимаю, что custom model binder, который может использовать мой DI, будет наиболее вероятным решением... но я не могу найти здесь никакой помощи о том, как начать работу. Пожалуйста, помогите мне с этим, особенно для Autofac в MVC 6.


person Serj Sagan    schedule 25.02.2016    source источник
comment
Проверьте этот ответ   -  person Erkan Demirel    schedule 25.02.2016
comment
Да, я видел это, и мы, наконец, заработали, но нам пришлось использовать несколько хакерских мер, чтобы это сделать... чтобы исправить хакерство, нам теперь нужен ответ на этот вопрос (заданный моим коллегой) stackoverflow.com/q/35640858/550975, тогда мы поделимся нашей реализацией здесь   -  person Serj Sagan    schedule 26.02.2016
comment
Я также разместил вопрос здесь: groups.google.com/forum/# !topic/autofac/1Zin8oh7x1E   -  person Serj Sagan    schedule 26.02.2016
comment
Также опубликовано здесь: github.com/aspnet/Mvc/issues/4167   -  person Serj Sagan    schedule 26.02.2016
comment
Мне удалось получить желаемое поведение, внедрив собственный ContractResolver. См. пример на github.com/tjeerdhans/DiModelBindingExample.   -  person tjeerdhans    schedule 15.06.2018


Ответы (2)


Мы получили ответ здесь: https://github.com/aspnet/Mvc/issues/4167< /а>

И ответ заключается в использовании: [FromServices]

Моя модель выглядит так:

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

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

Хотя делать это свойство public грустно, это гораздо менее грустно, чем необходимость использовать custom model binder.

Кроме того, предположительно вы должны иметь возможность передать [FromServices] как часть параметра в методе Action, он разрешает класс, но это нарушает привязку модели... т. е. ни одно из моих свойств не было сопоставлено. Это выглядит так: (но опять же, ЭТО НЕ РАБОТАЕТ, поэтому используйте приведенный выше пример)

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

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

ОБНОВЛЕНИЕ 1

После работы с атрибутом [FromServices] мы решили, что внедрение свойств во все наши ViewModels — это не тот путь, которым мы хотели бы идти, особенно когда мы думали о долгосрочном обслуживании с помощью тестирования. Итак, мы решили удалить атрибуты [FromServices] и заработали наши пользовательские связыватели моделей:

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);
    }
}

В методе Startup ConfigureServices это прописано так:

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

        });

И это все. (Даже не уверен, что options.ModelBinders.Clear(); нужен.)

ОБНОВЛЕНИЕ 2 После прохождения различных итераций, чтобы заставить это работать (с помощью https://github.com/aspnet/Mvc/issues/4196), вот окончательный результат:

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;
    }
}

Вы, очевидно, захотите заменить OUR.SOLUTION... любым namespace для вашей ViewModels нашей регистрации:

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

ОБНОВЛЕНИЕ 3. Это последняя версия Model Binder и ее Provider, которая работает с 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);
    }
}

Затем в 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));
person Serj Sagan    schedule 26.02.2016
comment
В последней версии ASP.NET Core 1.0 нет OperationBindingContext на bindingContext, есть ли у вас какие-либо обновления для последней версии ядра asp.net? - person psulek; 28.09.2016

Этот вопрос помечен как ASP.NET Core, поэтому вот наше решение для dotnet core 3.1.

Схема нашего решения: TheProject должен сделать ICustomerService доступным для объекта, созданного автоматически в конвейере запросов. Классы, которым это необходимо, помечены интерфейсом IUsesCustomerService. Затем этот интерфейс проверяется связующим при создании объекта, и обрабатывается особый случай.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;

namespace TheProject.Infrastructure.DependencyInjection
{
    /// <summary>
    /// This is a simple pass through class to the binder class.
    /// It gathers some information from the context and passes it along.
    /// </summary>
    public class TheProjectModelBinderProvider : IModelBinderProvider
    {
        public TheProjectModelBinderProvider()
        {
        }

        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            ILoggerFactory ilogger;

            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            // The Binder that gets returned is a <ComplexTypeModelBinder>, but I'm
            // not sure what side effects returning early here might cause.
            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));
            }

            ilogger = (ILoggerFactory)context.Services.GetService(typeof(ILoggerFactory));

            return new TheProjectModelBinder(propertyBinders, ilogger);
        }
    }
    
    /// <summary>
    /// Custom model binder.
    /// Allows interception of endpoint method to adjust object construction
    /// (allows automatically setting properties on an object that ASP.NET creates for the endpoint).
    /// Here this is used to make sure the <see cref="ICustomerService"/> is set correctly.
    /// </summary>
    public class TheProjectModelBinder : ComplexTypeModelBinder
    {
        public TheProjectModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders, ILoggerFactory loggerFactory)
            : base(propertyBinders, loggerFactory)
        {
        }

        /// <summary>
        /// Method to construct an object. This normally calls the default constructor.
        /// This method does not set property values, setting those are handled elsewhere in the pipeline,
        /// with the exception of any special properties handled here.
        /// </summary>
        /// <param name="bindingContext">Context.</param>
        /// <returns>Newly created object.</returns>
        protected override object CreateModel(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
                throw new ArgumentNullException(nameof(bindingContext));

            var customerService = (ICustomerService)bindingContext.HttpContext.RequestServices.GetService(typeof(ICustomerService));
            bool setcustomerService = false;

            object model;

            if (typeof(IUsesCustomerService).IsAssignableFrom(bindingContext.ModelType))
            {
                setcustomerService = true;
            }
            
            // I think you can also just call Activator.CreateInstance here.
            // The end result is an object that's constructed, but no properties are set yet.
            model = base.CreateModel(bindingContext);

            if (setcustomerService)
            {
                ((IUsesCustomerService)model).SetcustomerService(customerService);
            }

            return model;
        }
    }
}

Затем в коде запуска обязательно установите AddMvcOptions.

public void ConfigureServices(IServiceCollection services)
{
    // ...
    
    // asp.net core 3.1 MVC setup 
    services.AddControllersWithViews()
        .AddApplicationPart(assembly)
        .AddRazorRuntimeCompilation()
        .AddMvcOptions(options =>
        {
            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 Infrastructure.DependencyInjection.TheProjectModelBinderProvider());
        });
}
person BurnsBA    schedule 05.02.2021