Error Handling and Logging

Learning Objectives

  • Understand how to implement global error handling using middleware in ASP.NET Core.
  • Learn how to create a consistent error response format for better client-side debugging.
  • Learn how to use the built-in logging system in ASP.NET Core, as well as integrate third-party logging providers like Serilog.

Global Exception Handling

What is Global Exception Handling? Global exception handling allows you to catch and handle errors that occur anywhere in your application. It is essential to prevent unexpected crashes and to ensure that the user receives meaningful error messages.

In ASP.NET Core, you can implement global exception handling using middleware to provide a consistent response format for all unhandled exceptions.

Why Use Middleware for Error Handling?

  • Middleware can handle exceptions globally instead of managing them in individual controllers or services.
  • It provides a consistent response format for clients and prevents the server from leaking sensitive information.

Example: Global Error Handling Middleware:

Create Custom Middleware for Error Handling:

public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate _next;

    public ErrorHandlingMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

        var response = new { message = "An unexpected error occurred.", details = ex.Message };
        return context.Response.WriteAsync(JsonSerializer.Serialize(response));
    }
}

Register Middleware in Program.cs:

var app = builder.Build();

app.UseMiddleware<ErrorHandlingMiddleware>();

app.UseRouting();
app.UseAuthorization();
app.MapControllers();
app.Run();

Explanation: The ErrorHandlingMiddleware catches any unhandled exceptions, sets the appropriate status code, and returns a JSON response with an error message. This provides a consistent error format for all clients.

Real-Life Example: In an e-commerce application, when something goes wrong (e.g., unable to add an item to the cart due to a database issue), the global error handler provides a structured error response, avoiding the app crashing.

Logging

What is Logging? Logging involves recording information about your application’s runtime behavior. Logs are crucial for debugging and troubleshooting, especially for tracking errors and understanding how users interact with the system.

Built-in Logging System in ASP.NET Core: ASP.NET Core provides a built-in logging framework that works with various logging providers (e.g., Console, Debug).

Example: Using Built-in Logging in a Controller:

using Microsoft.Extensions.Logging;

[ApiController]
[Route("api/products")]
public class ProductController : ControllerBase
{
    private readonly ILogger<ProductController> _logger;

    public ProductController(ILogger<ProductController> logger)
    {
        _logger = logger;
    }

    [HttpGet("{id}")]
    public IActionResult GetProduct(int id)
    {
        _logger.LogInformation("Getting product with ID: {ProductId}", id);

        if (id <= 0)
        {
            _logger.LogWarning("Invalid product ID: {ProductId}", id);
            return BadRequest("Invalid product ID.");
        }

        try
        {
            // Simulate fetching product logic
            return Ok(new { Id = id, Name = "Laptop", Price = 1200 });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error occurred while getting product with ID: {ProductId}", id);
            return StatusCode(500, "Internal server error");
        }
    }
}

Explanation:

  • LogInformation: Logs informational messages (e.g., tracking a specific action).
  • LogWarning: Logs a warning for situations that are not errors but could lead to problems.
  • LogError: Logs error details, typically when an exception is caught.

Real-Life Example: In a financial application, logging can be used to track each transaction made by a user to understand what actions took place before an error occurred.

Integrating Third-Party Logging: Serilog

What is Serilog? Serilog is a popular third-party logging library for .NET that provides rich features like structured logging and the ability to write logs to various destinations, such as files, databases, and cloud logging platforms.

How to Integrate Serilog:

  1. Install Serilog NuGet Packages:
    • Install Serilog.AspNetCore and Serilog.Sinks.File via NuGet.
  2. Configure Serilog in Program.cs:
using Serilog;

Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .WriteTo.File("logs/log.txt", rollingInterval: RollingInterval.Day)
    .CreateLogger();

var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog();

var app = builder.Build();

Example: Logging with Serilog:

public class ProductService
{
    private readonly ILogger<ProductService> _logger;

    public ProductService(ILogger<ProductService> logger)
    {
        _logger = logger;
    }

    public void AddProduct(Product product)
    {
        try
        {
            _logger.LogInformation("Adding a new product: {ProductName}", product.Name);
            // Add product logic here
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error occurred while adding product: {ProductName}", product.Name);
            throw;
        }
    }
}

Explanation: Serilog provides enhanced logging capabilities, such as writing log data to files, which can help with debugging and auditing actions in the system.

Real-Life Example: In a user management system, when an admin adds or deletes a user, Serilog logs this information to a file, which can be reviewed for audit purposes.

Examples

Simple Example: Logging in a Controller Action

Log when an endpoint is accessed and if it fails.

Code Snippet:

[HttpGet("get-all")]
public IActionResult GetAllProducts()
{
    _logger.LogInformation("Getting all products.");
    try
    {
        // Simulate fetching all products
        return Ok(new List<string> { "Product1", "Product2" });
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Failed to get all products.");
        return StatusCode(500, "Internal server error.");
    }
}

Real-Life Example: Logs when users access the list of products in an e-commerce site.

Simple Example: Middleware-Based Error Handling

Create a middleware to handle unhandled exceptions.

Code Snippet:

public async Task InvokeAsync(HttpContext context)
{
    try
    {
        await _next(context);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Unhandled exception occurred.");
        context.Response.StatusCode = 500;
        await context.Response.WriteAsync("An unexpected error occurred.");
    }
}

Real-Life Example: Handles unexpected errors while processing payments in a financial transaction.

Simple Example: Using Serilog to Log to a File

Write logs to a file for persistent storage.

Code Snippet:

Log.Logger = new LoggerConfiguration()
    .WriteTo.File("logs/errors.txt", rollingInterval: RollingInterval.Day)
    .CreateLogger();

_logger.LogInformation("Application started and logging initialized.");

Real-Life Example: Logs user activities in a user management system to track who added or removed users.

Key Takeaways

  • Global Exception Handling: Use middleware to provide a consistent error-handling mechanism across your application.
  • Logging: Use built-in ASP.NET Core logging to track the behavior of your system and capture issues.
  • Third-Party Logging with Serilog: Serilog allows you to log information to different sinks like console, files, and databases for easier debugging and auditing.

Practical Questions

  1. What are the benefits of using global exception handling?
  2. How can Serilog enhance logging in a .NET application compared to the built-in logger?
  3. How would you implement a consistent error response format across all your API endpoints?

New Concepts in .NET 8

New in .NET 8:

  • Improved Logging Configuration: .NET 8 makes configuring logging in the Program.cs file more streamlined, reducing boilerplate code while keeping flexibility.
  • Exception Middleware Improvements: The built-in exception middleware has been enhanced to provide better performance and easier integration for handling complex error scenarios.

E-commerce program example

Code Overview

  • Global Exception Handling Middleware: Differentiates between general exceptions and SQL database exceptions.
  • Built-in ASP.NET Core Logging: Logs user activities, including user details, visited pages, products added to the cart, and products purchased.

Step 1: Configure Built-in Logging in Program.cs

The built-in ASP.NET Core logger is available by default.

var builder = WebApplication.CreateBuilder(args);

// Configure logging in `Program.cs` (optional)
builder.Logging.ClearProviders(); // Optional, to start fresh with only selected providers
builder.Logging.AddConsole();     // Add Console Logging
builder.Logging.AddDebug();       // Add Debug Logging

builder.Services.AddControllers();
builder.Services.AddScoped<IProductRepository, ProductRepository>();

var app = builder.Build();

app.UseMiddleware<ErrorHandlingMiddleware>();

app.UseRouting();
app.UseAuthorization();
app.MapControllers();

app.Run();

AddConsole and AddDebug: Use the default built-in providers to write logs to the console and debug output.

Step 2: Create Global Error Handling Middleware

The middleware will handle exceptions and differentiate between general and SQL database exceptions.

using System.Data.SqlClient;
using Microsoft.Extensions.Logging;

public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ErrorHandlingMiddleware> _logger;

    public ErrorHandlingMiddleware(RequestDelegate next, ILogger<ErrorHandlingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (SqlException sqlEx)
        {
            _logger.LogError(sqlEx, "Database connection error occurred.");
            context.Response.StatusCode = 500;
            await context.Response.WriteAsync("A database error occurred. Please try again later.");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An unexpected error occurred.");
            context.Response.StatusCode = 500;
            await context.Response.WriteAsync("An unexpected error occurred. Please contact support.");
        }
    }
}

Logging for Exceptions: The middleware catches SQL exceptions separately from general exceptions, logs both, and returns appropriate responses.

Step 3: Create Product Controller

The ProductController manages user actions, such as viewing products, adding products to the cart, and purchasing products, with appropriate logging.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

[ApiController]
[Route("api/products")]
public class ProductController : ControllerBase
{
    private readonly IProductRepository _productRepository;
    private readonly ILogger<ProductController> _logger;

    public ProductController(IProductRepository productRepository, ILogger<ProductController> logger)
    {
        _productRepository = productRepository;
        _logger = logger;
    }

    [HttpGet("{id}")]
    public IActionResult GetProduct(int id)
    {
        _logger.LogInformation("User visited product page with ID: {ProductId}", id);

        var product = _productRepository.GetProductById(id);
        if (product == null)
        {
            _logger.LogWarning("Product with ID: {ProductId} not found", id);
            return NotFound("Product not found.");
        }

        return Ok(product);
    }

    [HttpPost("add-to-cart/{id}")]
    public IActionResult AddToCart(int id)
    {
        _logger.LogInformation("User added product with ID: {ProductId} to the cart", id);
        // Simulate adding product to the cart
        return Ok($"Product with ID: {id} added to the cart.");
    }

    [HttpPost("purchase/{id}")]
    public IActionResult PurchaseProduct(int id)
    {
        try
        {
            _logger.LogInformation("User purchased product with ID: {ProductId}", id);
            // Simulate purchasing the product
            return Ok($"Product with ID: {id} purchased successfully.");
        }
        catch (SqlException sqlEx)
        {
            _logger.LogError(sqlEx, "Failed to purchase product due to database issue.");
            return StatusCode(500, "A database error occurred. Please try again later.");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to purchase product due to an unexpected error.");
            return StatusCode(500, "An unexpected error occurred. Please contact support.");
        }
    }
}

Logging:

  • Logs when users visit a product page, add a product to the cart, and purchase a product.
  • Logs warning messages when products are not found.

Step 4: Create Product Repository

The repository interacts with the database. Here, we're simulating the database logic with potential SQL exceptions.

using System.Data.SqlClient;

public interface IProductRepository
{
    Product GetProductById(int productId);
}

public class ProductRepository : IProductRepository
{
    public Product GetProductById(int productId)
    {
        // Mocking database interaction. In a real application, this would involve a database call.
        if (productId <= 0)
        {
            throw new SqlException("Invalid product ID");
        }

        return new Product { Id = productId, Name = "Product" + productId, Price = 99.99m };
    }
}

Simulated Database Logic: Throws an SQL exception when an invalid product ID is provided.

Step 5: Create Product Model

Define a Product model to represent products in the system.

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

Logging Output

The logs generated by the built-in ASP.NET Core logging system can be output to the console and the Debug window in Visual Studio. Here is what typical log messages might look like:

info: ProductController[0]
      User visited product page with ID: 1
warn: ProductController[0]
      Product with ID: 999 not found
info: ProductController[0]
      User added product with ID: 1 to the cart
info: ProductController[0]
      User purchased product with ID: 1
error: ErrorHandlingMiddleware[0]
      Database connection error occurred. SqlException: Invalid product ID
error: ErrorHandlingMiddleware[0]
      An unexpected error occurred. Exception: NullReferenceException...

Summary

  • Global Exception Handling: Implemented via custom middleware to differentiate between general exceptions and SQL exceptions.
  • Built-in Logging System: Logs various user activities such as viewing products, adding products to the cart, and purchasing products.
  • Product Controller: Manages user actions with logging to track each step.

Saving logs to file

Option 1: Using an Existing Simple File Logger Package

There is a simple community-built file logging provider that can be used for .NET Core, like FileLoggerExtensions. Below is how to use it.

Add FileLoggerExtensions NuGet Package:


Configure File Logging in Program.cs: Once the package is installed, you can configure it in your program to write to a file.

var builder = WebApplication.CreateBuilder(args);

// Configure logging
builder.Logging.ClearProviders(); // Optional: Clear default providers
builder.Logging.AddConsole();     // Console logging
builder.Logging.AddDebug();       // Debug window logging
builder.Logging.AddFile("logs/ecommerce_log.txt"); // File logging

builder.Services.AddControllers();
builder.Services.AddScoped<IProductRepository, ProductRepository>();

var app = builder.Build();

app.UseMiddleware<ErrorHandlingMiddleware>();

app.UseRouting();
app.UseAuthorization();
app.MapControllers();

app.Run();

AddFile("logs/ecommerce_log.txt"): Configures logging to write log messages to a file named ecommerce_log.txt in the logs folder.

Option 2: Creating a Custom File Logger

If you want more control over how logging works, you can create your own file logger. This involves creating a custom implementation of ILogger and ILoggerProvider.

Step 1: Create a Custom File Logger

The FileLogger will implement the ILogger interface and write logs to a file.

using Microsoft.Extensions.Logging;
using System.IO;

public class FileLogger : ILogger
{
    private readonly string _filePath;
    private static readonly object _lock = new object();

    public FileLogger(string filePath)
    {
        _filePath = filePath;
    }

    public IDisposable BeginScope<TState>(TState state) => null;

    public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Information;

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        if (!IsEnabled(logLevel))
            return;

        var logRecord = $"{DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss")} [{logLevel}] {formatter(state, exception)}";
        lock (_lock)
        {
            File.AppendAllText(_filePath, logRecord + Environment.NewLine);
        }
    }
}
  • Log Method: Formats the log entry and writes it to the specified log file.
  • Thread-Safe Logging: The lock (_lock) ensures that only one thread can write to the file at a time.

Step 2: Create a File Logger Provider

A provider is responsible for creating instances of the FileLogger.

using Microsoft.Extensions.Logging;

public class FileLoggerProvider : ILoggerProvider
{
    private readonly string _filePath;

    public FileLoggerProvider(string filePath)
    {
        _filePath = filePath;
    }

    public ILogger CreateLogger(string categoryName)
    {
        return new FileLogger(_filePath);
    }

    public void Dispose() { }
}

CreateLogger Method: Creates an instance of FileLogger for each category.

Step 3: Add Custom File Logger Extension

Add an extension method to easily add the FileLoggerProvider to the logging configuration.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

public static class FileLoggerExtensions
{
    public static ILoggingBuilder AddFile(this ILoggingBuilder builder, string filePath)
    {
        builder.Services.AddSingleton<ILoggerProvider>(new FileLoggerProvider(filePath));
        return builder;
    }
}

Step 4: Configure the Custom File Logger in Program.cs

Use the new file logger extension in your application.

var builder = WebApplication.CreateBuilder(args);

// Configure logging
builder.Logging.ClearProviders(); // Optional: Clear default providers
builder.Logging.AddConsole();     // Console logging
builder.Logging.AddDebug();       // Debug window logging
builder.Logging.AddFile("logs/ecommerce_log.txt"); // Custom file logging

builder.Services.AddControllers();
builder.Services.AddScoped<IProductRepository, ProductRepository>();

var app = builder.Build();

app.UseMiddleware<ErrorHandlingMiddleware>();

app.UseRouting();
app.UseAuthorization();
app.MapControllers();

app.Run();

Step 5: Updated Product Controller with Logging

The controller will log user activities such as viewing products, adding products to the cart, and purchasing products.

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/products")]
public class ProductController : ControllerBase
{
    private readonly IProductRepository _productRepository;
    private readonly ILogger<ProductController> _logger;

    public ProductController(IProductRepository productRepository, ILogger<ProductController> logger)
    {
        _productRepository = productRepository;
        _logger = logger;
    }

    [HttpGet("{id}")]
    public IActionResult GetProduct(int id)
    {
        _logger.LogInformation("User visited product page with ID: {ProductId}", id);

        var product = _productRepository.GetProductById(id);
        if (product == null)
        {
            _logger.LogWarning("Product with ID: {ProductId} not found", id);
            return NotFound("Product not found.");
        }

        return Ok(product);
    }

    [HttpPost("add-to-cart/{id}")]
    public IActionResult AddToCart(int id)
    {
        _logger.LogInformation("User added product with ID: {ProductId} to the cart", id);
        // Simulate adding product to the cart
        return Ok($"Product with ID: {id} added to the cart.");
    }

    [HttpPost("purchase/{id}")]
    public IActionResult PurchaseProduct(int id)
    {
        try
        {
            _logger.LogInformation("User purchased product with ID: {ProductId}", id);
            // Simulate purchasing the product
            return Ok($"Product with ID: {id} purchased successfully.");
        }
        catch (SqlException sqlEx)
        {
            _logger.LogError(sqlEx, "Failed to purchase product due to database issue.");
            return StatusCode(500, "A database error occurred. Please try again later.");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to purchase product due to an unexpected error.");
            return StatusCode(500, "An unexpected error occurred. Please contact support.");
        }
    }
}

Log File Output

The custom file logger or community-built file logger will generate a log file (logs/ecommerce_log.txt) similar to:

2024-10-30 10:00:00 [Information] User visited product page with ID: 1
2024-10-30 10:01:00 [Information] User added product with ID: 1 to the cart
2024-10-30 10:02:00 [Information] User purchased product with ID: 1
2024-10-30 10:03:00 [Error] Failed to purchase product due to database issue. SqlException: Invalid product ID

Summary

  • Built-in Logging System with File Logging: We used the built-in logging capabilities of ASP.NET Core and added a file logger to save logs to a specific file.
  • Custom Middleware: Differentiates between general exceptions and SQL database exceptions.
  • User Activity Logging: Logged when users visited product pages, added products to the cart, and purchased products.

ניווט במאמר

מאמרים אחרונים

Weekly Tutorial