0

We're using MediatR heavily in our LoB application, where we use the command & query pattern. Often, to continue in development, we make the commands and the queries first, since they are simple POCOs.

This sometimes can lead to forgetting to create an actual command handler/query handler. Since there's no compile-time validation if there is actually an implementation for the query/command, I was wondering what would be the best approach to see if there's an implementation and throw an error if not, before being able to merge into master.

My idea so far: Create a two tests, one for queries and one for commands, that scan all the assemblies for an implementation of IRequest<TResponse>, and then scan the assemblies for an associated implementation of IRequestHandler<TRequest, TResponse>

But this would make it still required to first execute the tests (which is happening in the build pipeline), which still depends on the developer manually executing the tests (or configuring VS to do so after compile).

I don't know if there's a compile-time solution for this, and even if that would be a good idea?

Mortana
  • 1,332
  • 3
  • 15
  • 29

2 Answers2

0

We've gone with a test (and thus build-time) verification; Sharing the code here for the actual test, which we have once per domain project. The mediator modules contain our query/command(handler) registrations, the infrastructure modules contain our handlers of queries;

public class MissingHandlersTests
{
    [Fact]
    public void Missing_Handlers()
    {
        List<Assembly> assemblies = new List<Assembly>();

        assemblies.Add(typeof(MediatorModules).Assembly);
        assemblies.Add(typeof(InfrastructureModule).Assembly);

        var missingTypes = MissingHandlersHelpers.FindUnmatchedRequests(assemblies);

        Assert.Empty(missingTypes);
    }
}

The helper class;

public class MissingHandlersHelpers
{      
    public static IEnumerable<Type> FindUnmatchedRequests(List<Assembly> assemblies)
    {
        var requests = assemblies.SelectMany(x => x.GetTypes())
            .Where(t => t.IsClass && t.IsClosedTypeOf(typeof(IRequest<>)))
            .ToList();

        var handlerInterfaces = assemblies.SelectMany(x => x.GetTypes())
            .Where(t => t.IsClass && (t.IsClosedTypeOf(typeof(IRequestHandler<>)) || t.IsClosedTypeOf(typeof(IRequestHandler<,>))))
            .SelectMany(t => t.GetInterfaces())
            .ToList();

        List<Type> missingRegistrations = new List<Type>();
        foreach(var request in requests)
        {
            var args = request.GetInterfaces().Single(i => i.IsClosedTypeOf(typeof(IRequest<>)) && i.GetGenericArguments().Any() && !i.IsClosedTypeOf(typeof(ICacheableRequest<>))).GetGenericArguments().First();

            var handler = typeof(IRequestHandler<,>).MakeGenericType(request, args);

            if (handler == null || !handlerInterfaces.Any(x => x == handler))
                missingRegistrations.Add(handler);

        }
        return missingRegistrations;
    }
}
Mortana
  • 1,332
  • 3
  • 15
  • 29
0

If you are using .Net Core you could the Microsoft.AspNetCore.TestHost to create an endpoint your tests could hit. Sort of works like this:

var builder = WebHost.CreateDefaultBuilder()
                .UseStartup<TStartup>()
                .UseEnvironment(EnvironmentName.Development)
                .ConfigureTestServices(
                    services =>
                    {
                        services.AddTransient((a) => this.SomeMockService.Object);
                    });

            this.Server = new TestServer(builder);
            this.Services = this.Server.Host.Services;
            this.Client = this.Server.CreateClient();
            this.Client.BaseAddress = new Uri("http://localhost");

So we mock any http calls (or any other stuff we want) but the real startup gets called.

And our tests would be like this:

public SomeControllerTests(TestServerFixture<Startup> testServerFixture)
        : base(testServerFixture)
        {
        }

        [Fact]
        public async Task SomeController_Returns_Titles_OK()
        {
            var response = await this.GetAsync("/somedata/titles");

            response.StatusCode.Should().Be(HttpStatusCode.OK);
            var responseAsString = await response.Content.ReadAsStringAsync();
            var actualResponse = Newtonsoft.Json.JsonConvert.DeserializeObject<IEnumerable<string>>(responseAsString);

            actualResponse.Should().NotBeNullOrEmpty();
            actualResponse.Should().HaveCount(20);
        }

So when this test runs, if you have not registered your handler(s) it will fail! We use this to assert what we need (db records added, response what we expect etc) but it is a nice side effect that forgetting to register your handler gets caught at the test stage!

https://fullstackmark.com/post/20/painless-integration-testing-with-aspnet-core-web-api

PaulD
  • 206
  • 1
  • 5