ASP.NET Core 7: Implementing the Back-End for Front-End (BFF) Pattern in ASP.NET Core

In my previous article, we have seen an implementation of the API Gateway in ASP.NET Core using Ocelot, in this article we will see an implementation of the Back-End for Front-End (BFF) pattern implementation in ASP.NET Core. With an expansion in Web-based applications, there are several technologies that have emerged to support web-based apps. In recent years we have shifted from thick-client applications to thin-client applications those interact with various Back-End services. There are several domains that have Web-based User interfaces and mobile-based user interfaces e.g. e-commerce apps, Banking, and Social Media Apps along with these UIs such applications also have complex server-side architectures, and these applications address such complex architectures using Microservices as backend services. If the UI is tightly coupled with backend services, then it is exceedingly difficult to use such backend services for other UI. This is where the BFF pattern comes in.

Introduction to Back-End for Front-End (BFF) Pattern

The BFF pattern proposes a server-side component for the front-end application for enhancements in the User experience. The BFF consists of multiple back-ends so that the specific UI requirements of the front-end can be addressed. The BFF can be more useful when the front-end app wants to show data combined and processed from multiple back-end services. The advantage of this approach is that the front-end need not make various calls to multiple back-end services and write the logic to process the received data, instead, the BFF takes care of managing call to various back-end services and process the received data and delivers the data in exact in the same shape as demanded by the front-end. In this manner, we can say that the BFF pattern is a variant of the API Gateway pattern where each front-end application has its own BFF service. 

The BFF pattern is good when the application has multiple client interfaces with diverse needs and these client interfaces depend on the same underlying resources for data. Additionally, the BFF is implemented by the same team that works with Client interfaces. This team knew about the data requirements for the user interfaces and their logical approaches.   

Figure 1 explains the working of the BFF Pattern



Figure 1: The BFF Pattern Illustration          

As shown in Figure 1, we have a Mobile Client that wants to receive data from the Manufacturer and the Product Service to display the product catalog, In this case instead of making separate requests to each service, we can have a BFF service that accepts a single request from the Mobile client and underline it makes and manage multiple requests to downstream Product and Manufacturer services to fetch and process data as per demanded by the Mobile client's UI.     

The BFF pattern's design goal is to decouple front-end applications and back-end applications to use the API without receiving unnecessary data from the back-end services and without making unnecessary calls to back-end services from client applications. When the front-end receives unnecessary data then it is known as Over-Fetching, similarly, making multiple requests to back-end services to fetch data is called Over-requesting.  The BFF is the solution to handle these issues.

Advantages of BFF Pattern

  • Applications are easy to maintain since back-end services and front-end are implemented on the principle of separation of concerns.
  • Only the data requested from the client application is responded to by the BFF so the sensitive information can be secured.
  • Since the front-end team is responsible for implementing the BFF, they need not wait for the back-end team to release the changes requested for the front-end data requirements. This is especially useful because there may be multiple client applications, and each may have unique needs for data from the same back-end services.  
  • The overall speed of development is improvised because the BFF is the responsibility of the front-end team.
Design guidelines for BFF pattern

  • The BFF should contain the presentation logic. This will contain all logic for data manipulation as per the UI requirements for the front-end applications.
  • The BFF can contain the Domain Logic and hence this will off-load the front-end application containing the domain logic.
The Implementation

The code for this article is implemented using Visual Studio 2022 Enterprise Edition and .NET 7 SDK.

Step 1: Open Visual Studio and create a Blank Solution and name this solution BFF. In this solution add ASP.NET Core API Project and name it as Com.Manufacturer.Service.   

Step 2: In this project add a new folder as Models. In this folder add a class file and name it as Manufacturer.cs. In this file add the code for Manufacturers class as shown in Listing 1:


 public class Manufacturers
 {
     public int ManufacturerId { get; set; } = 0;
     public string? ManufacturerName { get; set; }
     public int ContactNo { get; set; } = 0;
     public string? City { get; set; }
 }

Listing 1: The Manufacturers class

In the same Models folder add a new class file and name it as Database.cs. In this class file, we will add code for creating hard-coded Manufacturer data by creating the ManufacturereDB class. (You can use the database instead of this). The code is provided in Listing 2


 public class ManufacturerDB : List<Manufacturers>
 {
     public ManufacturerDB()
     {
         Add(new Manufacturers() { ManufacturerId = 1, ManufacturerName = "MS-Tech", ContactNo = 123456, City = "Pune"});
         Add(new Manufacturers() { ManufacturerId = 2, ManufacturerName = "LS-Tech", ContactNo = 223456, City = "Pune" });
         Add(new Manufacturers() { ManufacturerId = 3, ManufacturerName = "TS-Tech", ContactNo = 323456, City = "Pune" });
         Add(new Manufacturers() { ManufacturerId = 4, ManufacturerName = "KK-Tech", ContactNo = 423456, City = "Pune" });
         Add(new Manufacturers() { ManufacturerId = 5, ManufacturerName = "SP-Tech", ContactNo = 523456, City = "Pune" });
         Add(new Manufacturers() { ManufacturerId = 6, ManufacturerName = "VP-Tech", ContactNo = 623456, City = "Pune" });
     }
 }

Listing 2: The ManufacturereDB class   

Step 3: In this project add a new folder and name it as Repositories. In this folder add a new interface file and name it as IRepository.cs. In this file, we will create a repository interface of which code is shown in Listing 3

public interface IRepository
{
    public IEnumerable<Manufacturers> GetManufacturers();
}

Listing 3: The Repository interface

Let's implement this interface in the RepositoryService class by adding a new class file in the Repositories folder. The code of the RepositoryService class is provided in Listing 4


public class RepositoryService : IRepository
{
    ManufacturerDB db;
    public RepositoryService()
    {
        db = new ManufacturerDB();
    }
    IEnumerable<Manufacturers> IRepository.GetManufacturers()
    {
        return db;
    }
}

Listing 4: The RepositoryService class

Step 4: We will modify the Program.cs to register the RepositoryService class in the dependency container and also will add the CORS policy for this service project. We will also manage the JSON serialization of the response from this service. The code is shown in Listing 5


using Com.Manufacturer.Service.Repositories;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddScoped<IRepository, RepositoryService>();

builder.Services.AddCors(options=>options.AddPolicy("cors",
    policy=>policy
    .AllowAnyHeader()
    .AllowAnyMethod()
    .AllowAnyOrigin()));

builder.Services.AddControllers()
    .AddJsonOptions(options=>options.JsonSerializerOptions.PropertyNamingPolicy = null);

var app = builder.Build();

// Configure the HTTP request pipeline.

app.UseHttpsRedirection();
app.UseCors("cors");
app.UseAuthorization();

app.MapControllers();

app.Run();

Listing 5: The Program.cs      

Step 5: In the Controllers folder add a new empty API Controller class and name it as ManufacturersController.cs. In this controller, we will inject the IRepository interface using constructor injection. This controller contains the HttpGet method to return Manufacturers data. Listing 6 shows the code for the controller.


[Route("api/[controller]")]
[ApiController]
public class ManufacturersController : ControllerBase
{
    IRepository manuServ;
    public ManufacturersController(IRepository serv)
    {
        manuServ = serv;
    }

    [HttpGet]
    public IActionResult Get()
    {
        return Ok(manuServ.GetManufacturers());
    }
}

Listing 6: The ManufacturereController class

Step 6: Change the endpoint port of the Manufacturer Service project to 8001 as shown in Figure 2   



Figure 2: The Port change for the Manufacturer Service

Run the project and test the Manufacturer API on port http://localhost:8001/api/Manufacturers

The Product Service

Step 7: In the BFF Solution, add a new ASP.NET Core API project and name it as Com.Product.Service. In this project folder, add Models and Repositories folders. 

Step 8: In the Models folder, add a class file named Products.cs and in this class file, add the code for the Products class as shown in Listing 7      


 public class Products
 {
     public int ProductId { get; set; } = 0;
     public string? ProductName { get; set; }
     public string? Specifications { get; set; }
     public decimal Price { get; set; } = 0;
     public int ManufacturerId { get; set; } = 0;
 }

Listing 7: The Products class

In the Models folder, add a new class file named Database.cs. In this class file, we will add code for ProductsDb class which will have hardcoded the Products data. The code for ProductsDb data is shown in Listing 8


 public class ProductDb : List<Products>
 {
     public ProductDb()
     {
         Add(new Products() { ProductId = 1001, ProductName = "Laptop", Specifications = "32 GB Gaming Laptop", Price = 450000, ManufacturerId = 1});
         Add(new Products() { ProductId = 1002, ProductName = "Desktop", Specifications = "16 GB Workstation", Price = 50000, ManufacturerId = 2 });
         Add(new Products() { ProductId = 1003, ProductName = "RAM", Specifications = "32 GB DDR 4", Price = 45000, ManufacturerId = 3 });
         Add(new Products() { ProductId = 1004, ProductName = "Printer", Specifications = "Lazer Printer", Price = 4500, ManufacturerId = 4 });
         Add(new Products() { ProductId = 1005, ProductName = "Memory Card", Specifications = "128 GB Storage", Price = 2500, ManufacturerId = 1 });
         Add(new Products() { ProductId = 1006, ProductName = "DVD Writer", Specifications = "100 MBPS Fast Writer", Price = 1500, ManufacturerId = 2 });
         Add(new Products() { ProductId = 1007, ProductName = "Power Adapter", Specifications = "64 KVA 18 Hrs. Backup", Price = 500, ManufacturerId = 3 });
         Add(new Products() { ProductId = 1008, ProductName = "Keyboard", Specifications = "105 Keys Gaming", Price = 1500, ManufacturerId = 4 });
         Add(new Products() { ProductId = 1009, ProductName = "Mouse", Specifications = "Wired USB Mouse", Price = 500, ManufacturerId = 1 });
         Add(new Products() { ProductId = 1010, ProductName = "Hard Disk", Specifications = "5 TB Fast Storage", Price = 15000, ManufacturerId = 2 });
         Add(new Products() { ProductId = 1011, ProductName = "USB Drive", Specifications = "64 GB Storage", Price = 5000, ManufacturerId = 3 });
         Add(new Products() { ProductId = 1012, ProductName = "Monitor", Specifications = "52 Inches UHD", Price = 3000, ManufacturerId = 4 });
         Add(new Products() { ProductId = 1013, ProductName = "Laptop", Specifications = "64 GB Gaming Laptop with Developer Workstation", Price = 550000, ManufacturerId = 1 });
         Add(new Products() { ProductId = 1014, ProductName = "Desktop", Specifications = "512 GB RAM Server Machine", Price = 35000, ManufacturerId = 2 });
         Add(new Products() { ProductId = 1015, ProductName = "Keyboard", Specifications = "120 Keys Optical External Keyboard", Price = 1700, ManufacturerId = 3 });
         Add(new Products() { ProductId = 1016, ProductName = "Mouse", Specifications = "Optical Gaming Mouse", Price = 1800, ManufacturerId = 4 });
         Add(new Products() { ProductId = 1017, ProductName = "RAM", Specifications = "32 GB DDR 6 RAM", Price = 7500, ManufacturerId = 1 });
         Add(new Products() { ProductId = 1018, ProductName = "Hard Disk", Specifications = "6 TB Fast Storage", Price = 6300, ManufacturerId = 2 });
         Add(new Products() { ProductId = 1019, ProductName = "Power Adapter", Specifications = "24 Hrs. Backup for Servers", Price = 9500, ManufacturerId = 3 });
         Add(new Products() { ProductId = 1020, ProductName = "DVD Writer", Specifications = "Fast Blue Ray Writer", Price = 3700, ManufacturerId = 4 });
         Add(new Products() { ProductId = 1021, ProductName = "Desktop", Specifications = "Server Machines", Price = 85000, ManufacturerId = 1 });
         Add(new Products() { ProductId = 1022, ProductName = "Printer", Specifications = "4-In-1 Printer", Price = 6400, ManufacturerId = 2 });
         Add(new Products() { ProductId = 1023, ProductName = "Router", Specifications = "1000/Mbps Fast Messager", Price = 1200, ManufacturerId = 3 });
         Add(new Products() { ProductId = 1024, ProductName = "Blade Server", Specifications = "Mult-Vm Storage", Price = 4650000, ManufacturerId = 4 });
     }
 }

Listing 8: The ProductsDb class

Step 9: In the Repositories folder, add an interface file named IRepository.cs we the repository interface code in it. The code for this interface is shown in Listing 9


public interface IRepository
{
    IEnumerable<Products.Service.Models.Products> GetProducts();
}

Listing 9: The repository interface

We will implement this interface in the ProductService class by adding the new class file named ProductService.cs in the Repositories folder. The code for this class is shown in Listing 10


public class ProductService : IRepository
{
    ProductDb db;

    public ProductService()
    {
        db = new ProductDb();
    }

    IEnumerable<Models.Products> IRepository.GetProducts()
    {
        return db;
    }
}

Listing 10: The ProductService class

Step 10: Modify the Program.cs file to register the ProductService class in dependency container, we will also add the CORS policy and JSON serialization settings as shown in Listing 11


using Com.Products.Service.Repositories;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddScoped<IRepository, ProductService>();
builder.Services.AddCors(options => options.AddPolicy("cors",
    policy => policy
    .AllowAnyHeader()
    .AllowAnyMethod()
    .AllowAnyOrigin()));
builder.Services.AddControllers().AddJsonOptions(options=>options.JsonSerializerOptions.PropertyNamingPolicy = null);

var app = builder.Build();

// Configure the HTTP request pipeline.

app.UseHttpsRedirection();
app.UseCors("cors");
app.UseAuthorization();

app.MapControllers();

app.Run();

Listing 11: The Program.cs file code for CORS and Dependency registration

Step 11: In the Controllers folder, add a new Empty API Controller and name it as ProductsController.cs. This controller will inject the IRepository interface using the constructor injection. The HttpGet method is used to return the Products response. The code for this controller is shown in Listing 12


[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
    IRepository prdServ;

    public ProductsController(IRepository serv)
    {
        prdServ = serv;
    }

    [HttpGet]
    public IActionResult Get() 
    {
        return Ok(prdServ.GetProducts());
    }
}

Listing 12: The ProductsController class

As shown in Figure 2, change the endpoint port for the ProductService to 8002. Run the project and test the Product Service by requesting it using http://localhost:8002/api/Products URL.

The Blazor WebAssembly Client Application

In the BFF solution, we will add a Blazor WebAssembly client application. This client application will access the Manufacturer and the Product services. 

Step 12: In the solution, add a new Blazor WebAssembly application and name it as Com.Blazor.UI. In this project, we will add the Models and Domain folder. In the Models folder, add the Manufacturer and Products classes as shown in Listing 1 and Listing 7 respectively. We will be using these classes to handle the Manufacturer and Products data received from the Manufacturer and Products services. 

Step 13: In the Models folder, add a new class file named ProductCatelog.cs. In this class file, we will add the code for ProductCatelog class. This class will be used to show the ProductCatelog as per the need from the Blazor UI. The code of the ProductCatelog is shown in Listing 13.


public class ProductCatalog
{
    public string? ProductName { get; set; }
    public string? ManufacturerName { get; set; }
    public string? Description { get; set; }
    public decimal Price { get; set; } = 0;
    public decimal Tax { get; set; } = 0;
    public decimal TotalPrice { get; set; } = 0;
}

Listing 13: The ProductCatelog class

Step 14: The Blazor client application shows the Product Catalog data as ProductName, ManufacturereName, Description, Price, Tax, and TotalPrice. To show this information the Blazor Application must make HTTP requests to the Manufacturer and Service and then the Blazor application has to manipulate the received data so that the data can be shown on the UI. To do this, in the Domain folder add the new class file and name it as Logic.cs. In this class file, add the code for Logic class as shown in Listing 14.


using Com.Blazor.UI.Models;
using System.Net.Http.Json;

namespace Com.Blazor.UI.Domain
{
    public class Logic
    {
        HttpClient client;
        public Logic(HttpClient client)
        {
            this.client = client;
        }

        public async Task<IEnumerable<ProductCatalog>> GetProductCatalog()
        {
            List<ProductCatalog> productsCatalog = new List<ProductCatalog>();

            /* Get The Manufacturers */
            List<Manufacturers>? manufacturers = await client.GetFromJsonAsync<List<Manufacturers>>("http://localhost:8001/api/Manufacturers");

            /* Get The Products */

            List<Products>? products = await client.GetFromJsonAsync<List<Products>>("http://localhost:8002/api/Products");


            /* Generate Products Catalog */

            productsCatalog = (from manufacturer in manufacturers
                               from product in products
                               where manufacturer.ManufacturerId == product.ManufacturerId
                               select new ProductCatalog()
                               {
                                   ManufacturerName = manufacturer.ManufacturerName,
                                   ProductName = product.ProductName,
                                   Description = product.Specifications,
                                   Price = product.Price,
                                   Tax = Convert.ToDecimal(0.25) * product.Price,
                                   TotalPrice = (Convert.ToDecimal(0.25) * product.Price) + product.Price
                               }).ToList();

            return productsCatalog;
        }
    }
}

Listing 14: The Logic for Processing the data

If you carefully look at the code in Listing 14, we can see that the GetProductCatelog() method makes a call to the Manufacturer and Products service to fetch the data. Once the response is received the LINQ is used to read data received from both responses and generate records for the ProductCatelog by calculating tax and total price.

Step 15: Modify the Program.cs file to register the Logic class in dependency injection container as shown in Listing 15.


......
builder.Services.AddScoped<Logic>();
......

Listing 15: The registration of the Logic class in Dependency Injection Container  


Step 16: In the Pages folder, modify the FetchData.razor component to call the Logic class and its GetProductCatalog() method as shown in Listing 16


@page "/fetchdata"
@inject HttpClient Http
@inject Logic logic
@using Com.Blazor.UI.Models
@using Com.Blazor.UI.Domain

<PageTitle>Product Catalog</PageTitle>

<h1>Product Catalog</h1>

<p>This component shows the Products Catalog.</p>

@if (products == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Product Name</th>
                <th>Manufacturer Name</th>
                <th>Description</th>
                <th>Price</th>
                <th>Tax</th>
                <th>Total Price</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var product in products)
            {
                <tr>
                   <td>@product.ProductName</td>
                   <td>@product.ManufacturerName</td>
                   <td>@product.Description</td>
                   <td>@product.Price</td>
                   <td>@product.Tax</td>
                   <td>@product.TotalPrice</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private IEnumerable<ProductCatalog>? products;

    protected override async Task OnInitializedAsync()
    {
        products = await logic.GetProductCatalog();         
    }

}

Listing 16: The FetchData Component

The code in Listing 16 shows that Logic and HttpClient classes are injected into the component. When the component is loaded the GetProductsCatelog() method from the Logic class is called which further makes HTTP calls to Manufacturer and Products services. After processing the received data, the ProductCatelog information is received, and the data is shown on the UI. 

Modify the solution properties and run API Services and Blazor application as shown in Figure 3



Figure 3: The Solution Properties

Run the solution. Once all services and Blazor applications are loaded in browser, click on the Fetch Data link. This will show the ProductCatalog in the browser as shown in Figure 4



Figure 4: TheProductCatalog

This implementation does not use BFF. The drawback of this approach is that the client application contains code for HTTP calls to back-end services and the necessary logic to process data as per the UI requirement hence if there is a new service on the back-end is added then the client application needs the code changes. This issue can be solved using the BFF pattern.

The BFF Pattern Implementation

Step 17: In the solution add a new ASP.NET Core API project and name it as Com.BFF.Service. Now move the Models and Domain folders those we have created in Blazor application to this BFF Service project and change namespaces for all classes in Models and Domain folders. So now the BFF Service manages all underline HTTP calls to the Manufacturer and the Product Services and process the received data. 

Step 18: Modify the Project.cs file to register the Logic class in dependency container and we will also register the CORS service and the CORS middleware as shown in Listing 17

using Com.BFF.Service.Domain;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
 
builder.Services.AddScoped<Logic>();
builder.Services.AddCors(options => options.AddPolicy("cors",
    policy => policy
    .AllowAnyHeader()
    .AllowAnyMethod()
    .AllowAnyOrigin()));
builder.Services.AddControllers()
     .AddJsonOptions(options=>options.JsonSerializerOptions.PropertyNamingPolicy=null);

var app = builder.Build();

// Configure the HTTP request pipeline.

app.UseHttpsRedirection();
app.UseCors("cors");
app.UseAuthorization();

app.MapControllers();

app.Run();


Listing 17: The Program.cs modification    

Step 19: In the Controllers folder, add a new empty API Controller and name it as ProductCatalogBFFController.cs. We will inject the Logic class using the constructor injection in this controller class. In this controller we will add the HttpGet Method to accept request for generating the ProductCatalog. The code for the controller is shown in Listing 18


[Route("api/[controller]")]
[ApiController]
public class ProductCatalogBFFController : ControllerBase
{
    Logic logic;

    public ProductCatalogBFFController(Logic logic)
    {
        this.logic = logic;
    }

    [HttpGet]
    public async Task<IActionResult> Get()
    {
        var productCatalog = await logic.GetProductCatalog();
        return Ok(productCatalog);
    }
}

Listing 18: The ProductCatalogBFFController

Expose the BFF Service on port 8000 as explained in Figure 2.


Adding a new Razor Component in Blazor Client App

Step 20: In the Blazor application, add a new Razor Component in the Pages folder and name it as BFFDataFetch. In this component, add the code for inject the HttpClient class. Using the HttpClient class the HTTP request is made to the BFF Service which is hosted on port 8000. The code for the component is shown in Listing 19.


@page "/bffdatafetch"
@using Com.Blazor.UI.Models;
@inject HttpClient httpClient

<PageTitle>Product Catalog with Back-End for Front-End Pattern</PageTitle>

<h1>Product Catalog Back-End for Front-End Pattern</h1>

<p>This component shows the Products Catalog usign Back-End for Front-End Pattern</p>
@if (products == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Product Name</th>
                <th>Manufacturer Name</th>
                <th>Description</th>
                <th>Price</th>
                <th>Tax</th>
                <th>Total Price</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var product in products)
            {
                <tr>
                    <td>@product.ProductName</td>
                    <td>@product.ManufacturerName</td>
                    <td>@product.Description</td>
                    <td>@product.Price</td>
                    <td>@product.Tax</td>
                    <td>@product.TotalPrice</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private IEnumerable<ProductCatalog>? products;

    protected override async Task OnInitializedAsync()
    {
        products = await httpClient
            .GetFromJsonAsync<IEnumerable<ProductCatalog>>("http://localhost:8000/api/ProductCatalogBFF");
    }
}

Listing 19: The Razor Component                           

Modify the NavMenu.razor component from the Shared folder to define link for BFFDataFetch component as shown in Listing 20


  <div class="nav-item px-3">
      <NavLink class="nav-link" href="bffdatafetch">
          <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data with BFF
      </NavLink>
  </div>

Listing 20: The NavMenu for the BFF Razor Component link

Step 21: Modify the Solution Properties to Run all projects as shown in Figure 5



Figure 5: The Solution Properties to run all projects in the solution

Run app projects and Click on the Fetch data with BFF link, this will make call to BFF Service and the data will be shown on the Component as shown in Figure 6



Figure 6: The BFF Component showing data  

The Code for this article can be downloaded from this link.

Conclusion:  The BFF Pattern is a variant of API Gateway pattern. This pattern make sure that the Front-End can be decoupled from the Back-End Services. The BFF Contains all domain code which is required as per the UI requirements in the Front-End.                            

Popular posts from this blog

Uploading Excel File to ASP.NET Core 6 application to save data from Excel to SQL Server Database

ASP.NET Core 6: Downloading Files from the Server

ASP.NET Core 6: Using Entity Framework Core with Oracle Database with Code-First Approach