Controllers don't have to derive from a base class.

In most tutorials about ASP.NET web APIs you'll be told to let Controller classes derive from a base class. It may be convenient if you believe that productivity is measured by how fast you can get an initial version of the software into production. Granted, sometimes that's the case, but usually there's a price to be paid. Did you produce legacy code in the process?

One common definition of legacy code is that it's code without tests. With ASP.NET I've repeatedly found that when Controllers derive from ControllerBase they become harder to unit test. It may be convenient to have access to the Url and User properties, but this tends to make the arrange phase of a unit test much more complex. Not impossible; just more complex than I like. In short, inheriting from ControllerBase just to get access to, say, Url or User violates the Interface Segregation Principle. That ControllerBase is too big a dependency for my taste.

I already use self-hosting in integration tests so that I can interact with my REST APIs via HTTP. When I want to test how my API reacts to various HTTP-specific circumstances, I do that via integration tests. So, in a recent code base I decided to see if I could write an entire REST API in ASP.NET Core without inheriting from ControllerBase.

The short answer is that, yes, this is possible, and I'd do it again, but you have to jump through some hoops. I consider that hoop-jumping a fine price to pay for the benefits of simpler unit tests and (it turns out) better separation of concerns.

In this article, I'll share what I've learned.

POCO Controllers #

Just so that we're on the same page: A POCO Controller is a Controller class that doesn't inherit from any base class. In my code base, they're defined like this:

[Route("")]
public sealed class HomeController

or

[Authorize(Roles = "MaitreD")]
public class ScheduleController

As you can tell, they don't inherit from ControllerBase or any other base class. They are annotated with attributes, like [Route], [Authorize], or [ApiController]. Strictly speaking, that may disqualify them as true POCOs, but in practice I've found that those attributes don't impact the sustainability of the code base in a negative manner.

Dependency Injection is still possible, and in use:

[ApiController]
public class ReservationsController
{
    public ReservationsController(
        IClock clock,
        IRestaurantDatabase restaurantDatabase,
        IReservationsRepository repository)
    {
        Clock = clock;
        RestaurantDatabase = restaurantDatabase;
        Repository = repository;
    }
 
    public IClock Clock { get; }
    public IRestaurantDatabase RestaurantDatabase { get; }
    public IReservationsRepository Repository { get; }
 
    // Controller actions and other members go here...

I consider Dependency Injection nothing but an application of polymorphism, so I think that still fits the POCO label. After all, Dependency Injection is just argument-passing.

HTTP responses and status codes #

The first thing I had to figure out was how to return various HTTP status codes. If you just return some model object, the default status code is 200 OK. That's fine in many situations, but if you're implementing a proper REST API, you should be using headers and status codes to communicate with the client.

The ControllerBase class defines many helper methods to return various other types of responses, such as BadRequest or NotFound. I had to figure out how to return such responses without access to the base class.

Fortunately, ASP.NET Core is now open source, so it isn't too hard to look at the source code for those helper methods to see what they do. It turns out that they are just discoverable methods that create and return various result objects. So, instead of calling the BadRequest helper method, I can just return a BadRequestResult:

return new BadRequestResult();

Some of the responses were a little more involved, so I created my own domain-specific helper methods for them:

private static ActionResult Reservation201Created(int restaurantId, Reservation r)
{
    return new CreatedAtActionResult(
        actionName: nameof(Get),
        controllerName: null,
        routeValues: new { restaurantId, id = r.Id.ToString("N") },
        value: r.ToDto());
}

Apparently, the controllerName argument isn't required, so should be null. I had to experiment with various combinations to get it right, but I have self-hosted integration tests that cover this result. If it doesn't work as intended, tests will fail.

Returning this CreatedAtActionResult object produces an HTTP response like this:

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Location: https://example.net:443/restaurants/2112/reservations/276d124f20cf4cc3b502f57b89433f80

{
  "id""276d124f20cf4cc3b502f57b89433f80",
  "at""2022-01-14T19:45:00.0000000",
  "email""donkeyman@example.org",
  "name""Don Keyman",
  "quantity": 4
}

I've edited and coloured the response for readability. The Location URL actually also includes a digital signature, which I've removed here just to make the example look a little prettier.

In any case, returning various HTTP headers and status codes is less discoverable when you don't have a base class with all the helper methods, but once you've figured out the objects to return, it's straightforward, and the code is simple.

Generating links #

A true (level 3) REST API uses hypermedia as the engine of application state; that is, links. This means that a HTTP response will typically include a JSON or XML representation with several links. This article includes several examples.

ASP.NET provides IUrlHelper for exactly that purpose, and if you inherit from ControllerBase the above-mentioned Url property gives you convenient access to just such an object.

When a class doesn't inherit from ControllerBase, you don't have convenient access to an IUrlHelper. Then what?

It's possible to get an IUrlHelper via the framework's built-in Dependency Injection engine, but if you add such a dependency to a POCO Controller, you'll have to jump through all sorts of hoops to configure it in unit tests. That was exactly the situation I wanted to avoid, so that got me thinking about design alternatives.

That was the real, underlying reason I came up with the idea to instead add REST links as a cross-cutting concern. The Controllers are wonderfully free of that concern, which helps keeping the complexity down of the unit tests.

I still have test coverage of the links, but I prefer testing HTTP-related behaviour via the HTTP API instead of relying on implementation details:

[Theory]
[InlineData("Hipgnosta")]
[InlineData("Nono")]
[InlineData("The Vatican Cellar")]
public async Task RestaurantReturnsCorrectLinks(string name)
{
    using var api = new SelfHostedApi();
    var client = api.CreateClient();
 
    var response = await client.GetRestaurant(name);
 
    var expected = new HashSet<string?>(new[]
    {
        "urn:reservations",
        "urn:year",
        "urn:month",
        "urn:day"
    });
    var actual = await response.ParseJsonContent<RestaurantDto>();
    var actualRels = actual.Links.Select(l => l.Rel).ToHashSet();
    Assert.Superset(expected, actualRels);
    Assert.All(actual.Links, AssertHrefAbsoluteUrl);
}

This test verifies that the representation returned in response contains four labelled links. Granted, this particular test only verifies that each link contains an absolute URL (which could, in theory, be http://www.example.com), but the whole point of fit URLs is that they should be opaque. I've other tests that follow links to verify that the API affords the desired behaviour.

Authorisation #

The final kind of behaviour that caused me a bit of trouble was authorisation. It's easy enough to annotate a Controller with an [Authorize] attribute, which is fine as long all you need is role-based authorisation.

I did, however, run into one situation where I needed something closer to an access control list. The system that includes all these code examples is a multi-tenant restaurant reservation system. There's one protected resource: a day's schedule, intended for use by the restaurant's staff. Here's a simplified example:

GET /restaurants/2112/schedule/2021/2/23 HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInCI6IkpXVCJ9.eyJ...

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{
  "name""Nono",
  "year": 2021,
  "month": 2,
  "day": 23,
  "days": [
    {
      "date""2021-02-23",
      "entries": [
        {
          "time""19:45:00",
          "reservations": [
            {
              "id""2c7ace4bbee94553950afd60a86c530c",
              "at""2021-02-23T19:45:00.0000000",
              "email""anarchi@example.net",
              "name""Ann Archie",
              "quantity": 2
            }
          ]
        }
      ]
    }
  ]
}

Since this resource contains personally identifiable information (email addresses) it's protected. You have to present a valid JSON Web Token with required claims. Role claims, however, aren't enough. A minimum bar is that the token must contain a sufficient role claim like "MaitreD" shown above, but that's not enough. This is, after all, a multi-tenant system, and we don't want one restaurant's MaitreD to be able to see another restaurant's schedule.

If ASP.NET can address that kind of problem with annotations, I haven't figured out how. The Controller needs to check an access control list against the resource being accessed. The above-mentioned User property of ControllerBase would be really convenient here.

Again, there are ways to inject an entire ClaimsPrincipal class into the Controller that needs it, but once more I felt that that would violate the Interface Segregation Principle. I didn't need an entire ClaimsPrincipal; I just needed a list of restaurant IDs that a particular JSON Web Token allows access to.

As is often my modus operandi in such situations, I started by writing the code I wished to use, and then figured out how to make it work:

[HttpGet("restaurants/{restaurantId}/schedule/{year}/{month}/{day}")]
public async Task<ActionResult> Get(int restaurantId, int year, int month, int day)
{
    if (!AccessControlList.Authorize(restaurantId))
        return new ForbidResult();
 
    // Do the real work here...

AccessControlList is a Concrete Dependency. It's just a wrapper around a collection of IDs:

public sealed class AccessControlList
{
    private readonly IReadOnlyCollection<int> restaurantIds;

I had to create a new class in order to keep the built-in ASP.NET DI Container happy. Had I been doing Pure DI I could have just injected IReadOnlyCollection<int> directly into the Controller, but in this code base I used the built-in DI Container, which I had to configure like this:

services.AddHttpContextAccessor();
services.AddTransient(sp => AccessControlList.FromUser(
    sp.GetService<IHttpContextAccessor>().HttpContext.User));

Apart from having to wrap IReadOnlyCollection<int> in a new class, I found such an implementation preferable to inheriting from ControllerBase. The Controller in question only depends on the services it needs:

public ScheduleController(
    IRestaurantDatabase restaurantDatabase,
    IReservationsRepository repository,
    AccessControlList accessControlList)

The ScheduleController uses its restaurantDatabase dependency to look up a specific restaurant based on the restaurantId, the repository to read the schedule, and accessControlList to implement authorisation. That's what it needs, so that's its dependencies. It follows the Interface Segregation Principle.

The ScheduleController class is easy to unit test, since a test can just create a new AccessControlList object whenever it needs to:

[Fact]
public async Task GetScheduleForAbsentRestaurant()
{
    var sut = new ScheduleController(
        new InMemoryRestaurantDatabase(Some.Restaurant.WithId(2)),
        new FakeDatabase(),
        new AccessControlList(3));
 
    var actual = await sut.Get(3, 2089, 12, 9);
 
    Assert.IsAssignableFrom<NotFoundResult>(actual);
}

This test requests the schedule for a restaurant with the ID 3, and the access control list does include that ID. The restaurant, however, doesn't exist, so despite correct permissions, the expected result is 404 Not Found.

Conclusion #

ASP.NET has supported POCO Controllers for some time now, but it's clearly not a mainstream scenario. The documentation and Visual Studio tooling assumes that your Controllers inherit from one of the framework base classes.

You do, therefore, have to jump through a few hoops to make POCO Controllers work. The result, however, is more lightweight Controllers and better separation of concerns. I find the jumping worthwhile.


Comments

Hi Mark, as I read the part about BadRequest and NotFound being less discoverable, I wondered if you had considered creating a ControllerHelper (terrible name I know) class with BadRequest, etc?
Then by adding using static Resteraunt.ControllerHelper as a little bit of boilerplate code to the top of each POCO controller, you would be able to do return BadRequest(); again.

2021-02-03 22:00 UTC

Dave, thank you for writing. I hadn't thought of that, but it's a useful idea. Someone would have to figure out how to write such a helper class, but once it exists, it would offer the same degree of discoverability. I'd suggest a name like Result or HttpResult, so that you could write e.g. Result.BadRequest().

Wouldn't adding a static import defy the purpose, though? As I see it, C# discoverability is enabled by 'dot-driven development'. Don't you lose that by a static import?

2021-02-04 7:18 UTC


Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or somewhere else with a permalink. Ping me with the link, and I may respond.

Published

Monday, 01 February 2021 08:10:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Monday, 01 February 2021 08:10:00 UTC