Skip to content

Commit 477d6dc

Browse files
authored
Merge pull request #15 from managedcode/filters
Add exception and model validation filters for improved error handling
2 parents d60a217 + 8cbaa33 commit 477d6dc

24 files changed

+492
-144
lines changed

Directory.Build.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
<RepositoryUrl>https://github.com/managedcode/Communication</RepositoryUrl>
2525
<PackageProjectUrl>https://github.com/managedcode/Communication</PackageProjectUrl>
2626
<Product>Managed Code - Communication</Product>
27-
<Version>9.0.0</Version>
28-
<PackageVersion>9.0.0</PackageVersion>
27+
<Version>9.0.1</Version>
28+
<PackageVersion>9.0.1</PackageVersion>
2929

3030
</PropertyGroup>
3131
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">

ManagedCode.Communication.Extensions/CommunicationHubFilter.cs

Lines changed: 0 additions & 39 deletions
This file was deleted.

ManagedCode.Communication.Extensions/CommunicationMiddleware.cs

Lines changed: 0 additions & 54 deletions
This file was deleted.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
namespace ManagedCode.Communication.Extensions.Constants;
2+
3+
/// <summary>
4+
/// Constants for Problem details to avoid string literals throughout the codebase.
5+
/// </summary>
6+
public static class ProblemConstants
7+
{
8+
/// <summary>
9+
/// Problem titles
10+
/// </summary>
11+
public static class Titles
12+
{
13+
/// <summary>
14+
/// Title for validation failure problems
15+
/// </summary>
16+
public const string ValidationFailed = "Validation failed";
17+
18+
/// <summary>
19+
/// Title for unexpected error problems
20+
/// </summary>
21+
public const string UnexpectedError = "An unexpected error occurred";
22+
}
23+
24+
/// <summary>
25+
/// Problem extension keys
26+
/// </summary>
27+
public static class ExtensionKeys
28+
{
29+
/// <summary>
30+
/// Key for validation errors in problem extensions
31+
/// </summary>
32+
public const string ValidationErrors = "validationErrors";
33+
34+
/// <summary>
35+
/// Key for trace ID in problem extensions
36+
/// </summary>
37+
public const string TraceId = "traceId";
38+
39+
/// <summary>
40+
/// Key for hub method name in problem extensions
41+
/// </summary>
42+
public const string HubMethod = "hubMethod";
43+
44+
/// <summary>
45+
/// Key for hub type name in problem extensions
46+
/// </summary>
47+
public const string HubType = "hubType";
48+
49+
/// <summary>
50+
/// Key for inner exception in problem extensions
51+
/// </summary>
52+
public const string InnerException = "innerException";
53+
54+
/// <summary>
55+
/// Key for stack trace in problem extensions
56+
/// </summary>
57+
public const string StackTrace = "stackTrace";
58+
}
59+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System;
2+
using System.Net;
3+
using Microsoft.AspNetCore.Mvc;
4+
using Microsoft.AspNetCore.Mvc.Filters;
5+
using Microsoft.Extensions.Logging;
6+
using Microsoft.Extensions.Logging.Abstractions;
7+
using static ManagedCode.Communication.Extensions.Helpers.HttpStatusCodeHelper;
8+
using static ManagedCode.Communication.Extensions.Constants.ProblemConstants;
9+
10+
namespace ManagedCode.Communication.Extensions;
11+
12+
public abstract class ExceptionFilterBase : IExceptionFilter
13+
{
14+
protected readonly ILogger Logger = NullLogger<ExceptionFilterBase>.Instance;
15+
16+
public virtual void OnException(ExceptionContext context)
17+
{
18+
try
19+
{
20+
var exception = context.Exception;
21+
var actionName = context.ActionDescriptor.DisplayName;
22+
var controllerName = context.ActionDescriptor.RouteValues["controller"] ?? "Unknown";
23+
24+
Logger.LogError(exception, "Unhandled exception in {ControllerName}.{ActionName}",
25+
controllerName, actionName);
26+
27+
var statusCode = GetStatusCodeForException(exception);
28+
29+
var problem = new Problem()
30+
{
31+
Title = exception.GetType().Name,
32+
Detail = exception.Message,
33+
Status = statusCode,
34+
Instance = context.HttpContext.Request.Path,
35+
Extensions =
36+
{
37+
[ExtensionKeys.TraceId] = context.HttpContext.TraceIdentifier
38+
}
39+
};
40+
41+
var result = Result<Problem>.Fail(exception.Message, problem);
42+
43+
context.Result = new ObjectResult(result)
44+
{
45+
StatusCode = (int)statusCode
46+
};
47+
48+
context.ExceptionHandled = true;
49+
50+
Logger.LogInformation("Exception handled by {FilterType} for {ControllerName}.{ActionName}",
51+
GetType().Name, controllerName, actionName);
52+
}
53+
catch (Exception ex)
54+
{
55+
Logger.LogError(ex, "Error occurred while handling exception in {FilterType}", GetType().Name);
56+
57+
var fallbackProblem = new Problem
58+
{
59+
Title = Titles.UnexpectedError,
60+
Status = HttpStatusCode.InternalServerError,
61+
Instance = context.HttpContext.Request.Path
62+
};
63+
64+
context.Result = new ObjectResult(Result<Problem>.Fail(Titles.UnexpectedError, fallbackProblem))
65+
{
66+
StatusCode = (int)HttpStatusCode.InternalServerError
67+
};
68+
context.ExceptionHandled = true;
69+
}
70+
}
71+
}

ManagedCode.Communication.Extensions/Extensions/CommunicationAppBuilderExtensions.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
using System;
22
using Microsoft.AspNetCore.Builder;
3+
using Microsoft.AspNetCore.Mvc;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Microsoft.Extensions.Options;
6+
using ManagedCode.Communication.Extensions;
37

48
namespace ManagedCode.Communication.Extensions.Extensions;
59

@@ -10,6 +14,6 @@ public static IApplicationBuilder UseCommunication(this IApplicationBuilder app)
1014
if (app == null)
1115
throw new ArgumentNullException(nameof(app));
1216

13-
return app.UseMiddleware<CommunicationMiddleware>();
17+
return app;
1418
}
1519
}
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
using System;
12
using Microsoft.AspNetCore.SignalR;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using ManagedCode.Communication.Extensions;
25

36
namespace ManagedCode.Communication.Extensions.Extensions;
47

58
public static class HubOptionsExtensions
69
{
7-
public static void AddCommunicationHubFilter(this HubOptions result)
10+
public static void AddCommunicationHubFilter(this HubOptions result, IServiceProvider serviceProvider)
811
{
9-
result.AddFilter<CommunicationHubFilter>();
12+
var hubFilter = serviceProvider.GetRequiredService<HubExceptionFilterBase>();
13+
result.AddFilter(hubFilter);
1014
}
1115
}
Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,83 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using Microsoft.AspNetCore.Http;
5+
using Microsoft.AspNetCore.SignalR;
6+
using Microsoft.AspNetCore.WebUtilities;
27
using Microsoft.Extensions.DependencyInjection;
8+
using Microsoft.Extensions.Hosting;
9+
using static ManagedCode.Communication.Extensions.Constants.ProblemConstants;
310

411
namespace ManagedCode.Communication.Extensions.Extensions;
512

13+
14+
public static class HostApplicationBuilderExtensions
15+
{
16+
public static IHostApplicationBuilder AddCommunication(this IHostApplicationBuilder builder)
17+
{
18+
builder.Services.AddCommunication(options => options.ShowErrorDetails = builder.Environment.IsDevelopment());
19+
return builder;
20+
}
21+
22+
public static IHostApplicationBuilder AddCommunication(this IHostApplicationBuilder builder, Action<CommunicationOptions> config)
23+
{
24+
builder.Services.AddCommunication(config);
25+
return builder;
26+
}
27+
}
28+
629
public static class ServiceCollectionExtensions
730
{
8-
public static IServiceCollection AddCommunication(this IServiceCollection services,
9-
Action<CommunicationOptions> options)
31+
32+
public static IServiceCollection AddCommunication(this IServiceCollection services, Action<CommunicationOptions>? configure = null)
1033
{
11-
services.AddOptions<CommunicationOptions>().Configure(options);
34+
if (configure != null)
35+
services.Configure(configure);
36+
37+
return services;
38+
}
39+
40+
41+
42+
public static IServiceCollection AddDefaultProblemDetails(this IServiceCollection services)
43+
{
44+
services.AddProblemDetails(options =>
45+
{
46+
options.CustomizeProblemDetails = context =>
47+
{
48+
var statusCode = context.ProblemDetails.Status.GetValueOrDefault(StatusCodes.Status500InternalServerError);
49+
50+
context.ProblemDetails.Type ??= $"https://httpstatuses.io/{statusCode}";
51+
context.ProblemDetails.Title ??= ReasonPhrases.GetReasonPhrase(statusCode);
52+
context.ProblemDetails.Instance ??= context.HttpContext.Request.Path;
53+
context.ProblemDetails.Extensions.TryAdd(ExtensionKeys.TraceId, Activity.Current?.Id ?? context.HttpContext.TraceIdentifier);
54+
};
55+
});
56+
57+
return services;
58+
}
59+
60+
public static IServiceCollection AddCommunicationFilters<TExceptionFilter, TModelValidationFilter, THubExceptionFilter>(
61+
this IServiceCollection services)
62+
where TExceptionFilter : ExceptionFilterBase
63+
where TModelValidationFilter : ModelValidationFilterBase
64+
where THubExceptionFilter : HubExceptionFilterBase
65+
{
66+
services.AddScoped<TExceptionFilter>();
67+
services.AddScoped<TModelValidationFilter>();
68+
services.AddScoped<THubExceptionFilter>();
69+
70+
services.AddControllers(options =>
71+
{
72+
options.Filters.Add<TExceptionFilter>();
73+
options.Filters.Add<TModelValidationFilter>();
74+
});
75+
76+
services.Configure<HubOptions>(options =>
77+
{
78+
options.AddFilter<THubExceptionFilter>();
79+
});
80+
1281
return services;
1382
}
1483
}

0 commit comments

Comments
 (0)