Autofac “action injection” with ASP.NET MVC model binding

不打扰是莪最后的温柔 提交于 2019-12-12 03:15:21

问题


I'm using Autofac with ASP.NET MVC and am wondering if my view model setup in the web project is a good approach or not. In the past I only used constructer injection at the Controller level but thought it would be interesting to see if everything could be injected via Autofac.

Let's say I have a view model that looks this:

public class CollegeViewModel : BaseViewModel
{
    private CollegeModel _model { get; set; }

    public int Id { get; set; }
    public string Name { get; set; }

    public CollegeViewModel(ICsnCache cache, CollegeModel model)
    {
        this._cache = cache;
        this._model = model;
    }

    public void Populate() { /* TODO: */ }
    public void Update() { /* TODO: */ }
}
  • Cache is to access code sets (i.e. for drop down lists)
  • The model has methods that are called from the view model (e.g. Save())

This is where things get a bit strange. I found myself having to create a custom action invoker which derives from System.Web.Mvc.Async.AsyncControllerActionInvoker. Autofac has one of these already Autofac.Integration.Mvc.ExtensibleActionInvoker. The problem I found with the one built into Autofac is that it stopped the default model binding. i.e. it succeeded in injecting my dependencies but then the rest of the view model properties were empty even though the POST had valid form data.

This is a combination of Autofac and ASP.NET MVC code (taken out some validation and comments for readability):

public class CustomExtensibleActionInvoker : System.Web.Mvc.Async.AsyncControllerActionInvoker
{
    protected override object GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor)
    {
        object value = null;
        try
        {
            value = base.GetParameterValue(controllerContext, parameterDescriptor);
        }
        catch (MissingMethodException) { }

        if (value == null)
        {
            // We got nothing from the default model binding, so try to resolve it.
            var context = Autofac.Integration.Mvc.AutofacDependencyResolver.Current.RequestLifetimeScope;
            value = context.ResolveOptional(parameterDescriptor.ParameterType);

            // This is the part I added, which is from the ASP.NET MVC source code
            ModelBindingContext bindingContext = new ModelBindingContext()
            {
                FallbackToEmptyPrefix = (parameterDescriptor.BindingInfo.Prefix == null), // only fall back if prefix not specified
                ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => value, parameterDescriptor.ParameterType),
                ModelName = parameterDescriptor.BindingInfo.Prefix ?? parameterDescriptor.ParameterName,
                ModelState = controllerContext.Controller.ViewData.ModelState,
                PropertyFilter = GetPropertyFilter(parameterDescriptor),
                ValueProvider = controllerContext.Controller.ValueProvider,
            };

            value = System.Web.Mvc.ModelBinders.Binders.DefaultBinder.BindModel(controllerContext, bindingContext);
        }
        return value;
    }

    private Predicate<string> GetPropertyFilter(ParameterDescriptor parameterDescriptor)
    {
        ParameterBindingInfo bindingInfo = parameterDescriptor.BindingInfo;
        return propertyName => IsPropertyAllowed(propertyName, bindingInfo.Include, bindingInfo.Exclude);
    }

    private bool IsPropertyAllowed(string propertyName, ICollection<string> includeProperties, ICollection<string> excludeProperties)
    {
        bool includeProperty = (includeProperties == null) || (includeProperties.Count == 0) || includeProperties.Contains(propertyName, StringComparer.OrdinalIgnoreCase);
        bool excludeProperty = (excludeProperties != null) && excludeProperties.Contains(propertyName, StringComparer.OrdinalIgnoreCase);
        return includeProperty && !excludeProperty;
    }
}

I realise I'm always using the default model binder, but that could be enhanced if I ever needed other model binders.

Could it be a bug in Autofac (the fact model binding doesn't happen but the DI works) or am I misusing the framework?

Note that this does work, but since it's early days I'm worried I might be adding complexity and maybe should just handle some dependency injection myself.

Edit (tweaked Ruskin's code to suit my app):

public class MyCustomModelBinder : DefaultModelBinder
{
    /// <summary>
    /// If the model type is registered in our Autofac configuration,
    /// use that, otherwise let MVC create the model as per usual
    /// </summary>        
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        var item = DependencyResolver.Current.GetService(modelType);

        if (item != null)
        {
            return item;
        }
        return base.CreateModel(controllerContext, bindingContext, modelType);
    }
}

Global.asax.cs:

protected void Application_Start()
{
    // removed other code for readability    
    System.Web.Mvc.ModelBinders.Binders.DefaultBinder = new MyCustomModelBinder();
}

回答1:


To answer your question (I think that's your question):

The problem I found with the one built into Autofac is that it stopped the default model binding. i.e. it succeeded in injecting my dependencies but then the rest of the view model properties were empty even though the POST had valid form data.

I don't think it's a bug in Autofac, I believe the model resolution happens well before the MVC has bound it's properties into the view model, so when are you expecting the properties to be present in the view model?

I had the exact same issue: have a a read of this question

Edit:

This is your autofac registration where builder is your ContainerBuilder...

var types = LoadMyViewModels(); // Do some reflection to get all your viewmodels
foreach (var type in types)
{
    Type modelBinderType = typeof(MyCustomModelBinder<>);
    Type[] typeArgs = { modelType };

    Type genType = modelBinderType.MakeGenericType(typeArgs);
    object viewmodel = Activator.CreateInstance(genType);
    ModelBinders.Binders.Add(modelType, (IModelBinder)viewmodel);

    var registeredType = builder.RegisterType(modelType).AsSelf();
}

CustomModelBinder

[ModelBinderType]
public class MyCustomModelBinder<T> : DefaultModelBinder where T : class
{
    [NotNull]
    protected override object CreateModel([NotNull] ControllerContext controllerContext, [NotNull] ModelBindingContext bindingContext, [NotNull] Type modelType)
    {
        var item = DependencyResolver.Current.GetService<T>();

        if (item != null)
        {
            return item;
        }
        throw new ArgumentException(string.Format("model type {0} is not registered", modelType.Name));
    }
}


来源:https://stackoverflow.com/questions/28260285/autofac-action-injection-with-asp-net-mvc-model-binding

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!