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
- IExceptionHandler
Interface
- Specific
Handlers (UnauthorizedAccessExceptionHandler, NotFoundExceptionHandler,
etc.)
- ErrorResponseFactory
(Updated to use handlers)
- ExceptionMiddleware
(Updated to use ErrorResponseFactory)
- DI
Configuration (In Program.cs)
How This Implements OCP:
- 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.
- 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
Post a Comment