Cannot call action method 'System.Web.Mvc.PartialViewResult Foo[T](T)' on controller 'Controller' because the action method is a generic method
The code that throws that is inside the default ActionDescriptor:
internal static string VerifyActionMethodIsCallable(MethodInfo methodInfo) {
// we can't call instance methods where the 'this' parameter is a type other than ControllerBase
if (!methodInfo.IsStatic && !typeof(ControllerBase).IsAssignableFrom(methodInfo.ReflectedType)) {
return String.Format(CultureInfo.CurrentUICulture, MvcResources.ReflectedActionDescriptor_CannotCallInstanceMethodOnNonControllerType,
methodInfo, methodInfo.ReflectedType.FullName);
}
// we can't call methods with open generic type parameters
if (methodInfo.ContainsGenericParameters) {
return String.Format(CultureInfo.CurrentUICulture, MvcResources.ReflectedActionDescriptor_CannotCallOpenGenericMethods,
methodInfo, methodInfo.ReflectedType.FullName);
}
// we can't call methods with ref/out parameters
ParameterInfo[] parameterInfos = methodInfo.GetParameters();
foreach (ParameterInfo parameterInfo in parameterInfos) {
if (parameterInfo.IsOut || parameterInfo.ParameterType.IsByRef) {
return String.Format(CultureInfo.CurrentUICulture, MvcResources.ReflectedActionDescriptor_CannotCallMethodsWithOutOrRefParameters,
methodInfo, methodInfo.ReflectedType.FullName, parameterInfo);
}
}
// we can call this method
return null;
}
Because the code is calling "methodInfo.ContainsGenericParameters" I don't think there is a way to override this behavior without creating your own ActionDescriptor. From glancing at the source code this seems non-trivial.
Another option is to make your controller class generic and create a custom generic controller factory. I have some experimental code that creates a generic controller. Its hacky but its just a personal experiment.
public class GenericControllerFactory : DefaultControllerFactory
{
protected override Type GetControllerType(System.Web.Routing.RequestContext requestContext, string controllerName)
{
//the generic type parameter doesn't matter here
if (controllerName.EndsWith("Co"))//assuming we don't have any other generic controllers here
return typeof(GenericController<>);
return base.GetControllerType(requestContext, controllerName);
throw new InvalidOperationException("Generic Factory wasn't able to resolve the controller type");
}
protected override IController GetControllerInstance(System.Web.Routing.RequestContext requestContext, Type controllerType)
{
//are we asking for the generic controller?
if (requestContext.RouteData.Values.ContainsKey("modelType"))
{
string typeName = requestContext.RouteData.Values["modelType"].ToString();
//magic time
return GetGenericControllerInstance(typeName, requestContext);
}
if (!typeof(IController).IsAssignableFrom(controllerType))
throw new ArgumentException(string.Format("Type requested is not a controller: {0}",controllerType.Name),"controllerType");
return base.GetControllerInstance(requestContext, controllerType);
}
/// <summary>
/// Returns the a generic IController tied to the typeName requested.
/// Since we only have a single generic controller the type is hardcoded for now
/// </summary>
/// <param name="typeName"></param>
/// <returns></returns>
private IController GetGenericControllerInstance(string typeName, RequestContext requestContext)
{
var actionName = requestContext.RouteData.Values["action"];
//try and resolve a custom view model
Type actionModelType = Type.GetType("Brainnom.Web.Models." + typeName + actionName + "ViewModel, Brainnom.Web", false, true) ??
Type.GetType("Brainnom.Web.Models." + typeName + ",Brainnom.Web", false, true);
Type controllerType = typeof(GenericController<>).MakeGenericType(actionModelType);
var controllerBase = Activator.CreateInstance(controllerType, new object[0] {}) as IController;
return controllerBase;
}
}
Six year later, with MVC5 (and MVC6) in town, I ran into this same problem. I'm building my site with MVC5 so I can safely assume it's not yet supported out of the box. I landed here in search of solution. Well, I eventually found a way to fix it without hacking into the controller or its factory, especially because I only needed this feature in just a few places.
The approach: modifying slightly the Command Pattern (which I was already using in my code anyway).
For this problem, you start by defining the interface
public interface IMyActionProcessor
{
PartialViewResult Process<T>(T theModel);
}
and the corresponding implementation:
public sealed class MyActionProcessor : IMyActionProcessor
{
// Your IoC container. I had it explicitly mapped just like any other
// registration to cause constructor injection. Using SimpleInjector, it will be
// <code>Container.Register(typeof(IServiceProvider), () => Container);</code>
private IServiceProvider _serviceProvider;
public MyActionProcessor(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public PartialViewResult Process<T>(T theModel)
{
var handlerType =
typeof(IMyActionHandler<>).MakeGenericType(theModel.GetType());
dynamic handler = _serviceProvider.GetService(handlerType);
return handler.Handle((dynamic)theModel);
}
}
The handler (userd in the code above) will look like this:
public interface IMyActionHandler<T> where T : class
{
PartialViewResult Execute(T theModel);
}
With the above in place, all you now have to do is provide implementation(s) for the handler based on the specifics of your class T
. Something like this:
public class ModelClassHandler : IMyActionHandler<ModelClass>
{
public PartialViewResult Execute(ModelClass theModel);
{
// Do Stuff here and return a PartialViewResult instance
}
}
With all the above in place, you can now simply do this in your controller:
var processor = YourServiceLocator.GetService<IMyActionProcessor>();
var model = new ModelClass { /* Supply parameters */ };
var partialViewResult = processor.Process(model);
I know it's an extra level of indirection but it worked excellently. In my case, I went on extend the handler and processor so that they can return anything I want, not just PartialViewResult
.
I'll be interested in seeing a simpler solution to this, if one exist. I guess it's not common to have this kind of problem using MVC.
PS: Any good IoC container should be able to register open generics by scanning through assemblies. Thus, once configured appropriately, you do not need to explicitly register the implementations for the handler interface.
PPS: This post provides better insight to the answer given here, especially on how the solution might be generalized.