Supporting different controller names in ASP.NET Web APIs
The code for this post can be found in the MSDN Code Gallery.
For a controller type in the ASP.NET Web API to be accessible, it needs to follow some rules:
- Be a class
- Be public
- Be non-abstract
- Implement the IHttpController interface (usually via the ApiController class)
- End with the “Controller” (case-insensitive) suffix
That’s fairly simple, but there are always people who have a scenario where they need a controller class don’t want to follow all of those rules. This is possible to do and actually fairly simple, by using a controller factory.
The IHttpControllerFactory is the interface responsible for defining how controller classes are found in the solution, and how to map the incoming requests to those controllers. It’s also responsible for creating the actual instance of the controller and for disposing the controller instance once it’s finished processing the request (hint: if you want to implement an instancing mode other than “per call”, that’s the place to go – but that’s a topic for another post). There’s one concrete implementation of the controller factory, appropriately called DefaultHttpControllerFactory, which enforces the rules mentioned above. But that doesn’t prevent us from creating another one which implements a different set of rules (the controller will still need to implement IHttpController, though, that’s not negotiable).
In this example, I’ll show one factory which requires the controller classes to have an “Api” suffix (although still supporting classes ending with “Controller”). Let’s start with the class (the code is in VB this time for a change, but in the code gallery I’ll add a C# version as well). This factory will look for all types in the same assembly as the factory is running (change the logic in the ApiControllerTypes property for a different search place).
- Public Class ApiSuffixControllerFactory
- Implements IHttpControllerFactory
- Private _defaultFactory As IHttpControllerFactory
- Private _configuration As HttpConfiguration
- Private _apiControllerTypes As Dictionary(Of String, Type)
- Public Sub New(ByVal configuration As HttpConfiguration)
- Me._configuration = configuration
- Me._defaultFactory = New DefaultHttpControllerFactory(configuration)
- End Sub
- Private ReadOnly Property ApiControllerTypes As Dictionary(Of String, Type)
- Get
- If IsNothing(_apiControllerTypes) Then
- Dim types As Dictionary(Of String, Type) = New Dictionary(Of String, Type)(StringComparer.OrdinalIgnoreCase)
- For Each type As Type In Assembly.GetExecutingAssembly().GetTypes()
- If type.IsSubclassOf(GetType(ApiController)) Then
- If type.Name.EndsWith("api", StringComparison.OrdinalIgnoreCase) Then
- types.Add(type.Name.Substring(0, type.Name.Length - 3), type)
- End If
- End If
- Next
- _apiControllerTypes = types
- End If
- Return _apiControllerTypes
- End Get
- End Property
- End Class
The CreateController method is the one which will actually create the controller. This implementation first tries to see if the type is one supported by this class (i.e., ends in “Api”). If it’s not, it just delegates the call to the default factory. If the type is a supported one, we’ll create a new HttpControllerDescriptor, given the controller type. Finally, we’ll use the IHttpControllerActivator from the description to create the actual controller instance – we could use something like Activator.CreateInstance for simple cases, but the activator takes care of IoC parameters as well. The factory then needs to assign the instance to the controller context (I forgot to do that at first, and was getting some exceptions inside the runtime).
- Public Function CreateController(controllerContext As HttpControllerContext, controllerName As String) As IHttpController _
- Implements IHttpControllerFactory.CreateController
- Dim controllerType As Type = Nothing
- Dim result As IHttpController
- If Me.ApiControllerTypes.TryGetValue(controllerName, controllerType) Then
- result = Me.CreateControllerInstance(controllerContext, controllerName, controllerType)
- Else
- result = Me._defaultFactory.CreateController(controllerContext, controllerName)
- End If
- Return result
- End Function
- Private Function CreateControllerInstance(controllerContext As HttpControllerContext, controllerName As String, controllerType As Type) _
- As IHttpController
- Dim descriptor As HttpControllerDescriptor
- descriptor = New HttpControllerDescriptor(Me._configuration, controllerName, controllerType)
- controllerContext.ControllerDescriptor = descriptor
- Dim controllerInstance As IHttpController = descriptor.HttpControllerActivator.Create(controllerContext, controllerType)
- controllerContext.Controller = controllerInstance
- Return controllerInstance
- End Function
Finally, the ReleaseController implementation will just delegate all calls to the default factory (even those controllers handled by this type), since the default factory calls dispose on types which implement IDisposable, and that’s a nice pattern to follow.
- Public Sub ReleaseController(controller As IHttpController) Implements IHttpControllerFactory.ReleaseController
- Me._defaultFactory.ReleaseController(controller)
- End Sub
And that’s it. On the code gallery sample there are some test controllers and a client application which validates that we can call both controllers with “Controller” and with “Api” suffixes.