But I don’t want to call Web Api controllers “Controller”!

· 769 words · 4 minutes to read

A friend of mine was recently complaining about how Web API controller centric approach doesn’t really make sense, and that he prefers feature-oriented endpoints. While - in all honesty - I am not sure what he means by that, it got me thinking. Maybe, if we didn’t have to suffix Web API controllers “Controller”, that would make him happy?

More after the jump.

Why do I have to suffix my HTTP endpoints with “Controller”? 🔗

Let’s start by answering this simple question - why is this magical string “Controller” needed? Actually, the answer is as simple - because it’s hardcoded into the Web API source. The string “Controller” is checked against class names when trying to extract controller types from your API assemblies.

In fact, when discovering controllers, Web API will require the following:

    1. implements IHttpController
    1. is a public, non-abstract class
    1. has a “Controller” suffix

That of course, doesn’t mean we cannot change that.

Changing API controllers 🔗

Web API exposes for us a nice little service called IHttpControllerTypeResolver (which can be implemented from scratch) and it’s default implementation, DefaultHttpControllerTypeResolver (which can be partially overriden).

The resolver has a predicate it uses to determine controller types. We can inherit from the default service and create our own version.

Here is the default one, corresponding to the condition I just mentioned a moment ago:

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);  
}

Well, our custom one might be:

internal static bool IsHttpEndpoint(Type t)  
{  
if (t == null) throw new ArgumentNullException("t");  
return t.IsClass && t.IsVisible && !t.IsAbstract &&  
typeof(ApiEndpoint).IsAssignableFrom(t) &&  
typeof(IHttpController).IsAssignableFrom(t);  
}  

So, we get rid of any suffixing - any class name can be an HTTP endpoint - but instead we force the types to inherit from ApiEndpoint which is a theoretical base class we want to force all our API developers to implement. This base class should obviously inherit from ApiController, so that we can use the Web API pipeline correctly.

(This is just an example, of what you can do, and there is some value to it - for example I personally am forcing everyone around me to base the API off our base RestController<T>).

Of course the static method needs to sit inside some class (which, as you might expect should inherit from DefaultHttpControllerTypeResolver, since I just set we would be modifying it):

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(ApiEndpoint).IsAssignableFrom(t) && typeof(IHttpController).IsAssignableFrom(t);  
}  
}  

The service should be registered against your configuration

config.Services.Replace(typeof(IHttpControllerTypeResolver), new CustomHttpControllerTypeResolver());  

It’s almost ready now, expect we have to change one more thing, and that I suspect is an oversight from the Web API team. There is a class DefaultHttpControllerSelector, which stores a hardcoded controller suffix, “Controller”, in a public static but readonly variable.

The problem is, this value is used when caching resolved controllers, in an arbitrary substring inside an internal HttpControllerTypeCache class, in a little LINQ grouping:

var groupedByName = controllerTypes.GroupBy(  
t => t.Name.Substring(0, t.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length),  
StringComparer.OrdinalIgnoreCase);  

Since we don’t want to:
a) reimplement an internal type responsinble for caching (insane) to remove that..
b) ..we could just reset the static suffix to string.empty. However, we don’t want to reimplement DefaultHttpControllerSelector just to be able to reset a readonly field

Then we can use a bit of reflection to save us time & effort. Notice this is a static field so it needs to be changed just once, so it’s not an issue to use a bit of reflection on application startup.

So somehwere inside our WebApiConfig or in a Main (if you self host) we could do this:

var suffix = typeof(DefaultHttpControllerSelector).GetField("ControllerSuffix", BindingFlags.Static | BindingFlags.Public);  
if (suffix != null) suffix.SetValue(null, string.Empty);  

We might as well introduce a new suffix - and make sure it lines up with our precondition to discover controllers. It could be “Endpoint” or “Service”, or, for what it’s worth “Monkey”.

But since we set it to an empty string, we ignore any suffixes whatsoever.

Running it 🔗

Now, based on our new rules, if I create a new controller, I need to inherit off ApiEndpoint (could be, again, some specific ApiController variation for our team i.e. including base logging or whatnot) and I can name it any way I want.

Ok, so - and this is a message for my friend, and you know I’m talking to you, here you got ApiControllers, without “Controller”. In fact you can use a suffix “Feature” if you want 🙂

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