diff --git a/DigitalData.Core.API/DIExtensions.cs b/DigitalData.Core.API/DIExtensions.cs index a1145b1..07e2230 100644 --- a/DigitalData.Core.API/DIExtensions.cs +++ b/DigitalData.Core.API/DIExtensions.cs @@ -1,53 +1,50 @@ -using Microsoft.AspNetCore.Builder; -using System.Configuration; +namespace DigitalData.Core.API; -namespace DigitalData.Core.API +/// +/// Provides extension methods for adding middleware to the application's request pipeline. +/// +public static class DIExtensions { /// - /// Provides extension methods for adding middleware to the application's request pipeline. + /// Adds the to the application's request pipeline to include + /// Content Security Policy (CSP) headers in the HTTP response. /// - public static class DIExtensions - { - /// - /// Adds the to the application's request pipeline to include - /// Content Security Policy (CSP) headers in the HTTP response. - /// - /// The application builder. - /// - /// The CSP policy string with placeholders. The first format parameter {0} will be replaced - /// by the nonce value. - /// - /// The application builder with the CSP middleware added. - public static IApplicationBuilder UseCSPMiddleware(this IApplicationBuilder app, string policy) - => app.UseMiddleware(policy); + /// The application builder. + /// + /// The CSP policy string with placeholders. The first format parameter {0} will be replaced + /// by the nonce value. + /// + /// The application builder with the CSP middleware added. + public static IApplicationBuilder UseCSPMiddleware(this IApplicationBuilder app, string policy) + => app.UseMiddleware(policy); - /// - /// Checks if the DiP (Development in Production) mode is enabled for the WebApplicationBuilder. - /// - /// The WebApplicationBuilder instance. - /// True if DiP mode is enabled; otherwise, false. - public static bool IsDiP(this WebApplicationBuilder builder) => builder.Configuration.GetValue("DiPMode"); + /// + /// Checks if the DiP (Development in Production) mode is enabled for the WebApplicationBuilder. + /// + /// The WebApplicationBuilder instance. + /// True if DiP mode is enabled; otherwise, false. + public static bool IsDiP(this WebApplicationBuilder builder) => builder.Configuration.GetValue("DiPMode"); - /// - /// Checks if the DiP (Development in Production) mode is enabled for the WebApplication. - /// - /// The WebApplication instance. - /// True if DiP mode is enabled; otherwise, false. - public static bool IsDiP(this WebApplication app) => app.Configuration.GetValue("DiPMode"); + /// + /// Checks if the DiP (Development in Production) mode is enabled for the WebApplication. + /// + /// The WebApplication instance. + /// True if DiP mode is enabled; otherwise, false. + public static bool IsDiP(this WebApplication app) => app.Configuration.GetValue("DiPMode"); - /// - /// Checks if the environment is Development or DiP (Development in Production) mode is enabled for the WebApplicationBuilder. - /// - /// The WebApplicationBuilder instance. - /// True if the environment is Development or DiP mode is enabled; otherwise, false. - public static bool IsDevOrDiP(this WebApplicationBuilder builder) => builder.Environment.IsDevelopment() || builder.IsDiP(); + /// + /// Checks if the environment is Development or DiP (Development in Production) mode is enabled for the WebApplicationBuilder. + /// + /// The WebApplicationBuilder instance. + /// True if the environment is Development or DiP mode is enabled; otherwise, false. + public static bool IsDevOrDiP(this WebApplicationBuilder builder) => builder.Environment.IsDevelopment() || builder.IsDiP(); - /// - /// Checks if the environment is Development or DiP (Development in Production) mode is enabled for the WebApplication. - /// - /// The WebApplication instance. - /// True if the environment is Development or DiP mode is enabled; otherwise, false. - public static bool IsDevOrDiP(this WebApplication app) => app.Environment.IsDevelopment() || app.IsDiP(); + /// + /// Checks if the environment is Development or DiP (Development in Production) mode is enabled for the WebApplication. + /// + /// The WebApplication instance. + /// True if the environment is Development or DiP mode is enabled; otherwise, false. + public static bool IsDevOrDiP(this WebApplication app) => app.Environment.IsDevelopment() || app.IsDiP(); /// /// Configures the services with options from the specified section of the appsettings.json file. @@ -65,5 +62,4 @@ namespace DigitalData.Core.API builder.Services.Configure(section); return builder; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/DigitalData.Core.API/DigitalData.Core.API.csproj b/DigitalData.Core.API/DigitalData.Core.API.csproj index 22ad268..054f693 100644 --- a/DigitalData.Core.API/DigitalData.Core.API.csproj +++ b/DigitalData.Core.API/DigitalData.Core.API.csproj @@ -33,6 +33,7 @@ + diff --git a/DigitalData.Core.API/ExceptionHandlingMiddleware.cs b/DigitalData.Core.API/ExceptionHandlingMiddleware.cs new file mode 100644 index 0000000..b02655d --- /dev/null +++ b/DigitalData.Core.API/ExceptionHandlingMiddleware.cs @@ -0,0 +1,84 @@ +namespace DigitalData.Core.API; + +using DigitalData.Core.Exceptions; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using System.Net; +using System.Text.Json; + +/// +/// Middleware for handling exceptions globally in the application. +/// Captures exceptions thrown during the request pipeline execution, +/// logs them, and returns an appropriate HTTP response with a JSON error message. +/// +public class ExceptionHandlingMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The next middleware in the request pipeline. + /// The logger instance for logging exceptions. + public ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + /// + /// Invokes the middleware to handle the HTTP request. + /// + /// The HTTP context of the current request. + /// A task that represents the asynchronous operation. + public async Task InvokeAsync(HttpContext context) + { + try + { + await _next(context); // Continue down the pipeline + } + catch (Exception ex) + { + await HandleExceptionAsync(context, ex, _logger); + } + } + + /// + /// Handles exceptions by logging them and writing an appropriate JSON response. + /// + /// The HTTP context of the current request. + /// The exception that occurred. + /// The logger instance for logging the exception. + /// A task that represents the asynchronous operation. + private static async Task HandleExceptionAsync(HttpContext context, Exception exception, ILogger logger) + { + context.Response.ContentType = "application/json"; + + string message; + + switch (exception) + { + case BadRequestException badRequestEx: + context.Response.StatusCode = (int)HttpStatusCode.BadRequest; + message = badRequestEx.Message; + break; + + case NotFoundException notFoundEx: + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + message = notFoundEx.Message; + break; + + default: + logger.LogError(exception, "Unhandled exception occurred."); + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + message = "An unexpected error occurred."; + break; + } + + await context.Response.WriteAsync(JsonSerializer.Serialize(new + { + message + })); + } +} diff --git a/DigitalData.Core.Exceptions/BadRequestException.cs b/DigitalData.Core.Exceptions/BadRequestException.cs new file mode 100644 index 0000000..bd2ff27 --- /dev/null +++ b/DigitalData.Core.Exceptions/BadRequestException.cs @@ -0,0 +1,22 @@ +namespace DigitalData.Core.Exceptions; + +/// +/// Represents an exception that is thrown when a bad request is encountered. +/// +public class BadRequestException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public BadRequestException() + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public BadRequestException(string? message) : base(message) + { + } +} diff --git a/DigitalData.Core.Exceptions/DigitalData.Core.Exceptions.csproj b/DigitalData.Core.Exceptions/DigitalData.Core.Exceptions.csproj new file mode 100644 index 0000000..bae5b9f --- /dev/null +++ b/DigitalData.Core.Exceptions/DigitalData.Core.Exceptions.csproj @@ -0,0 +1,9 @@ + + + + net7.0;net8.0;net9.0 + enable + enable + + + diff --git a/DigitalData.Core.Exceptions/NotFoundException.cs b/DigitalData.Core.Exceptions/NotFoundException.cs new file mode 100644 index 0000000..7e7a0fa --- /dev/null +++ b/DigitalData.Core.Exceptions/NotFoundException.cs @@ -0,0 +1,22 @@ +namespace DigitalData.Core.Exceptions; + +/// +/// Represents an exception that is thrown when a requested resource is not found. +/// +public class NotFoundException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public NotFoundException() + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public NotFoundException(string? message) : base(message) + { + } +} diff --git a/DigitalData.Core.sln b/DigitalData.Core.sln index 3a9b04a..7ae921e 100644 --- a/DigitalData.Core.sln +++ b/DigitalData.Core.sln @@ -39,6 +39,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.Core.Infrastruc EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{41795B74-A757-4E93-B907-83BFF04EEE5C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.Core.Exceptions", "DigitalData.Core.Exceptions\DigitalData.Core.Exceptions.csproj", "{BAEF6CC9-4FE2-4E3F-9D32-911C9E8CCFB4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -100,6 +102,10 @@ Global {CE00E1F7-2771-4D9C-88FB-E564894E539E}.Debug|Any CPU.Build.0 = Release|Any CPU {CE00E1F7-2771-4D9C-88FB-E564894E539E}.Release|Any CPU.ActiveCfg = Release|Any CPU {CE00E1F7-2771-4D9C-88FB-E564894E539E}.Release|Any CPU.Build.0 = Release|Any CPU + {BAEF6CC9-4FE2-4E3F-9D32-911C9E8CCFB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BAEF6CC9-4FE2-4E3F-9D32-911C9E8CCFB4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BAEF6CC9-4FE2-4E3F-9D32-911C9E8CCFB4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BAEF6CC9-4FE2-4E3F-9D32-911C9E8CCFB4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -121,6 +127,7 @@ Global {C9266749-9504-4EA9-938F-F083357B60B7} = {72CBAFBA-55CC-49C9-A484-F8F4550054CB} {CE00E1F7-2771-4D9C-88FB-E564894E539E} = {41795B74-A757-4E93-B907-83BFF04EEE5C} {41795B74-A757-4E93-B907-83BFF04EEE5C} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {BAEF6CC9-4FE2-4E3F-9D32-911C9E8CCFB4} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8E2C3187-F848-493A-9E79-56D20DDCAC94}