Autofac "action injection" 与 ASP.NET MVC 模型绑定
Autofac "action injection" with ASP.NET MVC model binding
我将 Autofac 与 ASP.NET MVC 一起使用,我想知道我在 Web 项目中设置视图模型是否是一个好方法。过去我只在控制器级别使用构造函数注入,但我认为看看是否所有东西都可以通过 Autofac 注入会很有趣。
假设我有一个如下所示的视图模型:
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: */ }
}
- 缓存用于访问代码集(即用于下拉列表)
- 该模型具有从视图模型调用的方法(例如 Save())
这就是事情变得有点奇怪的地方。我发现自己必须创建一个派生自 System.Web.Mvc.Async.AsyncControllerActionInvoker
的自定义操作调用程序。 Autofac 已经拥有其中之一 Autofac.Integration.Mvc.ExtensibleActionInvoker
。我发现 Autofac 内置的问题是它停止了默认模型绑定。即它成功地注入了我的依赖项,但是即使 POST 具有有效的表单数据,其余的视图模型属性也是空的。
这是 Autofac 和 ASP.NET MVC 代码的组合(为了便于阅读,删除了一些验证和注释):
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;
}
}
我知道我一直在使用默认模型绑定器,但如果我需要其他模型绑定器,可以增强它。
这可能是 Autofac 中的错误(事实上模型绑定没有发生但 DI 有效)还是我滥用了框架?
请注意,这确实有效,但由于现在还很早,我担心我可能会增加复杂性,也许应该自己处理一些依赖注入。
编辑(调整 Ruskin 的代码以适合我的应用程序):
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();
}
回答你的问题(我认为这是你的问题):
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.
我不认为这是 Autofac 中的错误,我相信模型解析发生在 MVC 将它的属性绑定到视图模型之前,所以您期望属性什么时候出现在视图模型中?
我遇到了完全相同的问题:阅读 this question
编辑:
这是您的 autofac 注册,其中 builder 是您的 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));
}
}
我将 Autofac 与 ASP.NET MVC 一起使用,我想知道我在 Web 项目中设置视图模型是否是一个好方法。过去我只在控制器级别使用构造函数注入,但我认为看看是否所有东西都可以通过 Autofac 注入会很有趣。
假设我有一个如下所示的视图模型:
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: */ }
}
- 缓存用于访问代码集(即用于下拉列表)
- 该模型具有从视图模型调用的方法(例如 Save())
这就是事情变得有点奇怪的地方。我发现自己必须创建一个派生自 System.Web.Mvc.Async.AsyncControllerActionInvoker
的自定义操作调用程序。 Autofac 已经拥有其中之一 Autofac.Integration.Mvc.ExtensibleActionInvoker
。我发现 Autofac 内置的问题是它停止了默认模型绑定。即它成功地注入了我的依赖项,但是即使 POST 具有有效的表单数据,其余的视图模型属性也是空的。
这是 Autofac 和 ASP.NET MVC 代码的组合(为了便于阅读,删除了一些验证和注释):
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;
}
}
我知道我一直在使用默认模型绑定器,但如果我需要其他模型绑定器,可以增强它。
这可能是 Autofac 中的错误(事实上模型绑定没有发生但 DI 有效)还是我滥用了框架?
请注意,这确实有效,但由于现在还很早,我担心我可能会增加复杂性,也许应该自己处理一些依赖注入。
编辑(调整 Ruskin 的代码以适合我的应用程序):
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();
}
回答你的问题(我认为这是你的问题):
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.
我不认为这是 Autofac 中的错误,我相信模型解析发生在 MVC 将它的属性绑定到视图模型之前,所以您期望属性什么时候出现在视图模型中?
我遇到了完全相同的问题:阅读 this question
编辑:
这是您的 autofac 注册,其中 builder 是您的 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));
}
}