Customizing controller discovery in ASP.NET Web API

Β· 1042 words Β· 5 minutes to read

One of the useful configuration features of ASP.NET Web API is that it allows you to be explicit about the assemblies into which it will look in order to discover controller types.

This is especially useful if you have assemblies residing outside of the bin folder, or if you are doing self hosting, and the controllers assemblies are not automatically loaded into the current AppDomain.

There are several hooks in the pipeline that you can plug into to achieve this goal. Let’s explore them, discussing the pros and cons of using any of these.

Custom IAssembliesResolver πŸ”—

The broadest reaching option, and first hook in the pipeline is IAssembliesResolver, with its default implementation, DefaultAssembliesResolver. Actually, we already discussed this option on the blog in the past, when Web API was still in RC version

The interface let’s you specify a list of assemblies which Web API should use to discover controller types. The interface definition is very straight forward, and the default implementation it comes with, simply looks into the AppDomain.CurrentDomain.

public interface AssembliesResolver  
{  
ICollection<Assembly> GetAssemblies();  
}  

Since all assemblies copied into the bin folder (when hosting on IIS) are loaded into the AppDomain, normally it’s enough to just copy your external assembly there.

However, in case you want to load DLL(s) from a different path (perhaps a shared network drive or a different pre-approved location) or you are self-hosting, and you don’t have the help of magical bin folder, you can easily add your assembly to the collection:

public class MyAssembliesResolver : DefaultAssembliesResolver  
{  
public override ICollection<Assembly> GetAssemblies()  
{  
ICollection<Assembly> baseAssemblies = base.GetAssemblies();  
List<Assembly> assemblies = new List<Assembly>(baseAssemblies);  
var controllersAssembly = Assembly.LoadFrom("c:/myAssymbly.dll");  
baseAssemblies.Add(controllersAssembly);  
return assemblies;  
}  
}  

Then, obviously, you need to register the resolver against your configuration:

config.Services.Replace(typeof(IAssembliesResolver), new MyAssemblyResolver());  

In this particular case we didn’t even implement the interface directly, but rather extended its default implementation. Since we add our source to the assembly source provided by default, this resolver looks both in to the AppDomain.Current and into our DLL path.

Once this solution is in place, Web API, whenever it will try to obtain list of assemblies to resolve controllers (at any point in the pipeline), will resort to this custom implementation.

Custom IHttpControllerTypeResolver πŸ”—

A bit deeper in the pipeline lies IHttpControllerTypeResolver which is responsible of taking in assemblies resolved by IAssembliesResolver and discover the types matching the predefined controller definition.

Due to such design, you can actually bypass implementing IAssembliesResolver altogether, and bunch up both assembly discovery and type discovery in a single place. This is useful if you wish to modify the rules which Web API uses to discover controller types.

The default rule set for a type to be discovered as valid API controller is as follows:

    • implements IHttpController (or inherits from ApiController)
    • is a public class
    • is a non-abstract class
    • has a “Controller” suffix

These rules are represented by the following method, found on DefaultHttpControllerTypeResolver.

internal static bool IsControllerType(Type t)  
{  
Contract.Assert(t != null);  
return  
t != null &&  
t.IsClass &&  
t.IsVisible &&  
!t.IsAbstract &&  
typeof(IHttpController).IsAssignableFrom(t) &&  
HasValidControllerName(t);  
}  

You could implement your own rules on top of that. An example implementation could extend the default DefaultHttpControllerTypeResolver as follows:

public class CustomHttpControllerTypeResolver : DefaultHttpControllerTypeResolver  
{  
public CustomHttpControllerTypeResolver()  
: base(IsHttpEndpoint)  
{}

internal static bool IsHttpEndpoint(Type t)  
{  
if (t == null) throw new ArgumentNullException("t");

return  
t.IsClass &&  
t.IsVisible &&  
!t.IsAbstract &&  
typeof(MyBaseApiController).IsAssignableFrom(t);  
}  
}  

Notice that DefaultHttpControllerTypeResolver takes in a predicate defining rules to be used to discover controllers. In our example, we drop the HasValidControllerName(t) check - and require all controllers to derive from an imaginary MyBaseApiController. This is a neat way to force all the developers in our team to inherit from it when they develop their HTTP endpoints.

Another interesting thing worth mentioning here, is that later on, in an internal class responsible for caching controllers, Web API will perform another crucial check - whether the assembly from which controllers are being is discovered is dynamic or not - and if it is, it will not be processed.

This is very important if you wish to emit assemblies dynamically that would contain controller types. If you do that, this is not an extensibility point for you and you have to dig a step deeper.

Custom IHttpControllerSelector πŸ”—

Finally, even deeper into the pipeline, you can find & implement your own IHttpControllerSelector.

There are many reasons of doing that, mainly if, as the name suggests, you want to introduce a custom mechanism of action selection. This is quite useful if you wish to override the default action selection mechanism or introduce some versioning mechanism into your API, and dispatch correct version of the action/controller based on the incoming request.

However, there are some specific cases where using IHttpControllerSelector for controller type discovery makes sense. IHttpControllerSelector runs after the cache of controllers has been established (in IHttpControllerTypeResolver). Therefore, implementing your own type discovery logic at this level, allows you to bypass any caching mechanism present at the earlier stages.

While this has little production-environment value (as it’s very inefficient), it can be sometimes useful during development.

public class BypassCacheSelector : DefaultHttpControllerSelector  
{  
private readonly HttpConfiguration _configuration;

public BypassCacheSelector(HttpConfiguration configuration)  
: base(configuration)  
{  
_configuration = configuration;  
}

public override HttpControllerDescriptor SelectController(HttpRequestMessage request)  
{  
var assembly = Assembly.LoadFile("c:/myAssembly.dll");  
var types = assembly.GetTypes(); //GetExportedTypes doesn't work with dynamic assemblies  
var matchedTypes = types.Where(i => typeof (IHttpController).IsAssignableFrom(i)).ToList();

var controllerName = base.GetControllerName(request);  
var matchedController =  
matchedTypes.FirstOrDefault(i => i.Name.ToLower() == controllerName.ToLower() + "controller");

return new HttpControllerDescriptor(_configuration, controllerName, matchedController);  
}  
}  

It obviously needs to be registered against the HttpConfiguration too:

config.Services.Replace(typeof(IHttpControllerSelector), new BypassCacheSelector(config));  

In this implementation, we load the assembly manually, and then resolve the controller types by looking through all ExportedTypes. Doing it at this level, will force the assembly to be reloaded and rescanned at every request, which now allows us to freely recompile that assembly and see the changes reflected upon next request, as there is no controller caching in place anymore.

As briefly mentioned before, if you use dynamic assemblies (generated i.e. using CodeDom or Roslyn) this is the route you have to take - because otherwise Web API will ignore your dynamic assemblies.

For more information on the topic of dynamic compilation I encourage you read one of the older posts about Roslyn and Web API.

About


Hi! I'm Filip W., a software architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP