Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#264 Improved exception handling with IExceptionHandler #276

Merged
merged 2 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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