具有依赖注入的MVC 6自定义模型绑定器

时间:2016-02-25 00:34:44

标签: c# dependency-injection autofac asp.net-core-mvc custom-model-binder

现在我的ViewModel看起来像这样:

public class MyViewModel
{
    private readonly IMyService myService;

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

消费此Controller的{​​{1}}如下所示:

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 classPOST会给你一个Index(MyViewModel model)例外。我意识到可以使用我No parameterless constructor defined for this object的{​​{1}}将是最有可能的解决方案......但我无法找到任何有关如何开始使用的帮助。请帮助我,尤其是custom model binder中的DI

2 个答案:

答案 0 :(得分:4)

我们在这里得到了答案: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

另外,据说你应该能够在Action方法中将[FromServices]作为param的一部分传递,它确实解析了类,但是它破坏了模型绑定...即我的属性都没有被映射。它看起来像这样:(但是,这不工作所以使用上面的例子)

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的{​​{1}}注册:

ViewModels

答案 1 :(得分:0)

这个问题被标记为 ASP.NET Core,所以这是我们针对 dotnet core 3.1 的解决方案。

我们的解决方案概要:TheProject 需要使 ICustomerService 可用于在请求管道中自动创建的对象。需要它的类用接口 IUsesCustomerService 标记。 Binder在创建对象时会检查这个接口,并处理特殊情况。

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