共用方式為


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

  1. Public Class ApiSuffixControllerFactory
  2.     Implements IHttpControllerFactory
  3.  
  4.     Private _defaultFactory As IHttpControllerFactory
  5.     Private _configuration As HttpConfiguration
  6.     Private _apiControllerTypes As Dictionary(Of String, Type)
  7.  
  8.     Public Sub New(ByVal configuration As HttpConfiguration)
  9.         Me._configuration = configuration
  10.         Me._defaultFactory = New DefaultHttpControllerFactory(configuration)
  11.     End Sub
  12.  
  13.     Private ReadOnly Property ApiControllerTypes As Dictionary(Of String, Type)
  14.         Get
  15.             If IsNothing(_apiControllerTypes) Then
  16.                 Dim types As Dictionary(Of String, Type) = New Dictionary(Of String, Type)(StringComparer.OrdinalIgnoreCase)
  17.                 For Each type As Type In Assembly.GetExecutingAssembly().GetTypes()
  18.                     If type.IsSubclassOf(GetType(ApiController)) Then
  19.                         If type.Name.EndsWith("api", StringComparison.OrdinalIgnoreCase) Then
  20.                             types.Add(type.Name.Substring(0, type.Name.Length - 3), type)
  21.                         End If
  22.                     End If
  23.                 Next
  24.  
  25.                 _apiControllerTypes = types
  26.             End If
  27.  
  28.             Return _apiControllerTypes
  29.         End Get
  30.     End Property
  31.  
  32. 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).

  1. Public Function CreateController(controllerContext As HttpControllerContext, controllerName As String) As IHttpController _
  2.     Implements IHttpControllerFactory.CreateController
  3.     Dim controllerType As Type = Nothing
  4.     Dim result As IHttpController
  5.     If Me.ApiControllerTypes.TryGetValue(controllerName, controllerType) Then
  6.         result = Me.CreateControllerInstance(controllerContext, controllerName, controllerType)
  7.     Else
  8.         result = Me._defaultFactory.CreateController(controllerContext, controllerName)
  9.     End If
  10.  
  11.     Return result
  12. End Function
  13.  
  14. Private Function CreateControllerInstance(controllerContext As HttpControllerContext, controllerName As String, controllerType As Type) _
  15.     As IHttpController
  16.     Dim descriptor As HttpControllerDescriptor
  17.     descriptor = New HttpControllerDescriptor(Me._configuration, controllerName, controllerType)
  18.     controllerContext.ControllerDescriptor = descriptor
  19.  
  20.     Dim controllerInstance As IHttpController = descriptor.HttpControllerActivator.Create(controllerContext, controllerType)
  21.     controllerContext.Controller = controllerInstance
  22.     Return controllerInstance
  23. 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.

  1. Public Sub ReleaseController(controller As IHttpController) Implements IHttpControllerFactory.ReleaseController
  2.     Me._defaultFactory.ReleaseController(controller)
  3. 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.

[Code in this post]