Using .NET 6 Minimal APIs and Darker to build BFFs
Want to skip to the last chapter?
All the code for this is on my GitHub under the MinimalBFF repository.
Firstly, what is a BFF?
The concept of a BFF, a.k.a. Backend-For-Frontend, has been explored quite a lot by other people, so instead of rehashing articles, Microsoft’s own documentation, and other blog posts I’m going to suggest you start with one of those and come back here.
There are a lot of reasons you might want to create a BFF, most are covered in the articles above. However if you skipped them a few examples are:
- You have a new API, but you have mobile and/or desktop clients that are slow to update and still require your old API
- You have a REST API, and you want a GraphQL API in front of it
- You’re consuming a third-party API, but you want to combine that data with your own into a mixed API
What are Minimal APIs in .NET 6?
Minimal APIs are a lightweight way of defining API end points in that were introduced in .NET 6. It introduces a concept that already existed in things like Node.js, but in a .NET world. Want to learn more? Microsoft’s documentation is the place to start, or their tutorial.
Finally, what is Darker?
Darker is the query-side counterpart of the command processor Brighter. Both together can be used to implement the CQRS pattern in your .NET applications. Darker is pretty lightweight, requires minimal configuration (see further on) but has some features that help us build BFFs. You could also using something like MediatR, the only reason I haven’t is because I use Darker on a daily basis.
Where to begin?
As per the Microsoft tutorial we start off by creating a new project:
dotnet new web -o MinimalBFF
cd MinimalBFF
code -r ../MinimalBFF
Once the project has been created we need to add the relevant Darker packages, for this we’re just need to add Paramore.Darker.AspNetCore
as Paramore.Darker
is a dependency of it so we get both packages in the one.
dotnet add package Paramore.Darker.AspNetCore
Once we have Darker installed, we can start writing some code. For this example we’re going to be using our BFF to be a “dumb down” the OpenWeather API into something simple, and we’re going to pretend that we have an existing API that’s being replaced by the OpenWeather API. The OpenWeather API will be our “new” shiny API, and the BFF will be acting as our “old” API. From here on out I will be referring to them as our new API, and our old API.
Enough already, show me code!
As I mentioned at the top, all the code for this is on my GitHub so I won’t be posting full code here, only the relevant bits.
Let’s start off with our “Query” (in Darker terms) which will be the incoming request that will be in the old API format. This request will be on the query string of our API and looks something like:
?lat=51.5072&lon=0.1276
so our query will look like:
public class WeatherRequest : IQuery<IWeatherResponse>
{
public float? Lat { get; }
public float? Lon { get; }
public WeatherRequest(float? lat, float? lon)
{
Lat = lat;
Lon = lon;
}
}
Our query is implementing Darker’s IQuery<>
with the generic type IWeatherResponse
so when we execute our query we will get an instance of IWeatherResponse
back.
With our query created, we now need some code to handle it. For that we need a class that implements Darker’s IQueryHandler<,>
, which we I’ve called GetWeatherHandler
. The important part here is the ExecuteAsync
method which is doing the work, you may also notice the [ResultConverter(1)]
attribute above, but we’ll come back to that later.
[ResultConverter(1)]
public override async Task<IWeatherResponse> ExecuteAsync(WeatherRequest query, CancellationToken cancellationToken = new())
{
var (lon, lat) = (query.Lon, query.Lat);if (lon is null || lat is null)
throw new ValidationException("Lon and Lat must not be null");
var weatherResponse = await _weatherService.GetWeather(new WeatherRequest(lon, lat));return weatherResponse;
}
This method is pretty straight forward, we have some validation logic (I’ve deliberately kept this simple for this example) and then we call the GetWeather(..)
method of the IWeatherService
. This returns an instance of IWeatherResponse
which we simply return. That’s it.
You may notice that IWeatherResponse
has a IResult Result
property on it that we haven’t set yet. This will be what we return from our API ultimately. How is it set? Remember that [ResultConverter(1)]
I mentioned earlier, let’s move on to that.
This is an implementation of the Darker IQueryHandlerDecorator<>
interface. Implementations of the IQueryHandlerDecorator
are used in the Darker pipeline and ultimately wrap your instance of IQueryHandler
. So in this case we can use the returned IWeatherResponse
instance, and do something with it…like setting the Result
property.
In order to know what type of IResult
we need check the type of the IWeatherResponse
using the new(ish) switch expression:
weatherResponse.Result = result switch
{
IWeatherCreatedResponse createdResponse => Results.Created(createdResponse.ContentLocation, createdResponse),
IWeatherAcceptedResponse acceptedResponse => Results.Accepted(acceptedResponse.Location, acceptedResponse),
_ => Results.Ok(weatherResponse)
};
So if our “new” API returns a 201 response we can use our IWeatherService
to return a type that implements IWeatherCreatedResponse
, for a 202 response it would be a IWeatherAcceptedResponse
. The ResultConverter
then turns a IWeatherCreatedResponse
, IWeatherAcceptedResponse
, and IWeatherResponse
into their appropriate IResult
so CreatedResult
, AcceptedResult
, and OkResult
.
We can also handle exceptions in a similar way to handle error code responses from either our exceptions, or from response.EnsureSuccessCode()
.
That’s the guts of the code, to tie it all together we need to register Darker into the DI container, instances of IQueryHandler<,>
, and instances of IQueryHandlerDecorator<,>
. This is done using the extension methods from Paramore.Darker.AspNetCore
and to make this tidy in the Program.cs
I use ServiceExtensions.cs
.
public static void ConfigureDarker(this WebApplicationBuilder webApplicationBuilder)
{
webApplicationBuilder.Services
.AddDarker()
.AddHandlersFromAssemblies(typeof(GetWeatherHandler).Assembly)
.RegisterDecorator(typeof(ResultConverterDecorator<,>));
}
Finally we need register our endpoint that mimics the “old” API in the Program.cs
:
app.MapGet("/weather", async ([FromQuery] float? lon, [FromQuery] float? lat, [FromServices] IQueryProcessor queryProcessor)
=> (await queryProcessor.ExecuteAsync(new WeatherRequest(lon, lat))).Result);
So we’re registering the endpoint /weather
, we parse the Longitude (lon
), and Latitude (lat
) from the query string. Finally we inject the IQueryProcessor
from the DI container. The expression is making a call toqueryProcessor.ExecuteAsync(..)
and returning the Result
property.
Finishing up
That’s it, but if you have any questions just leave them in the comments below!