BFF

Using .NET 6 Minimal APIs and Darker to build BFFs

Jonny Olliff-Lee
5 min readApr 3, 2022

--

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!

--

--