Skip to content

Commit

Permalink
#264 Improved exception handling with IExceptionHandler (#276)
Browse files Browse the repository at this point in the history
  • Loading branch information
nkz-soft authored Aug 27, 2024
1 parent 23c03ae commit 200059d
Show file tree
Hide file tree
Showing 12 changed files with 72 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public sealed class LoggingBehaviour<TRequest> : IRequestPreProcessor<TRequest>
{
private readonly ILogger _logger;

public LoggingBehaviour(ILogger<TRequest> logger, ICurrentUserService currentUserService) => _logger = logger.ThrowIfNull();
public LoggingBehaviour(ILogger<LoggingBehaviour<TRequest>> logger, ICurrentUserService currentUserService) => _logger = logger.ThrowIfNull();

public async Task Process(TRequest request, CancellationToken cancellationToken)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ public sealed class PerformanceBehaviour<TRequest, TResponse> : IPipelineBehavio
where TRequest : IRequest<TResponse>
{
private readonly Stopwatch _timer;
private readonly ILogger<TRequest> _logger;
private readonly ILogger<PerformanceBehaviour<TRequest, TResponse>> _logger;

public PerformanceBehaviour(ILogger<TRequest> logger)
public PerformanceBehaviour(ILogger<PerformanceBehaviour<TRequest, TResponse>> logger)
{
_logger = logger.ThrowIfNull();
_timer = new Stopwatch();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@

public sealed class UnhandledExceptionBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : notnull, IRequest<TResponse>
{
private readonly ILogger<TRequest> _logger;
private readonly ILogger<UnhandledExceptionBehaviour<TRequest, TResponse>> _logger;

public UnhandledExceptionBehaviour(ILogger<TRequest> logger) => _logger = logger.ThrowIfNull();
public UnhandledExceptionBehaviour(ILogger<UnhandledExceptionBehaviour<TRequest, TResponse>> logger) => _logger = logger.ThrowIfNull();

public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
try
{
return await next();
}
catch (Exception ex)
catch (Exception)
{
_logger.UnhandledExceptionRequest(request.ToString(), ex);
_logger.UnhandledExceptionRequest(request.ToString());
throw;
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/NKZSoft.Template.Application/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ public static class DependencyInjection
{
public static IServiceCollection AddApplication(this IServiceCollection services)
{
services.ThrowIfNull();

services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly(), ServiceLifetime.Scoped, null, true);
services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssemblies(Assembly.GetExecutingAssembly()));


var typeAdapterConfig = TypeAdapterConfig.GlobalSettings;
typeAdapterConfig.Scan(Assembly.GetExecutingAssembly());

Expand Down
14 changes: 0 additions & 14 deletions src/NKZSoft.Template.Common/EventIds.cs

This file was deleted.

49 changes: 15 additions & 34 deletions src/NKZSoft.Template.Common/Extensions/LoggerExtension.cs
Original file line number Diff line number Diff line change
@@ -1,44 +1,25 @@
namespace NKZSoft.Template.Common.Extensions;

internal static class LoggerExtension
public static partial class LoggerExtension
{
private static readonly Action<ILogger, string, Exception?> _consumeIntegrationEvent = LoggerMessage.Define<string>(
LogLevel.Information,
EventIds.ConsumeIntegrationEvent,
"Integration event has been consumed: `{Message}`.");
[LoggerMessage(1, LogLevel.Information, "Integration event has been consumed: {Message}.")]
internal static partial void ConsumeIntegrationEvent(this ILogger logger, string message);

private static readonly Action<ILogger, string, Exception?> _raiseIntegrationEvent = LoggerMessage.Define<string>(
LogLevel.Information,
EventIds.RaiseIntegrationEvent,
"Domain event has been raised: `{Message}`.");
[LoggerMessage(2, LogLevel.Information, "Domain event has been raised: {Message}.")]
public static partial void RaiseIntegrationEvent(this ILogger logger, string message);

private static readonly Action<ILogger, long, string, Exception?> _longRunningRequest = LoggerMessage.Define<long, string>(
LogLevel.Warning,
EventIds.LongRunningRequest,
"Long running request: `{ElapsedMilliseconds}` milliseconds `{Request}.`");
[LoggerMessage(3, LogLevel.Warning, "Long running request: {ElapsedMilliseconds} milliseconds {Request}.")]
public static partial void LongRunningRequest(this ILogger logger, long elapsedMilliseconds, string request);

private static readonly Action<ILogger, string?, Exception?> _unhandledExceptionRequest = LoggerMessage.Define<string?>(
LogLevel.Error,
EventIds.UnhandledExceptionRequest,
"Unhandled exception has occured for request: `{Request}.`");
[LoggerMessage(4, LogLevel.Error, "Unhandled exception has occured for request: `{Request}.")]
public static partial void UnhandledExceptionRequest(this ILogger logger, string? request);

private static readonly Action<ILogger, string?, Exception?> _loggingRequest = LoggerMessage.Define<string?>(
LogLevel.Error,
EventIds.LoggingRequest,
"Request has executed: `{Request}.`");
[LoggerMessage(5, LogLevel.Error, "Request has executed: `{Request}.")]
public static partial void LoggingRequest(this ILogger logger, string? request);

public static void ConsumeIntegrationEvent(this ILogger logger, string message)
=> _consumeIntegrationEvent(logger, message, null);
[LoggerMessage(6, LogLevel.Error, "An error occurred while migrating or initializing the database.")]
public static partial void MigrationError(this ILogger logger, Exception ex);

public static void RaiseIntegrationEvent(this ILogger logger, string message)
=> _raiseIntegrationEvent(logger, message, null);

public static void LongRunningRequest(this ILogger logger, long elapsedMilliseconds, string request)
=> _longRunningRequest(logger, elapsedMilliseconds, request, null);

public static void UnhandledExceptionRequest(this ILogger logger, string? request, Exception? ex)
=> _unhandledExceptionRequest(logger, request, ex);

public static void LoggingRequest(this ILogger logger, string? request)
=> _loggingRequest(logger, request, null);
[LoggerMessage(7, LogLevel.Error, "Application: An unhandled exception has occurred.")]
public static partial void ApplicationUnhandledException(this ILogger logger, Exception ex);
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
namespace NKZSoft.Template.Presentation.Rest.Extensions;

using Middleware;

public static class ApplicationBuilderExtension
{
public static IApplicationBuilder UseRestPresentation(
this IApplicationBuilder app, IConfiguration configuration, IWebHostEnvironment env)
{
app.ThrowIfNull();
configuration.ThrowIfNull();
env.ThrowIfNull();

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseSwagger(configuration);

app.UseCors("CorsPolicy");

app.UseMiddleware(typeof(ErrorHandlingMiddleware));
app.UseSwagger(configuration)
.UseCors("CorsPolicy")
.UseExceptionHandler();

return app;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace NKZSoft.Template.Presentation.Rest.Extensions;

using Filters;
using Middleware;

public static class ServiceCollectionExtension
{
Expand All @@ -22,6 +23,8 @@ public static IServiceCollection AddRestPresentation(
services.AddHttpContextAccessor()
.AddSwagger(configuration, Assembly.GetExecutingAssembly())
.AddValidatorsFromAssemblyContaining<IApplicationDbContext>(ServiceLifetime.Scoped, null, true)
.AddExceptionHandler<GlobalExceptionHandler>()
.AddProblemDetails()
.AddControllers(options => options.Filters.Add<CustomExceptionFilterAttribute>())
.AddApplicationPart(Assembly.GetExecutingAssembly());

Expand Down
1 change: 1 addition & 0 deletions src/NKZSoft.Template.Presentation.Rest/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
global using FluentResults;
global using FluentValidation;
global using Microsoft.AspNetCore.Builder;
global using Microsoft.AspNetCore.Diagnostics;
global using Microsoft.AspNetCore.Mvc.ModelBinding;
global using Microsoft.AspNetCore.Routing;
global using NKZSoft.Template.Application.Common.Interfaces;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace NKZSoft.Template.Presentation.Rest.Middleware;

using Common.Extensions;
using ILogger = Microsoft.Extensions.Logging.ILogger;

public class ErrorHandlingMiddleware
Expand Down Expand Up @@ -27,9 +28,7 @@ public async Task Invoke(HttpContext httpContext)

private static async Task HandleExceptionAsync(HttpContext context, ILogger log, Exception exception)
{
#pragma warning disable CA1848
log.LogError(exception, "Application: An unhandled exception has occurred");
#pragma warning restore CA1848
log.ApplicationUnhandledException(exception);

const HttpStatusCode code = HttpStatusCode.InternalServerError;
var resultDto = new ResultDto<Unit>(Unit.Value, false, new[] { new ErrorDto(exception.Message, code.ToString()) });
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace NKZSoft.Template.Presentation.Rest.Middleware;

using Common.Extensions;

public class GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger) : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
{
logger.ApplicationUnhandledException(exception);

var problemDetails = new ProblemDetails { Instance = httpContext.Request.Path };
if (exception is ValidationException fluentException)
{
problemDetails.Title = "one or more validation errors occurred.";
problemDetails.Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1";
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
var validationErrors =
fluentException.Errors.Select(error => error.ErrorMessage).ToList();
problemDetails.Extensions.Add("errors", validationErrors);
}
else
{
problemDetails.Title = exception.Message;
}

problemDetails.Status = httpContext.Response.StatusCode;
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken).ConfigureAwait(false);
return true;
}
}
10 changes: 5 additions & 5 deletions src/NKZSoft.Template.Presentation.Starter/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using NKZSoft.Template.Common.Extensions;

var builder = WebApplication.CreateBuilder(args);

builder.Configuration
Expand Down Expand Up @@ -36,12 +38,12 @@
.AddHealthChecks();

builder.Services.AddOpenTelemetry()
.ConfigureResource(builder => builder
.ConfigureResource(b => b
.AddService(
serviceName: Assembly.GetExecutingAssembly().GetName().Name!,
serviceVersion: Assembly.GetExecutingAssembly().GetName().Version?.ToString(),
serviceInstanceId: Environment.MachineName))
.WithTracing(builder => builder
.WithTracing(b => b
.AddAspNetCoreInstrumentation(options =>
{
options.RecordException = true;
Expand All @@ -66,9 +68,7 @@
catch (Exception ex)
{
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
#pragma warning disable CA1848
logger.LogError(ex, "An error occurred while migrating or initializing the database.");
#pragma warning restore CA1848
logger.MigrationError(ex);
}
}

Expand Down

0 comments on commit 200059d

Please sign in to comment.