ASP.NET Core 8 Web API -

Error Handling

Error handling is a crucial part of building robust web APIs. Proper error handling ensures that your application can gracefully handle unexpected situations and provide meaningful responses to clients. This guide covers everything you need to know about error handling in ASP.NET Core 8 Web API, including basic concepts, middleware, custom exceptions, logging, and best practices.


1. Introduction to Error Handling

Error handling in web APIs involves intercepting errors, logging them, and returning appropriate HTTP responses to the client. This helps in maintaining a stable and user-friendly API.


2. Basic Error Handling

2.1 Try-Catch Blocks

The most basic form of error handling is using try-catch blocks in your controller actions.

Example:
        
            
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id}")]
    public IActionResult GetProduct(int id)
    {
        try
        {
            // Simulate a database fetch
            var product = new Product { Id = id, Name = "Sample Product", Price = 9.99m };
            if (product == null)
            {
                return NotFound();
            }
            return Ok(product);
        }
        catch (Exception ex)
        {
            // Log the exception (not shown)
            return StatusCode(500, "Internal server error");
        }
    }
}

        
    


3. Global Error Handling with Middleware

A more robust approach is to use middleware for global error handling. This ensures that all unhandled exceptions are caught and processed in a consistent manner.

3.1 Custom Exception Handling Middleware Create Middleware:
        
            
public class ExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ExceptionHandlingMiddleware> _logger;

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

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An unhandled exception has occurred.");
            await HandleExceptionAsync(context, ex);
        }
    }

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

        var response = new
        {
            message = "An unexpected error occurred.",
            detail = exception.Message
        };
        var jsonResponse = JsonSerializer.Serialize(response);

        return context.Response.WriteAsync(jsonResponse);
    }
}

        
    


Register Middleware:
        
            
var builder = WebApplication.CreateBuilder(args);

// Add services to the container
builder.Services.AddControllers();
builder.Services.AddLogging();

var app = builder.Build();

// Use the custom exception handling middleware
app.UseMiddleware<ExceptionHandlingMiddleware>();

app.MapControllers();
app.Run();

        
    

4. Custom Exception Types

Defining custom exception types helps in categorizing errors and handling them appropriately.

4.1 Define Custom Exceptions Example:
        
            
public class NotFoundException : Exception
{
    public NotFoundException(string message) : base(message) { }
}

public class BadRequestException : Exception
{
    public BadRequestException(string message) : base(message) { }
}

        
    


4.2 Throw Custom Exceptions Example:
        
            
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id}")]
    public IActionResult GetProduct(int id)
    {
        try
        {
            // Simulate a database fetch
            var product = new Product { Id = id, Name = "Sample Product", Price = 9.99m };
            if (product == null)
            {
                throw new NotFoundException("Product not found");
            }
            return Ok(product);
        }
        catch (NotFoundException ex)
        {
            return NotFound(new { message = ex.Message });
        }
        catch (Exception ex)
        {
            // Log the exception (not shown)
            return StatusCode(500, "Internal server error");
        }
    }
}

        
    

5. Handling Validation Errors

ASP.NET Core provides built-in support for model validation errors. You can customize how these errors are handled.

5.1 Customizing Validation Error Responses Configure API Behavior:
        
            
builder.Services.Configure<ApiBehaviorOptions>(options =>
{
    options.InvalidModelStateResponseFactory = context =>
    {
        var errors = context.ModelState
            .Where(e => e.Value.Errors.Count > 0)
            .Select(e => new
            {
                Field = e.Key,
                Errors = e.Value.Errors.Select(x => x.ErrorMessage).ToArray()
            }).ToArray();

        var errorResponse = new
        {
            message = "Validation failed",
            errors = errors
        };

        return new BadRequestObjectResult(errorResponse);
    };
});

        
    


Model Validation Example:
        
            
public class Product
{
    public int Id { get; set; }

    [Required(ErrorMessage = "Name is required")]
    [StringLength(100, ErrorMessage = "Name can't be longer than 100 characters")]
    public string Name { get; set; }

    [Range(0.01, 1000.00, ErrorMessage = "Price must be between 0.01 and 1000.00")]
    public decimal Price { get; set; }
}

        
    


Controller:
        
            
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpPost]
    public IActionResult CreateProduct([FromBody] Product product)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        // Save the product to the database (not shown)

        return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
    }

    [HttpGet("{id}")]
    public IActionResult GetProduct(int id)
    {
        // Retrieve the product from the database (not shown)
        var product = new Product { Id = id, Name = "Sample Product", Price = 9.99m };
        return Ok(product);
    }
}

        
    

6. Logging Errors

Logging errors is crucial for diagnosing and troubleshooting issues in your application.

6.1 Configure Logging Configure Logging in Program.cs:
        
            
var builder = WebApplication.CreateBuilder(args);

// Configure logging
builder.Logging.ClearProviders();
builder.Logging.AddConsole();

var app = builder.Build();

        
    


6.2 Log Errors in Middleware Example:
        
            
public class ExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ExceptionHandlingMiddleware> _logger;

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

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An unhandled exception has occurred.");
            await HandleExceptionAsync(context, ex);
        }
    }

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

        var response = new
        {
            message = "An unexpected error occurred.",
            detail = exception.Message
        };
        var jsonResponse = JsonSerializer.Serialize(response);

        return context.Response.WriteAsync(jsonResponse);
    }
}

        
    

7. Best Practices


8. Comprehensive Example

Here is a comprehensive example combining global error handling, custom exceptions, validation, and logging.

Program.cs:
        
            
var builder = WebApplication.CreateBuilder(args);

// Add services to the container
builder.Services.AddControllers();
builder.Services.AddLogging();
builder.Services.Configure<ApiBehaviorOptions>(options =>
{
    options.InvalidModelStateResponseFactory = context =>
    {
        var errors = context.ModelState
            .Where(e => e.Value.Errors.Count > 0)
            .Select(e => new
            {
                Field = e.Key,
                Errors = e.Value.Errors.Select(x => x.ErrorMessage).ToArray()
            }).ToArray();

        var errorResponse = new
        {
            message = "Validation failed",
            errors = errors
        };

        return new BadRequestObjectResult(errorResponse);
    };
});

var app = builder.Build();

// Use the custom exception handling middleware
app.UseMiddleware<ExceptionHandlingMiddleware>();

app.MapControllers();
app.Run();

        
    


Custom Exceptions:
        
            
public class NotFoundException : Exception
{
    public NotFoundException(string message) : base(message) { }
}

public class BadRequestException : Exception
{
    public BadRequestException(string message) : base(message) { }
}

        
    


Middleware:
        
            
public class ExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ExceptionHandlingMiddleware> _logger;

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

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (NotFoundException ex)
        {
            await HandleExceptionAsync(context, ex, HttpStatusCode.NotFound);
        }
        catch (BadRequestException ex)
        {
            await HandleExceptionAsync(context, ex, HttpStatusCode.BadRequest);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An unhandled exception has occurred.");
            await HandleExceptionAsync(context, ex, HttpStatusCode.InternalServerError);
        }
    }

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

        var response = new
        {
            message = exception.Message,
            detail = exception is CustomException ? exception.Message : "An unexpected error occurred."
        };
        var jsonResponse = JsonSerializer.Serialize(response);

        return context.Response.WriteAsync(jsonResponse);
    }
}

        
    


Controller:
        
            
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpPost]
    public IActionResult CreateProduct([FromBody] Product product)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        // Simulate a scenario where product creation fails
        throw new BadRequestException("Failed to create the product");

        return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
    }

    [HttpGet("{id}")]
    public IActionResult GetProduct(int id)
    {
        // Simulate a scenario where the product is not found
        throw new NotFoundException("Product not found");

        // Simulate a successful product retrieval
        var product = new Product { Id = id, Name = "Sample Product", Price = 9.99m };
        return Ok(product);
    }
}

        
    


Model with Validation:
        
            
public class Product
{
    public int Id { get; set; }

    [Required(ErrorMessage = "Name is required")]
    [StringLength(100, ErrorMessage = "Name can't be longer than 100 characters")]
    public string Name { get; set; }

    [Range(0.01, 1000.00, ErrorMessage = "Price must be between 0.01 and 1000.00")]
    public decimal Price { get; set; }
}

        
    

9. Conclusion

Proper error handling is essential for building robust and user-friendly web APIs. By leveraging global error handling middleware, custom exceptions, validation error handling, and logging, you can ensure that your ASP.NET Core 8 Web API handles errors gracefully and provides meaningful responses to clients. This comprehensive guide provides the tools and knowledge to implement effective error handling in your ASP.NET Core 8 Web API projects.