Open/Closed Principle (OCP) by using the Strategy Pattern combined with Dependency Injection (DI)

Open/Closed Principle (OCP) by using the Strategy Pattern combined with Dependency Injection (DI): 


This implementation of the Open/Closed Principle (OCP) applied to error handling system.

 

Step 1: Create IExceptionHandler Interface

The IExceptionHandler interface defines the contract for all exception handlers. Each handler should implement this interface.

public interface IExceptionHandler

{

    bool CanHandle(Exception exception);

    ErrorResponse Handle(HttpContext context, Exception exception);

}

 

Step 2: Implement Specific Exception Handlers

Each exception handler implements the IExceptionHandler interface. Here's how we can implement specific exception handlers for different types of exceptions.

Unauthorized Access Handler

public class UnauthorizedAccessExceptionHandler : IExceptionHandler

{

    public bool CanHandle(Exception exception)

    {

        return exception is UnauthorizedAccessException;

    }

 

    public ErrorResponse Handle(HttpContext context, Exception exception)

    {

        context.Response.StatusCode = 401;

        return new ErrorResponse

        {

            StatusCode = 401,

            ErrorCode = ErrorCodes.Unauthorized,

            Message = "Access denied.",

            UserMessage = "You are not authorized to perform this action.",

            Timestamp = DateTime.UtcNow,

            TraceId = context.Request.Headers["X-Request-ID"].ToString() ?? Guid.NewGuid().ToString()

        };

    }

}

Not Found Handler

public class NotFoundExceptionHandler : IExceptionHandler

{

    public bool CanHandle(Exception exception)

    {

        return exception is KeyNotFoundException;

    }

 

    public ErrorResponse Handle(HttpContext context, Exception exception)

    {

        context.Response.StatusCode = 404;

        return new ErrorResponse

        {

            StatusCode = 404,

            ErrorCode = ErrorCodes.NotFound,

            Message = "Resource not found.",

            UserMessage = "The requested resource was not found.",

            Timestamp = DateTime.UtcNow,

            TraceId = context.Request.Headers["X-Request-ID"].ToString() ?? Guid.NewGuid().ToString()

        };

    }

}

Invalid Operation Handler

public class InvalidOperationExceptionHandler : IExceptionHandler

{

    public bool CanHandle(Exception exception)

    {

        return exception is InvalidOperationException;

    }

 

    public ErrorResponse Handle(HttpContext context, Exception exception)

    {

        context.Response.StatusCode = 409;

        return new ErrorResponse

        {

            StatusCode = 409,

            ErrorCode = ErrorCodes.Conflict,

            Message = "Request conflict.",

            UserMessage = "There was a conflict with the request.",

            Timestamp = DateTime.UtcNow,

            TraceId = context.Request.Headers["X-Request-ID"].ToString() ?? Guid.NewGuid().ToString()

        };

    }

}

You can create additional handlers for other exceptions like ValidationException, RateLimitExceededException, etc., following the same pattern.

 

Step 3: Modify the ErrorResponseFactory to Use Exception Handlers

Instead of the ErrorResponseFactory handling the exception types directly, we will delegate the responsibility to the appropriate handler.

public class ErrorResponseFactory

{

    private readonly IHostEnvironment _environment;

    private readonly IEnumerable<IExceptionHandler> _exceptionHandlers;

 

    public ErrorResponseFactory(IHostEnvironment environment, IEnumerable<IExceptionHandler> exceptionHandlers)

    {

        _environment = environment ?? throw new ArgumentNullException(nameof(environment));

        _exceptionHandlers = exceptionHandlers ?? throw new ArgumentNullException(nameof(exceptionHandlers));

    }

 

    public ErrorResponse CreateErrorResponse(HttpContext context, Exception exception)

    {

        // Find the handler that can handle this exception

        var handler = _exceptionHandlers.FirstOrDefault(h => h.CanHandle(exception));

 

        if (handler != null)

        {

            return handler.Handle(context, exception);

        }

 

        // Default error response for unknown exceptions

        context.Response.StatusCode = 500;

        return new ErrorResponse

        {

            StatusCode = 500,

            ErrorCode = ErrorCodes.Unknown,

            Message = exception.Message,

            UserMessage = "An internal error occurred. Please try again later.",

            Timestamp = DateTime.UtcNow,

            TraceId = context.Request.Headers["X-Request-ID"].ToString() ?? Guid.NewGuid().ToString()

        };

    }

}

 

Step 4: Update ExceptionMiddleware to Use the Refactored ErrorResponseFactory

The ExceptionMiddleware class will delegate exception handling to the ErrorResponseFactory.

public class ExceptionMiddleware

{

    private readonly RequestDelegate _next;

    private readonly ExceptionLogger _logger;

    private readonly ErrorResponseFactory _errorFactory;

 

    public ExceptionMiddleware(RequestDelegate next, ExceptionLogger logger, ErrorResponseFactory errorFactory)

    {

        _next = next ?? throw new ArgumentNullException(nameof(next));

        _logger = logger ?? throw new ArgumentNullException(nameof(logger));

        _errorFactory = errorFactory ?? throw new ArgumentNullException(nameof(errorFactory));

    }

 

    public async Task InvokeAsync(HttpContext httpContext)

    {

        try

        {

            await _next(httpContext).ConfigureAwait(false);

        }

        catch (Exception ex)

        {

            _logger.LogException(httpContext, ex);

            await HandleExceptionAsync(httpContext, ex).ConfigureAwait(false);

        }

    }

 

    private async Task HandleExceptionAsync(HttpContext context, Exception exception)

    {

        context.Response.ContentType = "application/json";

 

        var errorResponse = _errorFactory.CreateErrorResponse(context, exception);

 

        var responseJson = JsonSerializer.Serialize(errorResponse, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });

        await context.Response.WriteAsync(responseJson).ConfigureAwait(false);

    }

}

 

Step 5: Register Handlers in Dependency Injection (DI)

In Program.cs (or Startup.cs), register the exception handlers and other services in the DI container.

public void ConfigureServices(IServiceCollection services)

{

    // Register the exception handlers

    services.AddSingleton<IExceptionHandler, UnauthorizedAccessExceptionHandler>();

    services.AddSingleton<IExceptionHandler, NotFoundExceptionHandler>();

    services.AddSingleton<IExceptionHandler, InvalidOperationExceptionHandler>(); // Add more handlers as needed

 

    // Register the ErrorResponseFactory and ExceptionLogger

    services.AddSingleton<ErrorResponseFactory>();

    services.AddSingleton<ExceptionLogger>();

 

    // Other services...

}

Complete Example Code Structure

  1. IExceptionHandler Interface
  2. Specific Handlers (UnauthorizedAccessExceptionHandler, NotFoundExceptionHandler, etc.)
  3. ErrorResponseFactory (Updated to use handlers)
  4. ExceptionMiddleware (Updated to use ErrorResponseFactory)
  5. DI Configuration (In Program.cs)

How This Implements OCP:

  1. Open for Extension: You can now easily add a new exception handler class by creating a class that implements IExceptionHandler and registering it in the DI container. You don’t need to modify the existing code in ErrorResponseFactory or ExceptionMiddleware.
  2. Closed for Modification: Existing code (like ErrorResponseFactory and ExceptionMiddleware) doesn’t need to change when new exception types are added. You just need to add new handlers.

Example of Adding a New Exception Handler:

If you wanted to add a new handler for ArgumentException, you'd do it like this:

public class ArgumentExceptionHandler : IExceptionHandler

{

    public bool CanHandle(Exception exception)

    {

        return exception is ArgumentException;

    }

 

    public ErrorResponse Handle(HttpContext context, Exception exception)

    {

        context.Response.StatusCode = 400; // Bad Request

        return new ErrorResponse

        {

            StatusCode = 400,

            ErrorCode = ErrorCodes.BadRequest,

            Message = "Invalid argument.",

            UserMessage = "There is a problem with the input parameters.",

            Timestamp = DateTime.UtcNow,

            TraceId = context.Request.Headers["X-Request-ID"].ToString() ?? Guid.NewGuid().ToString()

        };

    }

}

Then register it in the DI container:

public void ConfigureServices(IServiceCollection services)

{

    services.AddSingleton<IExceptionHandler, ArgumentExceptionHandler>();

    // Other handlers...

}

This implementation follows the Open/Closed Principle and allows easy extension of exception handling logic without modifying existing code.

 


Comments

Popular posts from this blog

Multiline to singleline IN C# - CODING

EF Core interview questions for beginners

EF Core interview questions for experienced