Error Handling and Logging in ASP.NET Core Minimal API

Error Handling and Logging in ASP.NET Core Minimal API

In this article, I will discuss how to implement Error Handling and Logging in ASP.NET Core Minimal API with examples. Please read our previous articles discussing How to Implement Minimal API in ASP.NET Core with Examples. We will be working with the same project as we worked so far with ASP.NET Core Minimal API.

Error Handling in ASP.NET Core Minimal API

Error Handling in ASP.NET Core Minimal APIs can be done using middleware for global error handling or by using try-catch blocks within endpoint logic.

Implementing Global Error Handling in an ASP.NET Core Minimal API involves creating middleware to catch unhandled exceptions and handle them in a centralized way. Global Error Handling not only improves the maintainability of your code but also enhances the user experience by providing clearer error messages. This approach ensures that any unhandled exceptions are caught and processed uniformly, providing a consistent error response.

Create an Exception Handling Middleware for Global Error Handling

First, create a new class file named ErrorHandlerMiddleware.cs within the Models folder, and then copy and paste the following code. The following ErrorHandlerMiddleware provides global error handling for the application. It catches unhandled exceptions that occur during the processing of HTTP requests, logs the error details, and returns a consistent JSON response to the client. This middleware ensures that unhandled exceptions are handled and processed uniformly. The following code is self-explained, so please read the comment lines for a better understanding.

// Import namespace for HTTP status codes
using System.Net;

// Import namespace for JSON serialization
using System.Text.Json; 

namespace MinimalAPIDemo.Models 
{
    public class ErrorHandlerMiddleware 
    {
        // Field to store the next middleware in the pipeline
        private readonly RequestDelegate _next;

        // Constructor to initialize the middleware with the next delegate
        public ErrorHandlerMiddleware(RequestDelegate next) 
        {
            // Assign the next delegate to the field
            _next = next; 
        }

        // Method to handle the HTTP request
        public async Task Invoke(HttpContext context) 
        {
            try
            {
                // Pass the context to the next middleware in the pipeline
                await _next(context); 
            }
            catch (Exception ex) // Catch any exceptions that occur
            {
                // Handle the exception
                await HandleExceptionAsync(context, ex); 
            }
        }

        // Method to handle exceptions
        private static Task HandleExceptionAsync(HttpContext context, Exception exception) 
        {
            // Set the response content type to JSON
            context.Response.ContentType = "application/json";

            // Set the status code to 500 (Internal Server Error)
            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; 

            // Create a JSON response with the error message and details
            var result = JsonSerializer.Serialize(new { Message = "An unexpected error occurred.", Detail = exception.Message });

            // Write the JSON response to the HTTP response
            return context.Response.WriteAsync(result); 
        }
    }
}
Register the Middleware

In the Program.cs file, register the ErrorHandlerMiddleware in the request pipeline. So, modify the Program.cs class file as follows. In the endpoint, which returns the employee by ID, we have created a scenario that will throw an unhandled exception.

using MinimalAPIDemo.Models;

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

// Add services to the DI container
// Add API explorer for endpoint documentation
builder.Services.AddEndpointsApiExplorer();

// Add Swagger for API documentation
builder.Services.AddSwaggerGen();

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

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

// Use the custom error handling middleware
app.UseMiddleware<ErrorHandlerMiddleware>();

// Configure the HTTP request pipeline for the development environment
if (app.Environment.IsDevelopment())
{
    // Use Swagger middleware to generate Swagger Documentation
    app.UseSwagger();
    // Use Swagger UI middleware to interact with the Swagger documentation
    app.UseSwaggerUI();
}

// CRUD operations for Employee model
// The EmployeeService is injected into the endpoints

// Endpoint to retrieve all employees
app.MapGet("/employees", (IEmployeeService employeeService) => employeeService.GetAllEmployees());

// Endpoint to retrieve a single employee by their ID
app.MapGet("/employees/{id}", (int id, IEmployeeService employeeService) =>
{
    //Creating Scenario to throw Unhandled Exception
    int x = 10, y = 0;
    int result = x / y;
    var employee = employeeService.GetEmployeeById(id);
    return employee is not null ? Results.Ok(employee) : Results.NotFound();
});

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

    // Add the new employee using the EmployeeService
    var createdEmployee = employeeService.AddEmployee(newEmployee);

    // Return 201 Created with the new employee's data
    return Results.Created($"/employees/{createdEmployee.Id}", createdEmployee); 
});

// Endpoint to update an existing employee with validation
app.MapPut("/employees/{id}", (int id, Employee updatedEmployee, IEmployeeService employeeService) =>
{
    // Validate the updated employee using ValidationHelper
    if (!ValidationHelper.TryValidate(updatedEmployee, out var validationResults)) 
    {
        return Results.BadRequest(validationResults); // Return 400 Bad Request if validation fails
    }

    // Update the employee using the EmployeeService
    var employee = employeeService.UpdateEmployee(id, updatedEmployee);

    // Return 200 OK if found and updated, otherwise 404 Not Found
    return employee is not null ? Results.Ok(employee) : Results.NotFound(); 
});

// Endpoint to delete an employee
app.MapDelete("/employees/{id}", (int id, IEmployeeService employeeService) =>
{
    var result = employeeService.DeleteEmployee(id);
    return result ? Results.NoContent() : Results.NotFound();
});

// Run the application
app.Run();

Now, run the application and access the /employees/{id} endpoint. It should return the following error message:

Error Handling and Logging in ASP.NET Core Minimal API

Try-Catch Error Handling in ASP.NE Core Minimal API:

Implementing try-catch error handling in an ASP.NET Core Minimal API involves wrapping the logic of your endpoint handlers in try-catch blocks to catch exceptions and handle them appropriately. This approach allows you to handle exceptions locally within each endpoint. This approach gives you more control over how different types of exceptions are handled for each specific API operation. So, modify the Program.cs class file as follows to implement Try-Catch Error Handling in ASP.NE Core Minimal API.

using MinimalAPIDemo.Models;

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

// Add services to the DI container
// Add API explorer for endpoint documentation
builder.Services.AddEndpointsApiExplorer();

// Add Swagger for API documentation
builder.Services.AddSwaggerGen();

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

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

// Use the custom error handling middleware
app.UseMiddleware<ErrorHandlerMiddleware>();

// Configure the HTTP request pipeline for the development environment
if (app.Environment.IsDevelopment())
{
    // Use Swagger middleware to generate Swagger Documentation
    app.UseSwagger();
    // Use Swagger UI middleware to interact with the Swagger documentation
    app.UseSwaggerUI();
}

// CRUD operations for Employee model
// The EmployeeService is injected into the endpoints

// Endpoint to retrieve all employees
app.MapGet("/employees", (IEmployeeService employeeService) =>
{
    try
    {
        return Results.Ok(employeeService.GetAllEmployees());
    }
    catch (Exception ex)
    {
        return Results.Problem(ex.Message);
    }
});

// Endpoint to retrieve a single employee by their ID
app.MapGet("/employees/{id}", (int id, IEmployeeService employeeService) =>
{
    try
    {
        //Creating Scenario to throw Unhandled Exception
        int x = 10, y = 0;
        int result = x / y;

        var employee = employeeService.GetEmployeeById(id);
        return employee is not null ? Results.Ok(employee) : Results.NotFound();
    }
    catch (Exception ex)
    {
        return Results.Problem(ex.Message);
    }
});

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

        // Add the new employee using the EmployeeService
        var createdEmployee = employeeService.AddEmployee(newEmployee);

        // Return 201 Created with the new employee's data
        return Results.Created($"/employees/{createdEmployee.Id}", createdEmployee);
    }
    catch (Exception ex)
    {
        return Results.Problem(ex.Message);
    }
});

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

        // Update the employee using the EmployeeService
        var employee = employeeService.UpdateEmployee(id, updatedEmployee);

        // Return 200 OK if found and updated, otherwise 404 Not Found
        return employee is not null ? Results.Ok(employee) : Results.NotFound();
    }
    catch (Exception ex)
    {
        return Results.Problem(ex.Message);
    }
});

// Endpoint to delete an employee
app.MapDelete("/employees/{id}", (int id, IEmployeeService employeeService) =>
{
    try
    {
        var result = employeeService.DeleteEmployee(id);
        return result ? Results.NoContent() : Results.NotFound();
    }
    catch (Exception ex)
    {
        return Results.Problem(ex.Message);
    }
});

// Run the application
app.Run();

Now, run the application and access the /employees/{id} endpoint. It should return the following error message:

Try-Catch Error Handling in ASP.NE Core Minimal API

Results.Problem

The Results.Problem method is a helper method provided by ASP.NET Core Minimal APIs to create a standardized ProblemDetails response. ProblemDetails provides a structured way to return error information. It includes properties like status, detail, type, and title. Using Results.Problem ensures that all error responses follow a consistent format, which can be helpful for clients consuming your API.

Properties of ProblemDetails
  • Status: The HTTP status code for the error.
  • Detail: A detailed description of the error.
  • Type: A URI reference that identifies the problem type.
  • Title: A short, human-readable summary of the problem.
Logging in ASP.NET Core Minimal API

Logging is one of the important features of any application, including those built with ASP.NET Core Minimal APIs. It helps diagnose issues, monitor the application’s behavior, troubleshoot applications, and much more. ASP.NET Core comes with a powerful logging API that integrates with various logging providers like Console, Debug, EventSource, and popular third-party libraries such as Serilog, NLog, or log4net. In ASP.NET Core Minimal APIs, you can use the built-in logging framework to log information, warnings, errors, and other types of messages.

Configure Logging

In ASP.NET Core, logging configuration is typically handled in the appsettings.json file and Program.cs. You can specify different logging levels for different environments and outputs. So, first, modify the appsettings.json file as follows. This is also the default code that Visual Studio Created when we created the Project.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}
Modify the Program.cs to Include Logging:

ASP.NET Core includes built-in support for dependency injection of logging services via the ILogger<T> interface, where T is the type of the class into which the logger is injected. Let us modify the Program.cs class to use ILogger:

using Microsoft.AspNetCore.Mvc;
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.AddSingleton<IEmployeeService, EmployeeService>();

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

// Use the custom error handling middleware
app.UseMiddleware<ErrorHandlerMiddleware>();

// 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", (IEmployeeService employeeService, ILogger<Program> logger) =>
{
    try
    {
        logger.LogInformation("Retrieving all employees");
        return Results.Ok(employeeService.GetAllEmployees());
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "An error occurred while retrieving all employees");
        return Results.Problem(ex.Message);
    }
});

// Endpoint to retrieve a single employee by their ID
app.MapGet("/employees/{id}", (int id, IEmployeeService employeeService, ILogger<Program> logger) =>
{
    try
    {
        logger.LogInformation("Retrieving employee with ID {Id}", id);
        var employee = employeeService.GetEmployeeById(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);
    }
});

// Endpoint to create a new employee with validation
app.MapPost("/employees", (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 = employeeService.AddEmployee(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);
    }
});

// Endpoint to update an existing employee
app.MapPut("/employees/{id}", (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 = employeeService.UpdateEmployee(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);
    }
});

// Endpoint to delete an employee
app.MapDelete("/employees/{id}", (int id, IEmployeeService employeeService, ILogger<Program> logger) =>
{
    try
    {
        logger.LogInformation("Deleting employee with ID {Id}", id);
        var result = employeeService.DeleteEmployee(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);
    }
});

// Run the application
app.Run();
Configure Logging:
  • builder.Logging.ClearProviders(): Clears the default logging providers.
  • builder.Logging.AddConsole(): Adds the console logging provider.
  • builder.Logging.AddDebug(): Adds the debug logging provider.
Inject ILogger<Program>:
  • Each endpoint receives an ILogger<Program> parameter to enable logging.
Log Information:
  • logger.LogInformation(…): Logs informational messages.
  • logger.LogWarning(…): Logs warning messages.
  • logger.LogError(…): Logs error messages with exception details.
Try-Catch Blocks:
  • Each endpoint is wrapped in a try-catch block to catch and log exceptions. Depending on the context and severity of the log, different log levels (Information, Warning, Error) are used.

Run your application and perform various operations (create, read, update, delete employees) to generate logs. Depending on the logging providers configured, you should see log messages in the console or debug output.

In the next article, I will discuss How to Implement Asynchronous Programming in ASP.NET Core Minimal API with Examples. In this article, I explain How to Implement Error Handling and Logging in ASP.NET Core Minimal API with Examples. I hope you enjoy this article, Error Handling and Logging in ASP.NET Core Minimal API.

Leave a Reply

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