One of the very cool features of the vNext of ASP.NET MVC (a unified framework set to succeed MVC, Web API and Web Pages) is the ability use POCO classes as controllers. No base class, no interface to implement, 100% convention.
Let’s look a little bit into that.
POCO controllers in ASP.NET vNext π
By default, as long as your class is public, non-abstract, has a Controller suffix and is defined in an assembly that references any of the MVC assemblies (Microsoft.AspNet.Mvc.Core, Microsoft.AspNet.Mvc etc), it will be discovered as a valid controller. An example is shown below.
public class DummyController
{
public string Get()
{
return "Hello";
}
}
Of course you cannot do much with just a POCO - since you typically need access to things like current HTTP request, View Data or current Principal. However, this is not a problem at all - POCO controllers support convention-based property injection. This is achieved through DefaultControllerFactory, which looks for the following properties:
-
- ActionContext and injects an instance of ActionContext there
-
- ViewData, and injects an instance of ViewDataDictionary there
-
- Url, and injects an instance of IUrlHelper there
You can see the source code here.
So we could modify our controller accordingly:
public class DummyController
{
public ActionContext ActionContext { get; set; }
public ViewDataDictionary ViewData { get; set; }
public IUrlHelper Url { get; set; }
public string Get()
{
return "Hello";
}
}
And boom, just like that you have all the contextual information you might need - for example, HttpRequest and Principal are both hanging off ActionContext.
In addition to that, the helpers, such as the aforementioned IUrlHelper or IActionResultHelper, can also be constructor injected, since ASP.NET vNext has dependency injection all the way through.
public class DummyController
{
private readonly IUrlHelper _urlHelper;
private readonly IActionResultHelper _actionResultHelper;
public DummyController(IActionResultHelper actionResultHelper, IUrlHelper urlHelper)
{
_actionResultHelper = actionResultHelper;
_urlHelper = urlHelper;
}
//omitted
}
Ditching the controller suffix π
Now, what if you wanted to go a step further and free yourself from the mandatory Controller suffix? Well, it’s actually not that difficult to do. Inspection whether a given Type qualifies as a controller happens inside DefaultActionDiscoveryConventions, the default IActionDiscoveryConventions implementation, inside of the IsController method. So you could either implement that interface yourself, or extend the DefaultActionDiscoveryConventions, which exposes all of its methods as virtual.
In the following example we extend the base logic with our new extra suffix - “Endpoint”.
public class EndpointActionDiscoveryConventions : DefaultActionDiscoveryConventions
{
public override bool IsController(TypeInfo typeInfo)
{
var isController = base.IsController(typeInfo);
return isController || typeInfo.Name.EndsWith("Endpoint", StringComparison.OrdinalIgnoreCase);
}
}
We could now change our class to
public class DummyEndpoint
{
//omitted
}
And it will work just fine - the only problem is that it will be publicly available as myapi.com/dummyendpoint - as the logic of stripping away the Controller suffix when exposing HTTP endpoints is embedded into ControllerDescriptor and handles only - surpise, surpise - Controller suffix.
We could remedy that by creating a custom ControllerDescriptorFactory - a factory class responsible for taking TypeInfo (desribing a controller) and producing an instance of a ControllerDescriptor out of it. The Name on the ControllerDescriptor will determine how a given endpoint is publicly visible. Unfortunately at this point ControllerDescriptor has private setters, so cannot be sublassed, and the default ControllerDescriptorFactory has no virtual members, so cannot be customized - but we can work around that with a bit of reflection.
public class EndpointControllerDescriptorFactory : IControllerDescriptorFactory
{
public ControllerDescriptor CreateControllerDescriptor(TypeInfo type)
{
var descriptor = new ControllerDescriptor(type);
if (descriptor.Name.EndsWith("Endpoint", StringComparison.Ordinal))
{
var nameProp = descriptor.GetType().GetProperty("Name");
var oldName = nameProp.GetValue(descriptor, null) as string;
nameProp.SetValue(descriptor, oldName.ToLowerInvariant().Replace("endpoint", string.Empty));
}
return descriptor;
}
}
So effectively we say, if our controller ends with “Endpoint”, we are going to strip that away.
All that’s left now is to register the new services against ServiceCollection used by the IBuilder. YOu have to do that after registering MVC, otherwise, MVC will overwrite your registrations.
app.UseServices(services =>
{
services.AddMvc();
services.AddTransient<IActionDiscoveryConventions, MyCoolActionDiscoveryConventions>();
services.AddTransient<IControllerDescriptorFactory, MyCoolIControllerDescriptorFactory>();
});
And now we can go all crazy with our custom endpoints. So now not only we go full POCO, but we even don’t need the mandatory “Controller” suffix anymore.