CraigMattson.net

MediatR vs. Minimal APIs vs. FastEndpoints

25 September 2022

In the world of .NET-based microservice development, no doubt you have come across the de-facto standard of all-things-CQRS - MediatR. It has been a great library to work with when your application stack contains multiple entry points to the same unit of code, and you really don't want to spend the time writing boilerplate reflection code to achieve similar outcomes. In some respects, it's become a bit of a hammer for me - download the library, add the references and start writing your request / response objects just as I've been writing since the first "simple" WCF replacements became available for REST. It's hard to move away from the simplicity of treating things as Requests, using some scaffolding to find the right handler and produce a Response. It's simple, it follows the way HTTP works, there's just not a lot to think about.

In .NET 6, there's been a push by Microsoft to start introducing minimal code. I was rudely introduced to this in a Console App I launched a few months ago when old mates public static void Main(string args[]) and using System; were all missing from my code. No namespace, no nothing. Just an empty screen with Console.WriteLine("Hello World");. Delving a little further, it appears that ASP.Net wasn't immune from the same smack-in-the-face change either. Just 4 lines of boilerplate code to instantiate your website removing slabs of Startup.cs and Program.cs code.

Embracing this new life of simplicity inching ever-so-much closer to the conveniences of Python (Fast) and NodeJS (expressjs), .NET 6 and a reasonably newcomer to the platform FastEndpoints might well become the second and third hammers I need to take the stickiness out of several layers of abstraction that MediatR and MVC usually requires before you can use it as intended.

A brief look at Minimal APIs

When you create your new empty ASP.Net Core website, you'll be greeted with a new option "Do not use top-level statements". This option removes the boilerplate / scaffolding code for using statements, Program.cs, Startup.cs and lets you get down to business almost immediately.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();

This is significantly less code than how you'd do this in MVC. I won't put an example here, but the above demonstrates the following:

  • Minimal APIs is initialised by default (there's no code here to activate MVC Controllers or Routes).
  • Routes are explicitly defined against their appropriate MapVerb (and thus not implied due to naming conventions or altered via Attributes).
  • No Namespaces or Class Decorators - your code should run a lot faster than initiating large Controller classes.

Getting started is simple - create some DTO objects to match your requests and responses, write minimal code here and gradually update them as you go on. If you're really embracing the microservices design, you might even find that a single Program.cs is enough for your few data manipulation services. Getting more complicated? Dependency Injection is still available, as is reflection or simple instantiation of your own abstractions will help you get there.

You can still instantiate controllers and use them where appropriate, but when working with APIs, I've never found API Controllers to be particularly intuitive. There's a lot going on simply to handle authorisation and routing, and if you're embracing MediatR / CQRS patterns - you're controllers become nothing more than conduits for mediator Send calls.

What is FastEndpoints?

FastEndpoints is the not-so-new kid on the block embracing the concept of Minimal APIs by allowing you to replace those MVC Controllers with Endpoints. When paired with "Do not use top-level statements", instantiation is super simple. You'll first need a couple of packages:

With those installed, we add the scaffolding and we're ready to write our first Endpoint.

global using FastEndpoints;
global using FastEndpoints.Swagger;

// do the services thing here
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFastEndpoints();
builder.Services.AddSwaggerDoc();
// including things like your DI

var app = builder.Build();

// do the app thing here
app.UseAuthorization();
app.UseFastEndpoints();
app.UseOpenApi();
app.UseSwaggerUi3(s => s.ConfigureDefaults());

// start!
app.Run();

Instead of the Minimal APIs approach with app.MapGet("/"), you create a typical class extending one of the following base classes:-

  • Endpoint<TRequest> or Endpoint<TRequest, TResponse>
  • EndpointWithoutRequest or EndpointWithoutRequest<TResponse>

Your TRequest and TResponse objects are Plain Old Class Objects. With this in mind, you can start scaffolding your class.

// usings for your POCO's

namespace App.Endpoints
{
    public class UpdatePocoEndpoint : Endpoint<PocoRequest, PocoResponse>
    {
        public override void Configure()
        {
            AllowAnonymous();
            Post("/poco");
            Summary(s =>
            {
                // put your swagger configurations here
                // per FastEndpoints.Swagger
            });
        }

        public override Task HandleAsync(PocoRequest req, CancellationToken ct)
        {
            // do something to update your Poco
            return SendOkAsync(new PocoResponse(), ct);
        }
    }
}

The first thing you notice is the Configure() method - this creates a semantic way of declaring the configuration. These also ways to use Attributes to achieve the same thing. More importantly, there's no expensive MVC framework, DI to be instantiated for all 'routes' and really - you're just focussing on your code.

If you're building simple request / response style APIs, give this a try - you'll remove a lot of unnecessary code while still supporting Dependency Injection through either your Resolve extensions or via your constructor as you do today.

For more details, see the comprehensive documentation on FastEndpoints.

What about MediatR?

While it's not the cleanest pattern out there, I'm a fan of MediatR. While it masquerades as the original Gang of Four 'Mediator' pattern, it  really is a glorified service locator pattern useful for mapping a series of requests and responses. In doing so, it's a useful receptacle for both Request / Response and CQRS style APIs when coupled with ASP.Net MVC Web APIs where your ApiControllers masquerade as some kind of gateway.

Once you add your MediatR library (and DependencyInjection component) via NuGet to your ASP.Net MVC Web API project, you then add MediatR to your ConfigureServices method in Startup.cs

services.AddMediatR(typeof(Startup));

In a roundabout way, MediatR extracts the executing assembly from your injected Type, and begins the hunt for things that implement: 

  • IRequest
  • IRequest<TResponse>
  • IRequestHandler<TRequest, TResponse>

That means you'll need a few components:

// queries - specify your repsonse type
public class PocoQuery : IRequest<Poco> {
    // stuff for your handler request
}

// commands (if dealing with CQRS, you don't want to return anything
public class AddPocoCommand : IRequest {
    // add your properties here    
}

// query handlers
public class PocoQueryHandler : IRequestHandler<PocoQuery, Poco> {
    // add your di and constructors

    public async Task<Poco> Handle(PocoQuery query, CancellationToken cancellationToken) {
        // return a Poco here.
    }
}

// command handlers
public class AddPocoCommandHandler : IRequestHandler<AddPocoCommand> {
    // add your di and constructors

    public async Task Handle(AddPocoCommand command, CancellationToken cancellationToken) {
        // do your writes and publishes here.
    }
}

This is where the "magic" reveals itself. All of those interfaces that you extend - and that's assuming you don't go further at creating base classes to mimic "Command" and "Query" nomenclature (e.g. public interface ICommand : IRequest { ... } and so forth). I don't mind it personally because it moves the 'mess' of Dependency Injection over to your commands - but it doesn't make it very portable, rather it tightly couples your library to the MediatR framework. Again - this is a tradeoff. If you are happy to embed MediatR into your application then you should make full use of it. It is unit testable and it can make things logically cleaner. But it's not magic.

It also makes code discoverability very tricky without third party plugins, or an aptitude in "ctrl+," exploring via naming conventions. You can also go searching for 'references in other locations' - it's still not as simple as finding the concrete implementation of an interface as you would with Dependency Injection.

Why not MediatR?

Some arguments for using MediatR is not having to write your own handlers or lists, but being totally honest - a competent developer will hammer that out as part of scaffolding. It's not an overly complicated task, and making architecture decisions like that means you need to be totally on-board with what the library offers. If you were to write your own Query Handler interface, one may assume it could look as simple as:

// query handler
public interface IQueryHandler<TQuery, TResult> {
    public abstract Task<TResult> ExecuteAsync(TQuery query, CancellationToken token);
}

// command handler
public interface ICommandHandler<TCommand> {
    public abstract Task ExecuteAsync(TCommand command, CancellationToken token);
}

You then register these in your Dependency Injection framework as follows:

// in your Startup.cs
services.AddScope<IQueryHandler<MyQuery, MyQueryResult>, QueryHandler<MyQuery, MyQueryResult>>();

// in your constructor
public class SomeWebController : ApiController {
    readonly IQueryHandler<MyQuery, MyQueryResult> handler;

    public SomeWebController(IQueryHandler<MyQuery, MyQueryResult>) { ... }
}

You can certainly see why MediatR can seem nice at first - there's less "stuff" going on, however all those handler registrations - all those interfaces you add to your commands and queries, and pointless abstractions on-top mean you end up writing the same amount of code anyway - and you don't get the same level of visibility.

If you don't want to specify all those types, you can proceed to add extras to your interfaces, and then not bind service locator patterns in your code.

If you want to automatically find all of your query handlers, then reflection can help you write a method to do exactly that - just as the MediatR "typeof(Startup)" component works.

That's where FastEndpoints may have an advantage here. Going back to why we use MediatR in our Web APIs, might have something to do with the way routing in MVC works. You tend to add lots of entrypoints - kind of like an API Gateway endpoint meaning your constructor could be totally bloated with handlers for each of your methods. That would be undesirable and messy.

However, if FastEndpoints allows you to specify your routing path (without having to instantiate full ApiControllers and adding lots of attributes to change route paths), then your DI spam will be reduced. In fact, if you need to share code or write directly into your endpoint - you'll find less lines of code and better discoverability to achieve the same.

Will I stop using MediatR? Yes - I've stopped using it for my personal projects and switched to FastEndpoints. Those also have different use cases though (e.g. if you make a Christmas Light controller - you have ~15 endpoints - not ~1,500). Either way, I'm looking forward to what Minimal APIs has to offer in upcoming releases and how much Microsoft may borrow from projects like FastEndpoints to create that experience enjoyed by node.js and Python web developers in 2022.


An Arcade Machine of Sorts

14 August 2022

About a month ago, I posted about testing out a P5 Panel I'd acquired during an end of financial year sale from one of my go-to suppliers. This weekend, I've had a chance now to do something a little more permanent with those panels by building an arcade machine. As my son becomes a little more interactive with things, it'll be a fun way to program a few little games for him to muck about with.

The first step for this build was to pull out my trusty old version of Sketchup 2017 and draw out a basic frame. Inside of this, some 60mm illuminated buttons from Amazon, 2 x P5 Panels and 2 x 165mm marine speakers will need to fit into the chassis. I made sure to also include the width of the MDF when measuring out the box to ensure we're not the 40mm short for the speakers to comfortably sit.

Drawing the box in Sketchup

I've opted to go straight lines with this, we can use a routing bit or T-moulding to clean this up later - but otherwise straight lines are going to be infinitely easier to work with here, and will support some vertical mounting later. With each of these panels determined, it was time to go to Bunnings to find some suitable MDF. In this case, it won't be exposed to weather so the ease of working with MDF over Plywood was the deciding factor here, but you could quite easily go the Plywood route if you wanted something a little more solid. I got some 1200mm x 600mm x 16mm MDF (as T-moulding usually comes in 16mm), some red spray paint and got to work cutting the panels out.

Cutting out the MDF

Yeah, that lawn really needs mowing - but given how wet it's been around here, it's been near impossible to find a good enough day that won't involve mud flinging from one side to the other. That aside, once it was all cut out and sanded back to the right size, some drilling + wood screws got the box assembled pretty easily. I used a hole saw bit with the drill to get the holes in for the arcade button - taking care to drill the pilot hole first, and cutting about 50% of the way in first before finishing from the other side. The advantage of doing this is you won't end up with any chipped wood as a result of the final bit being cut off (lesson learned from a previous arcade project!). All assembled to ensure things fit, it was time to get the wood filler to patch any holes before painting.

Checking the P5 panels and buttons all fit nicely

With the components all fitting nicely, it was time to print some joiners for the screens so that they line up as good as possible, while giving me something to screw into the chassis. Thankfully, someone had already designed some joiners so I didn't have to go out of my way to design them myself. With those on the printer, I had to make a door for the back and add some hinges. With that out of the way, it was time to paint. Now, I suck at painting - I can never get the right amount of spray paint on this. I was doing well until the final coat - but overall it's a nice colour - given my son's favourite colour right now is "James".

Spray painting the chassis

After a few coats of paint and an overnight dry, it was time to cut the holes for the speakers out by drawing a circle around 135mm diameter. Now, I don't have a protractor but I did have a ruler with a hole and a screw - so I was able to fashion something out of a ruler, marker and screw to draw a circle. With a large drill bit and a jig saw, the hole was in place and ready for installation.

Install speakers, screens and buttons - turn on the screen

Ok! This is starting to look pretty cool, still need to wire up the buttons and speakers. To do the speakers, I'd ordered a super cheap 50W amplifier board from Amazon while waiting for some slightly better ones to come along. This would allow for the marine speakers to have enough 5V juice to make some noise. Despite some crackling, the speakers are pretty good for some cheapies from DJ City. They'll do for some outdoor speakers mounted inside some resin rocks I'm planning for later in the year! I won't link the amplifier here, but any TPA3116D2 based amplifier will work here. With that wired in, I had a few USB sound cards in use here for a Raspberry Pi Zero. As the P5 Panels need the hardware pulse that is normally reserved for audio (otherwise you get lots of flickering), it's not a problem to use whatever USB sound card you might have lying around. I guess you could use HDMI audio if you can get the right converter.

For the button wiring, I had been using the rPi-P10 controller from Hanson electronics. You can of course wire this in manually if only using one chain, but this board does include some level shifters (3.3V to 5V) that will come in handy shortly. Unfortunately, the use of the level shifters means we can't detect any button presses. After jumping on the soldering iron, I'd pulled out a small PCB, several terminal connectors, used a tall-header GPIO Pin Header (to allow hat stacking), and wired on some Dupont connectors for lighting up the LEDs for the buttons individually. In hindsight, I might have replaced the buttons with some WS2811 LEDs to control the colour as well, maybe a future project than the LEDs with resistor values for 5V.

The rPi-P10 board is wired using this diagram - so reverse engineering it, I had determined I'll use the slots reserved for the second chain for the LED lights, and the third chain for buttons. I had confirmed the pinout works per the document before cutting away at pins. In the end, I had chopped 5 legs off the stacking connector so that they wouldn't pass through to the rPi-P10 hat for the buttons, and using Dupont connectors found the right R0/G0/B0/R1/B1 pins within the second connector so I could take advantage of the 5V level shifter on this board. You could simply use a level shifter and build your own board, but this crude board does the job for now. In the coming month, I'm going to give KiCad a go for this board and check out a process using JLCPCB or PCBWay to produce and send them through. It's so cheap these days to get custom PCBs, but it would have helped here to not have jump wires all over the board.

The final step was to wire it up, write some test code to ensure the buttons all work and to have some songs and pictures show up. Impressively, this set up under normal usage sits around 1A at 5V, so this whole thing is powered for several 10s of hours from a Romoss 30,000mAh battery. I had a Raspberry Pi 4 in at the time to do some debugging with (much faster to compile), but even that kept all lights lit and the matrix running at around that 1A mark (65% brightness). Obviously, the more white on the screen and loudness of the speakers all play a part, but impressive none-the-less.

5V at 1A for this setup

Suffice to say as I sit here on a lazy Sunday afternoon, it looks pretty awesome and kiddo loves it too. Hopefully it'll give my phone five minutes of peace while we can code some things for it such as some low-res PICO-8 games that make use of the buttons.

James the Red Engine on the Matrix Display
Tetris Clock might also seem interesting

« < 1 2 3 4 5 6 7 8 9 10 11 12 13 > »