BFFConductor 1.4.0

dotnet add package BFFConductor --version 1.4.0
                    
NuGet\Install-Package BFFConductor -Version 1.4.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="BFFConductor" Version="1.4.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="BFFConductor" Version="1.4.0" />
                    
Directory.Packages.props
<PackageReference Include="BFFConductor" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add BFFConductor --version 1.4.0
                    
#r "nuget: BFFConductor, 1.4.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package BFFConductor@1.4.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=BFFConductor&version=1.4.0
                    
Install as a Cake Addin
#tool nuget:?package=BFFConductor&version=1.4.0
                    
Install as a Cake Tool

BFFConductor — Backend Usage Guide

BFFConductor is a .NET 8 library for ASP.NET Core BFF (Backend for Frontend) APIs. It standardizes error responses, maps business errors and exceptions to HTTP status codes, and sends display-mode hints to the client via response headers — so your frontend always knows how to present errors to users.


Companion Package

A corresponding Angular library is available on npm: bffconductor. It reads the x-handle-message-as header and dispatches error display to the appropriate UI handler on the client side.

npm install bffconductor

Installation

dotnet add package BFFConductor

Quick Start

1. Create error-mapping.json

Place this file in your project root and ensure it is copied to the output directory:

{
  "defaults": {
    "displayMode": "toast"
  },
  "mappings": [
    {
      "errorCode": "VALIDATION_FAILED",
      "httpStatus": 422,
      "displayMode": "inline"
    },
    {
      "errorCode": "NOT_FOUND",
      "httpStatus": 404,
      "displayMode": "toast"
    }
  ],
  "exceptionMappings": [
    {
      "exceptionType": "UnauthorizedAccessException",
      "httpStatus": 401,
      "displayMode": "redirect"
    },
    {
      "exceptionType": "ArgumentNullException",
      "httpStatus": 400,
      "displayMode": "toast",
      "errorCode": "VALIDATION_FAILED"
    }
  ]
}

In your .csproj:

<ItemGroup>
  <Content Include="error-mapping.json">
    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
  </Content>
</ItemGroup>

2. Register in Program.cs

builder.Services.AddBFFConductor(options =>
{
    options.MappingSpecPath = "error-mapping.json";  // default
    options.FallbackDisplayMode = "toast";           // default
});

3. Decorate Your Controllers

[ApiController]
[UseBffExceptionFilter]
[Route("[controller]")]
public class OrdersController : ControllerBase
{
    private readonly ErrorMappingRegistry _registry;

    public OrdersController(ErrorMappingRegistry registry)
        => _registry = registry;

    [HttpPost]
    public IActionResult CreateOrder([FromBody] CreateOrderRequest request)
    {
        var result = _orderService.Create(request);
        return new OperationActionResult<Order>(result, _registry);
    }
}

Core Concepts

OperationResult<T> — Service Layer Return Type

Use OperationResult<T> in your service/application layer to represent success or failure without throwing exceptions.

// Success
return OperationResult<Order>.Ok(order);

// Failure with a single error
return OperationResult<Order>.Fail("Order not found", "NOT_FOUND");

// Failure with multiple errors
return OperationResult<Order>.Fail(new[]
{
    new OperationError { Message = "Name is required", Code = "VALIDATION_FAILED" },
    new OperationError { Message = "Email is invalid", Code = "VALIDATION_FAILED" }
});

// Success with a display mode hint
return OperationResult<Order>.Ok(order, displayMode: "toast");

OperationResult<T> properties:

Property Type Description
Success bool Whether the operation succeeded
Data T? The result payload (null on failure)
Errors List<OperationError> Business errors (message + optional code)
DisplayMode string? Optional client display hint override

OperationActionResult<T> — Controller Return Type

OperationActionResult<T> converts an OperationResult<T> into an HTTP response. It:

  • Returns 200 OK on success with the data payload
  • Looks up the error code in the registry to determine HTTP status on failure
  • Sets the x-handle-message-as response header with the display mode
  • Applies [ErrorDisplay] attribute overrides (action-level beats controller-level beats registry)
[HttpPut("{id}")]
public IActionResult UpdateOrder(int id, [FromBody] UpdateOrderRequest request)
{
    var result = _orderService.Update(id, request);
    return new OperationActionResult<Order>(result, _registry);
}

Response Format

All responses use a consistent envelope:

Success (200 OK)

{
  "success": true,
  "data": { "id": 42, "status": "pending" },
  "errors": []
}
x-handle-message-as: toast

Failure (422 Unprocessable Entity)

{
  "success": false,
  "data": null,
  "errors": [
    { "message": "Name is required", "code": "VALIDATION_FAILED" }
  ]
}
x-handle-message-as: inline

Display Modes

The x-handle-message-as header tells the frontend how to display the error or success message. You define your own display mode strings — common conventions include:

Mode Description
silent Suppress UI; handle programmatically
toast Brief notification (auto-dismissing)
snackbar Bottom-of-screen notification bar
inline Show validation errors alongside form fields
modal Blocking modal dialog
page Full-page error display
redirect Redirect user (pair with x-redirect-url header)

Attributes

[UseBffExceptionFilter]

Apply to a controller to enable automatic exception handling. Unhandled exceptions are caught, matched against exceptionMappings in your JSON config, and returned as structured ApiResponse errors.

[UseBffExceptionFilter]
public class MyController : ControllerBase { }

[ErrorDisplay(errorCode, displayMode)]

Override the display mode for a specific error code at the controller or action level. The most specific match wins: action-level overrides controller-level, which overrides the global registry.

// Controller-level: all VALIDATION_FAILED errors use "toast" by default for this controller
[ErrorDisplay("VALIDATION_FAILED", "toast")]
public class MyController : ControllerBase
{
    // Action-level: overrides the controller to use "inline" for this endpoint
    [ErrorDisplay("VALIDATION_FAILED", "inline")]
    [HttpPost]
    public IActionResult Create([FromBody] CreateRequest req) { ... }
}

[ExceptionDisplay(exceptionType, displayMode)]

Override the display mode for a specific exception type at the controller or action level.

[ExceptionDisplay(typeof(UnauthorizedAccessException), "redirect")]
[HttpDelete("{id}")]
public IActionResult Delete(int id) { ... }

error-mapping.json Reference

{
  "defaults": {
    "displayMode": "toast"          // Fallback display mode for unmapped errors
  },
  "mappings": [
    {
      "errorCode": "VALIDATION_FAILED",    // Business error code string
      "httpStatus": 422,                   // HTTP status to return
      "displayMode": "inline",             // x-handle-message-as value
      "additionalHeaders": {               // Optional: extra response headers
        "x-my-header": "my-value"
      }
    }
  ],
  "exceptionMappings": [
    {
      "exceptionType": "ArgumentNullException",  // Exception class name
      "httpStatus": 400,
      "displayMode": "toast",
      "errorCode": "VALIDATION_FAILED",          // Optional: included in error response
      "additionalHeaders": {}
    }
  ]
}

Exception type matching uses inheritance: if you map Exception, it will match any unhandled exception not matched by a more specific type.


DI Registration Reference

AddBFFConductor registers the following services:

Service Lifetime Description
BffResponseOptions Singleton Configuration options
ErrorMappingRegistry Singleton Error code → HTTP status + display mode
ExceptionMappingRegistry Singleton Exception type → HTTP status + display mode
BffExceptionFilter Transient Exception filter (used by [UseBffExceptionFilter])

You can inject ErrorMappingRegistry into any controller or service that constructs OperationActionResult<T>.


Full Example

error-mapping.json

{
  "defaults": { "displayMode": "toast" },
  "mappings": [
    { "errorCode": "VALIDATION_FAILED", "httpStatus": 422, "displayMode": "inline" },
    { "errorCode": "NOT_FOUND",          "httpStatus": 404, "displayMode": "toast"   },
    { "errorCode": "CONFLICT",           "httpStatus": 409, "displayMode": "modal"   }
  ],
  "exceptionMappings": [
    { "exceptionType": "UnauthorizedAccessException", "httpStatus": 401, "displayMode": "redirect" }
  ]
}

Program.cs

builder.Services.AddBFFConductor(options =>
{
    options.MappingSpecPath = "error-mapping.json";
    options.FallbackDisplayMode = "toast";
});

app.UseCors();
app.UseHttpsRedirection();
app.MapControllers();

ProductsController.cs

[ApiController]
[UseBffExceptionFilter]
[Route("[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _service;
    private readonly ErrorMappingRegistry _registry;

    public ProductsController(IProductService service, ErrorMappingRegistry registry)
    {
        _service = service;
        _registry = registry;
    }

    [HttpGet("{id}")]
    public IActionResult Get(int id)
    {
        var result = _service.GetById(id);
        return new OperationActionResult<Product>(result, _registry);
    }

    [HttpPost]
    [ErrorDisplay("VALIDATION_FAILED", "inline")]   // override global mapping for this action
    public IActionResult Create([FromBody] CreateProductRequest request)
    {
        var result = _service.Create(request);
        return new OperationActionResult<Product>(result, _registry);
    }
}

ProductService.cs

public class ProductService : IProductService
{
    public OperationResult<Product> GetById(int id)
    {
        var product = _repo.Find(id);
        if (product is null)
            return OperationResult<Product>.Fail("Product not found", "NOT_FOUND");

        return OperationResult<Product>.Ok(product);
    }

    public OperationResult<Product> Create(CreateProductRequest request)
    {
        if (string.IsNullOrWhiteSpace(request.Name))
            return OperationResult<Product>.Fail("Name is required", "VALIDATION_FAILED");

        var product = _repo.Save(new Product(request.Name));
        return OperationResult<Product>.Ok(product);
    }
}

Interfaces

If you need to abstract over response or result types, BFFConductor exposes:

// For API responses (returned to clients)
public interface IApiResponse<out T>
{
    bool Success { get; }
    T? Data { get; }
    IReadOnlyList<ApiError> Errors { get; }
}

// For service/application layer results
public interface IOperationResult
{
    bool Success { get; }
    IReadOnlyList<OperationError> Errors { get; }
    string? DisplayMode { get; }
    object? GetData();
}

public interface IOperationResult<out T> : IOperationResult
{
    T? Data { get; }
}
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.4.0 82 3/11/2026
1.3.0 80 3/11/2026
1.2.0 87 3/11/2026
1.1.0 91 3/10/2026
1.0.0 86 3/10/2026