Endpoint Filters in ASP.NET Core Minimal API

Endpoint Filters in ASP.NET Core Minimal API

In this article, I will discuss How to Implement Endpoint Filters in ASP.NET Core Minimal API with examples. Please read our previous articles discussing ASP.NET Core Minimal API using Entity Framework Core (EF Core) with Examples. We will be working with the same project as we worked so far with ASP.NET Core Minimal API.

Endpoint Filters in ASP.NET Core Minimal API

Implementing Endpoint Filters in ASP.NET Core Minimal API is a way to handle cross-cutting concerns such as logging, exception handling, authorization, and custom behaviors without making any changes to our endpoint logic. These filters intercept requests and responses, enabling centralized handling of logic without duplicating code in each endpoint handler. Endpoint filters allow you to execute code before or after the endpoint handler.

Create Custom Logging Endpoint Filter

This filter logs requests and responses. So, create a class file named LoggingFilter.cs within the Models folder and then copy and paste the following code. You can see the class implements the IEndpointFilter interface. The following code is self-explained, so please read the comment lines for a better understanding.

namespace MinimalAPIDemo.Models
{
    // Define the LoggingFilter class that implements the IEndpointFilter interface
    public class LoggingFilter : IEndpointFilter
    {
        // Declare a private readonly field for the ILogger<LoggingFilter> dependency
        private readonly ILogger<LoggingFilter> _logger;

        // Constructor for the LoggingFilter class that accepts an ILogger<LoggingFilter> parameter
        public LoggingFilter(ILogger<LoggingFilter> logger)
        {
            // Initialize the _logger field with the provided ILogger<LoggingFilter> instance
            _logger = logger;
        }

        // Implement the InvokeAsync method from the IEndpointFilter interface
        // This method is called when the filter is applied to an endpoint
        public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
        {
            // Log information about the incoming request using the logger
            _logger.LogInformation("Handling request: {RequestPath}", context.HttpContext.Request.Path);

            // Call the next filter or endpoint in the pipeline and wait for the result
            var result = await next(context);

            // Log information about the completed request using the logger
            _logger.LogInformation("Finished handling request: {RequestPath}", context.HttpContext.Request.Path);

            // Return the result of the next filter or endpoint in the pipeline
            return result;
        }
    }
}
Creating Custom Exception Handling Endpoint Filter

Logs unhandled exceptions and returns a standardized error response. So, create a class file named ExceptionHandlingFilter.cs within the Models folder and then copy and paste the following code. You can see the class implements the IEndpointFilter interface. The following code is self-explained, so please read the comment lines for a better understanding.

namespace MinimalAPIDemo.Models
{
    // Define the ExceptionHandlingFilter class that implements the IEndpointFilter interface
    public class ExceptionHandlingFilter : IEndpointFilter
    {
        // Declare a private readonly field for the ILogger<ExceptionHandlingFilter> dependency
        private readonly ILogger<ExceptionHandlingFilter> _logger;

        // Constructor for the ExceptionHandlingFilter class that accepts an ILogger<ExceptionHandlingFilter> parameter
        public ExceptionHandlingFilter(ILogger<ExceptionHandlingFilter> logger)
        {
            // Initialize the _logger field with the provided ILogger<ExceptionHandlingFilter> instance
            _logger = logger;
        }

        // Implement the InvokeAsync method from the IEndpointFilter interface
        // This method is called when the filter is applied to an endpoint
        public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
        {
            try
            {
                // Call the next filter or endpoint in the pipeline and return the result
                return await next(context);
            }
            catch (Exception ex)
            {
                // Log the exception using the logger
                _logger.LogError(ex, "An unhandled exception occurred while processing the request");

                // Return a standardized error response
                return Results.Problem("An unexpected error occurred. Please try again later.");
            }
        }
    }
}
Apply Filters to Endpoints in the Program.cs File:

Please modify the Program.cs file as follows to use the endpoint filters. At the end of the endpoint method, we apply the Custom endpoint filters using the AddEndpointFilter generic method. We have applied filters directly to the endpoints using AddEndpointFilter<LoggingFilter>() and AddEndpointFilter<ExceptionHandlingFilter>().

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MinimalAPIDemo.Models;

// Create a builder for the web application
var builder = WebApplication.CreateBuilder(args);

// Configure logging
builder.Logging.ClearProviders(); // Clear default providers
builder.Logging.AddConsole(); // Add Console logging provider
builder.Logging.AddDebug(); // Add Debug logging provider

// Add services to the DI container
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Register EmployeeService in the DI container
builder.Services.AddScoped<IEmployeeService, EmployeeService>();

// Add services to the container.
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// Build the application
var app = builder.Build();

// Configure the HTTP request pipeline for the development environment
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

// CRUD operations for Employee model
// The EmployeeService and ILogger Services are injected into the endpoints

// Endpoint to retrieve all employees
app.MapGet("/employees", async (IEmployeeService employeeService, ILogger<Program> logger) =>
{
    try
    {
        logger.LogInformation("Retrieving all employees");
        var employees = await employeeService.GetAllEmployeesAsync();
        return Results.Ok(employees);
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "An error occurred while retrieving all employees");
        return Results.Problem(ex.Message);
    }
})
.AddEndpointFilter<LoggingFilter>()
.AddEndpointFilter<ExceptionHandlingFilter>();

// Endpoint to retrieve a single employee by their ID
app.MapGet("/employees/{id}", async (int id, IEmployeeService employeeService, ILogger<Program> logger) =>
{
    try
    {
        logger.LogInformation("Retrieving employee with ID {Id}", id);
        var employee = await employeeService.GetEmployeeByIdAsync(id);
        if (employee == null)
        {
            logger.LogWarning("Employee with ID {Id} not found", id);
            return Results.NotFound(new { Message = $"Employee with ID {id} not found" });
        }
        return Results.Ok(employee);
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "An error occurred while retrieving employee with ID {Id}", id);

        var problemDetails = new ProblemDetails
        {
            Status = 500,
            Title = "An unexpected error occurred.",
            Detail = ex.Message,
            Instance = $"/employees/{id}"
        };

        return Results.Problem(ex.Message);
    }
})
.AddEndpointFilter<LoggingFilter>()
.AddEndpointFilter<ExceptionHandlingFilter>();

// Endpoint to create a new employee with validation
app.MapPost("/employees", async (Employee newEmployee, IEmployeeService employeeService, ILogger<Program> logger) =>
{
    try
    {
        if (!ValidationHelper.TryValidate(newEmployee, out var validationResults))
        {
            // Return 400 Bad Request if validation fails
            return Results.BadRequest(validationResults);
        }

        logger.LogInformation("Creating a new employee");
        var createdEmployee = await employeeService.AddEmployeeAsync(newEmployee);
        return Results.Created($"/employees/{createdEmployee.Id}", createdEmployee);
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "An error occurred while creating a new employee");
        return Results.Problem(ex.Message);
    }
})
.AddEndpointFilter<LoggingFilter>()
.AddEndpointFilter<ExceptionHandlingFilter>();

// Endpoint to update an existing employee
app.MapPut("/employees/{id}", async (int id, Employee updatedEmployee, IEmployeeService employeeService, ILogger<Program> logger) =>
{
    try
    {
        if (!ValidationHelper.TryValidate(updatedEmployee, out var validationResults))
        {
            // Return 400 Bad Request if validation fails
            return Results.BadRequest(validationResults);
        }

        logger.LogInformation("Updating employee with ID {Id}", id);
        var employee = await employeeService.UpdateEmployeeAsync(id, updatedEmployee);
        if (employee == null)
        {
            logger.LogWarning("Employee with ID {Id} not found", id);
            return Results.NotFound(new { Message = $"Employee with ID {id} not found" });
        }
        return Results.Ok(employee);
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "An error occurred while updating employee with ID {Id}", id);
        return Results.Problem(ex.Message);
    }
})
.AddEndpointFilter<LoggingFilter>()
.AddEndpointFilter<ExceptionHandlingFilter>();

// Endpoint to delete an employee
app.MapDelete("/employees/{id}", async (int id, IEmployeeService employeeService, ILogger<Program> logger) =>
{
    try
    {
        logger.LogInformation("Deleting employee with ID {Id}", id);
        var result = await employeeService.DeleteEmployeeAsync(id);
        if (!result)
        {
            logger.LogWarning("Employee with ID {Id} not found", id);
            return Results.NotFound(new { Message = $"Employee with ID {id} not found" });
        }
        return Results.NoContent();
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "An error occurred while deleting employee with ID {Id}", id);
        return Results.Problem(ex.Message);
    }
})
.AddEndpointFilter<LoggingFilter>()
.AddEndpointFilter<ExceptionHandlingFilter>();

// Run the application
app.Run();

Note: The filters are instantiated by the AddEndpointFilter<TFilter>() method, so there is no need to register them separately in the DI container unless you need to inject other services into the filters.

After implementing these filters, please test each endpoint to ensure that the filters are correctly intercepting requests and modifying behaviors as expected. This includes simulating errors to test the exception handling filter and checking logs to confirm the logging filter’s outputs.

Endpoint filters in ASP.NET Core Minimal APIs provide a clean, modular way to handle cross-cutting concerns across your application. They keep your core application logic clean and focused while providing the flexibility to handle concerns like logging, exceptions, and authorization in a reusable manner.

In the next article, I will discuss How to implement JWT Authentication in ASP.NET Core Minimal API with Examples. In this article, I explain How to implement Endpoint Filters in ASP.NET Core Minimal API with Examples. I hope you enjoy this article, Endpoint Filters in ASP.NET Core Minimal API.

Leave a Reply

Your email address will not be published. Required fields are marked *