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