Centralized Error Handling in Dotnet Core

 Centralized Error Handling in Dotnet Core:


using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Hosting;
using System.Text.Json;
using ECommerceWebAPI.Models;

namespace ECommerceWebAPI.Globals
{
    public class ExceptionMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly ILogger<ExceptionMiddleware> _logger;
        private readonly IHostEnvironment _environment;

        private static class ErrorCodes
        {
            public const string Unknown = "UNKNOWN_ERROR";
            public const string Unauthorized = "UNAUTHORIZED";
            public const string BadRequest = "BAD_REQUEST";
            public const string NotFound = "NOT_FOUND";
            public const string ValidationError = "VALIDATION_ERROR";
            public const string RateLimitExceeded = "RATE_LIMIT_EXCEEDED";
            public const string Conflict = "CONFLICT";
        }

        private const string DefaultMessage = "An unexpected error occurred.";
        private const string DefaultUserMessage = "Something went wrong. Please try again later.";

        public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger, IHostEnvironment environment)
        {
            _next = next ?? throw new ArgumentNullException(nameof(next));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            _environment = environment ?? throw new ArgumentNullException(nameof(environment));
        }

        public async Task InvokeAsync(HttpContext httpContext)
        {
            try
            {
                await _next(httpContext).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                LogException(httpContext, ex);
                await HandleExceptionAsync(httpContext, ex).ConfigureAwait(false);
            }
        }

        private string GetTraceId(HttpContext context)
        {
            var traceId = context.Request.Headers["X-Request-ID"].ToString();
            if (string.IsNullOrEmpty(traceId))
            {
                traceId = Guid.NewGuid().ToString();
                _logger.LogWarning("No TraceId provided. A new TraceId was generated: {TraceId}", traceId);
            }
            return traceId;
        }

        private void LogException(HttpContext context, Exception exception)
        {
            string traceId = GetTraceId(context);
            string ipAddress = context.Connection.RemoteIpAddress?.ToString();
            string userAgent = context.Request.Headers["User-Agent"].ToString();

            var logMessage = _environment.IsDevelopment()
                ? $"Development Error: {{Message}} | TraceId: {{TraceId}} | IP: {{IpAddress}} | UserAgent: {{UserAgent}} | Method: {{Method}} | URL: {{Url}}"
                : $"Error: {{Message}} | TraceId: {{TraceId}} | IP: {{IpAddress}} | UserAgent: {{UserAgent}} | Method: {{Method}} | URL: {{Url}}";

            // Log the exception with the appropriate level based on exception type
            if (exception is ValidationException || exception is ArgumentException)
            {
                _logger.LogWarning(exception, logMessage, exception.Message, traceId, ipAddress, userAgent, context.Request.Method, context.Request.Path);
            }
            else
            {
                _logger.LogError(exception, logMessage, exception.Message, traceId, ipAddress, userAgent, context.Request.Method, context.Request.Path);
            }
        }

        private async Task HandleExceptionAsync(HttpContext context, Exception exception)
        {
            context.Response.ContentType = "application/json";

            var errorResponse = CreateErrorResponse(context, exception);

            var responseJson = JsonSerializer.Serialize(errorResponse, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
            await context.Response.WriteAsync(responseJson).ConfigureAwait(false);
        }

        private ErrorResponse CreateErrorResponse(HttpContext context, Exception exception)
        {
            var traceId = GetTraceId(context); // Extract trace ID once
            var errorResponse = new ErrorResponse
            {
                StatusCode = context.Response.StatusCode,
                ErrorCode = ErrorCodes.Unknown,
                Message = DefaultMessage,
                UserMessage = DefaultUserMessage,
                Timestamp = DateTime.UtcNow,
                TraceId = traceId
            };

            // Handle specific exception types and map them to appropriate error responses
            switch (exception)
            {
                case UnauthorizedAccessException _:
                    context.Response.StatusCode = 401;
                    return BuildErrorResponse(context, exception, 401, ErrorCodes.Unauthorized, "Access denied.", "You are not authorized to perform this action.");
                case KeyNotFoundException _:
                    context.Response.StatusCode = 404;
                    return BuildErrorResponse(context, exception, 404, ErrorCodes.NotFound, "Resource not found.", "The requested resource was not found.");
                case InvalidOperationException _:
                    context.Response.StatusCode = 409;
                    return BuildErrorResponse(context, exception, 409, ErrorCodes.Conflict, "Request conflict.", "There was a conflict with the request.");
                case ValidationException _:
                    context.Response.StatusCode = 422;
                    return BuildErrorResponse(context, exception, 422, ErrorCodes.ValidationError, "Invalid request body.", "Please check the input data and try again.");
                case RateLimitExceededException _:
                    context.Response.StatusCode = 429;
                    return BuildErrorResponse(context, exception, 429, ErrorCodes.RateLimitExceeded, "Rate limit exceeded.", "You have exceeded the maximum number of requests.");
                default:
                    context.Response.StatusCode = 500;
                    return BuildErrorResponse(context, exception, 500, ErrorCodes.Unknown, exception.Message, "An internal error occurred. Please try again later.");
            }
        }

        private ErrorResponse BuildErrorResponse(HttpContext context, Exception exception, int statusCode, string errorCode, string message, string userMessage)
        {
            var traceId = GetTraceId(context); // Extract trace ID once
            var errorResponse = new ErrorResponse
            {
                StatusCode = statusCode,
                ErrorCode = errorCode,
                Message = message,
                UserMessage = userMessage,
                Timestamp = DateTime.UtcNow,
                TraceId = traceId
            };

            // Include detailed error information in development environment
            if (_environment.IsDevelopment())
            {
                errorResponse.DetailedError = exception.ToString();
            }

            return errorResponse;
        }
    }
}


The code you provided implements a custom middleware in an ASP.NET Core web API for handling exceptions and logging errors. Here's a breakdown of the key components and what they do:

1. ExceptionMiddleware Class

  • This class is designed to catch and handle exceptions that occur during HTTP request processing in the application.
  • It intercepts the request pipeline using the InvokeAsync method, which is called automatically during the processing of a request.
  • The middleware logs exceptions and formats an appropriate error response before sending it back to the client.

2. Constructor: ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger, IHostEnvironment environment)

  • next: The next middleware in the pipeline.
  • logger: Injected logger for logging information related to exceptions.
  • environment: Injected environment information to tailor error responses depending on whether the app is in a development or production environment.

3. InvokeAsync Method

  • This is the core of the middleware. It:
    • Tries to pass the request to the next middleware using _next(httpContext).
    • If an exception is thrown, it catches the exception and handles it by calling HandleExceptionAsync and logs it via LogException.

4. GetTraceId Method

  • Extracts a trace ID from the request header (X-Request-ID), which can be used to track requests and correlate logs. If the header is missing, it generates a new Guid.
  • This trace ID is used to provide better traceability of requests in logs, especially helpful for debugging issues in production environments.

5. LogException Method

  • This method logs the exception in the appropriate format depending on the environment (Development or Production).
  • It logs:
    • Exception message
    • Trace ID
    • IP address of the client
    • User-Agent string from the request header
    • HTTP method (GET, POST, etc.)
    • Requested URL
  • The log level is determined based on the exception type:
    • Warning for client errors like ValidationException or ArgumentException.
    • Error for more severe exceptions like unexpected server errors.

6. HandleExceptionAsync Method

  • Sets the response content type to application/json.
  • It calls CreateErrorResponse to generate a structured error response.
  • The error response is serialized to JSON and sent to the client.

7. CreateErrorResponse Method

  • Creates a generic error response with a status code, error code, message, user message, and timestamp.
  • This method handles different types of exceptions (e.g., UnauthorizedAccessException, KeyNotFoundException, ValidationException) and maps them to appropriate HTTP status codes and error details.

8. Error Handling and Error Codes

  • The middleware defines a static class ErrorCodes that contains constants for various error codes like UNKNOWN_ERROR, UNAUTHORIZED, NOT_FOUND, VALIDATION_ERROR, etc.
  • Based on the exception type, it maps exceptions to specific error responses. For example:
    • UnauthorizedAccessException results in a 401 (Unauthorized) status.
    • KeyNotFoundException results in a 404 (Not Found) status.
    • ValidationException results in a 422 (Unprocessable Entity) status.
    • Other exceptions, like RateLimitExceededException, also have specific status codes.

9. BuildErrorResponse Method

  • This method constructs the final error response with all the required details, including:
    • Status code, error code, message, and user-friendly message.
    • If the app is in development, it adds the detailed error information to the response, providing stack traces or other detailed logs.

10. ErrorResponse Model

  • The error response is structured in an ErrorResponse object. This object likely has properties such as:
    • StatusCode: The HTTP status code (e.g., 400, 404, 500).
    • ErrorCode: A custom error code indicating the type of error.
    • Message: A technical message describing the error.
    • UserMessage: A user-friendly message meant to be shown to the client.
    • Timestamp: The time when the error occurred.
    • TraceId: A unique trace ID for tracking the request.
    • DetailedError: (Optional) A detailed stack trace or error message, only included in development environments.

Usage

This middleware should be added to the application's request pipeline in the Startup.cs or Program.cs file, typically inside the Configure method:

app.UseMiddleware<ExceptionMiddleware>();


Benefits

  • Centralized Exception Handling: This middleware provides a consistent way to handle exceptions throughout the entire application.
  • Logging: Detailed logging of exceptions is generated, which is crucial for diagnosing issues.
  • User-friendly Error Responses: The middleware ensures that users receive meaningful error messages while keeping technical details (such as stack traces) hidden in production environments.
  • Traceability: The use of TraceId allows the tracing of requests, making it easier to correlate logs and track down issues in a distributed system.

This middleware is a robust solution for error handling and logging in an ASP.NET Core application, ensuring both developers and users have the necessary information to understand and resolve issues.

 

Your code for the ExceptionMiddleware in an ASP.NET Core application looks solid and well-structured. This middleware handles different types of exceptions and returns appropriate responses with error codes, messages, and trace information. Here's a brief breakdown of what each section does and some suggestions for potential improvements or observations:

1. Exception Handling Logic:

  • Exception handling: You wrap the request processing in a try-catch block, catching any exception that may occur during request processing and logging it. The LogException and HandleExceptionAsync methods help capture and log detailed error information.

2. Error Handling Strategy:

  • You classify errors into different categories based on exception types (e.g., UnauthorizedAccessException, KeyNotFoundException, etc.), and map them to appropriate HTTP status codes, error codes, and messages. This is a great practice for providing clear and consistent error responses.

3. Trace ID Generation:

  • Trace ID Handling: The code extracts the X-Request-ID header from the request to track the request. If it is not provided, you generate a new Guid. This is important for correlating logs across services and debugging production issues.

4. Logging Mechanism:

  • Development vs Production logging: You log different levels of information depending on whether the environment is development or production. This ensures that in production, sensitive error details (like stack traces) are not exposed to the end user, but in development, they are included for debugging purposes.

5. Error Response Format:

  • The error response contains:
    • StatusCode: The HTTP status code for the error.
    • ErrorCode: A custom error code representing the type of error.
    • Message: A message to describe the error (for developers).
    • UserMessage: A user-friendly message for the client.
    • Timestamp: The timestamp when the error occurred.
    • TraceId: The trace ID for tracking purposes.
    • Optionally, DetailedError: A detailed error message that will be included in development environments for debugging.

6. Possible Enhancements or Observations:

  • Custom Exception Types: You have custom exception handling for several common exception types like UnauthorizedAccessException, KeyNotFoundException, and others. If your project includes additional custom exceptions (e.g., BusinessRuleException), you could add more specific handling for those as well.
  • Rate Limit Logging: For the RateLimitExceededException, it might be useful to log the specific number of requests made, especially if there's a limit imposed, so you can track clients who are hitting the rate limit.
  • ErrorCode Consistency: You use predefined ErrorCodes for each exception type, which is great for consistency. Ensure that these error codes are aligned with any public API documentation you have or any error handling standard you are following.
  • Unit Testing: The exception middleware is difficult to unit test directly since it deals with HttpContext and logging. However, you can mock ILogger, IHostEnvironment, and HttpContext to create unit tests that ensure proper behavior in different scenarios (e.g., how the middleware behaves with different exception types).
  • ValidationException: You handle ValidationException, which appears to be a custom exception in this code. Ensure that this exception is being thrown and handled properly within your application. You might also want to add validation-specific details to the response (e.g., which field failed validation).

7. Code Maintenance and Readability:

  • The code is clean and readable, with methods broken down logically (e.g., LogException, HandleExceptionAsync). Consider grouping related constants (like ErrorCodes) or exception handling logic into separate classes if your application grows and the logic becomes more complex.

Example Response:

For an UnauthorizedAccessException, the response might look like:

{

  "statusCode": 401,

  "errorCode": "UNAUTHORIZED",

  "message": "Access denied.",

  "userMessage": "You are not authorized to perform this action.",

  "timestamp": "2024-12-28T12:00:00Z",

  "traceId": "1234-abcd-5678-efgh"

}

Overall, this is a well-implemented exception handling middleware for an API, and it should work effectively for catching and logging errors, while providing clear and helpful responses to clients.

 


Comments

Popular posts from this blog

Multiline to singleline IN C# - CODING

EF Core interview questions for beginners

EF Core interview questions for experienced