Using Architecture Tests for CancellationTokens and sealed classes

7/28/2025
4 minute read

Architectural Unit tests are nice to constraint your system (like which parts of your application can access other parts of your application). This blog post is not about that - that is covered a lot in the internet. No, I want to show what I did in a recent project: Checking if all controllers do use CancellationToken and some other entities are sealed.

Checking for CancellationTokens

I use the NetArchTest.Rules package to retrieve many of the information, but everything I show you here can be solely done by simply using Reflection. But I do like the fluent builder that reads very nicely.

So we are still using the classic controllers - yes how dare I! Anyhow, a classic controller might look like this:

[ApiController]
[Route("/api/permissions")]
public class PermissionsController : ControllerBase
{
    // ...

    [HttpGet]
    public async Task<IReadOnlyCollection<PermissionModel>> GetPermissionModel(CancellationToken token)

So we want to check if that controller and all others are using a CancellationToken. We can do something like this:

[Fact]
public void Controllers_ShouldHaveCancellationTokenParameters()
{
    // This of course can be done via Reflection as well - nothing magically here
    var controllerTypes = Types.InAssembly(typeof(Api.Controllers.PermissionsController).Assembly)
        .That()
        .HaveNameEndingWith("Controller")
        .And()
        .Inherit(typeof(Microsoft.AspNetCore.Mvc.ControllerBase))
        .GetTypes();

    var methodsWithoutCancellationToken = new List<string>();
    var methodsWithImproperTokenNaming = new List<string>();

    foreach (var controllerType in controllerTypes)
    {
        var actionMethods = controllerType.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
            .Where(m => m.DeclaringType == controllerType)
            .Where(m => !m.IsSpecialName)
            .Where(m => m.GetCustomAttributes(typeof(Microsoft.AspNetCore.Mvc.HttpGetAttribute), false).Length != 0 ||
                       m.GetCustomAttributes(typeof(Microsoft.AspNetCore.Mvc.HttpPostAttribute), false).Length != 0 ||
                       m.GetCustomAttributes(typeof(Microsoft.AspNetCore.Mvc.HttpPutAttribute), false).Length != 0);

        foreach (var method in actionMethods)
        {
            var parameters = method.GetParameters();
            var cancellationTokenParam = parameters.FirstOrDefault(p => p.ParameterType == typeof(CancellationToken));

            if (cancellationTokenParam is null)
            {
                methodsWithoutCancellationToken.Add($"{controllerType.Name}.{method.Name}");
            }
            else
            {
                // We could also allow here certain spelling versions 
                var allowedNames = new[] { "token", "cancellationToken" };
                if (!allowedNames.Contains(cancellationTokenParam.Name))
                {
                    methodsWithImproperTokenNaming.Add($"{controllerType.Name}.{method.Name} (parameter: {cancellationTokenParam.Name})");
                }
            }
        }
    }

    methodsWithoutCancellationToken.ShouldBeEmpty(
        $"The following controller action methods don't have CancellationToken parameters: {string.Join(", ", methodsWithoutCancellationToken)}");

    methodsWithImproperTokenNaming.ShouldBeEmpty(
        $"The following controller action methods have improperly named CancellationToken parameters: {string.Join(", ", methodsWithImproperTokenNaming)}");
}

Complimentary you can use Analyzers to make sure, that those tokens are passed down. Of course you can also force your other objects to offer a CancellationToken. With that the whole chain is secured to use the token!

Sealed classes

I while back I wrote: "Why are sealed classes faster in C#? And should I seal them?" I do love to have many things sealed by default, because it it hard to get right. Read the article for more rationale. In our codebase we have special "names"/"suffix" for stuff that goes from the backend to the frontend or vice versa (also for the database for that matter). So as DTO's shouldn't have inheritence (for me at least, you can have obivously a different opinion on this one).

[Fact]
public void ViewClasses_ShouldBeSealed()
{
    var result = Types.InAssembly(typeof(YourDbAssemblyType).Assembly)
        .That()
        .HaveNameEndingWith("View")
        .Should()
        .BeSealed()
        .GetResult();

    result.IsSuccessful.ShouldBeTrue(
        $"The following View classes are not sealed: {string.Join(", ", result.FailingTypes?.Select(t => t.Name) ?? [])}");
}

[Fact]
public void ModelClasses_ShouldBeSealed()
{
    var result = Types.InAssembly(typeof(YourDtoAssemblyType).Assembly)
        .That()
        .HaveNameEndingWith("Model")
        .Should()
        .BeSealed()
        .GetResult();

    result.IsSuccessful.ShouldBeTrue(
        $"The following Model classes are not sealed: {string.Join(", ", result.FailingTypes?.Select(t => t.Name) ?? [])}");
}
An error has occurred. This application may no longer respond until reloaded. Reload x