Skip to content

Commit 7c358e0

Browse files
committed
Add exception and model validation filters for improved error handling
1 parent 7d57c17 commit 7c358e0

17 files changed

Lines changed: 371 additions & 184 deletions

ManagedCode.Communication.Extensions/CommunicationHubFilter.cs

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

ManagedCode.Communication.Extensions/CommunicationMiddleware.cs

Lines changed: 0 additions & 42 deletions
This file was deleted.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Net;
5+
using System.Security;
6+
using System.Text.Json;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using System.Xml;
10+
using Microsoft.AspNetCore.Mvc;
11+
using Microsoft.AspNetCore.Mvc.Filters;
12+
using Microsoft.Extensions.Logging;
13+
14+
namespace ManagedCode.Communication.Extensions;
15+
16+
public abstract class ExceptionFilterBase(ILogger logger) : IExceptionFilter
17+
{
18+
protected readonly ILogger Logger = logger ?? throw new ArgumentNullException(nameof(logger));
19+
20+
public virtual void OnException(ExceptionContext context)
21+
{
22+
try
23+
{
24+
var exception = context.Exception;
25+
var actionName = context.ActionDescriptor.DisplayName;
26+
var controllerName = context.ActionDescriptor.RouteValues["controller"] ?? "Unknown";
27+
28+
Logger.LogError(exception, "Unhandled exception in {ControllerName}.{ActionName}",
29+
controllerName, actionName);
30+
31+
var statusCode = GetStatusCodeForException(exception);
32+
var problem = new Problem()
33+
{
34+
Title = exception.GetType().Name,
35+
Detail = exception.Message,
36+
Status = (int)statusCode,
37+
Instance = context.HttpContext.Request.Path,
38+
Extensions =
39+
{
40+
["traceId"] = context.HttpContext.TraceIdentifier
41+
}
42+
};
43+
44+
var result = Result<Problem>.Fail(exception.Message, problem);
45+
46+
context.Result = new ObjectResult(result)
47+
{
48+
StatusCode = (int)statusCode
49+
};
50+
51+
context.ExceptionHandled = true;
52+
53+
Logger.LogInformation("Exception handled by {FilterType} for {ControllerName}.{ActionName}",
54+
GetType().Name, controllerName, actionName);
55+
}
56+
catch (Exception ex)
57+
{
58+
Logger.LogError(ex, "Error occurred while handling exception in {FilterType}", GetType().Name);
59+
60+
var fallbackProblem = new Problem
61+
{
62+
Title = "An unexpected error occurred",
63+
Status = (int)HttpStatusCode.InternalServerError,
64+
Instance = context.HttpContext.Request.Path
65+
};
66+
67+
context.Result = new ObjectResult(Result<Problem>.Fail("An unexpected error occurred", fallbackProblem))
68+
{
69+
StatusCode = (int)HttpStatusCode.InternalServerError
70+
};
71+
context.ExceptionHandled = true;
72+
}
73+
}
74+
75+
protected virtual HttpStatusCode GetStatusCodeForException(Exception exception)
76+
{
77+
return exception switch
78+
{
79+
ArgumentException => HttpStatusCode.BadRequest,
80+
InvalidOperationException => HttpStatusCode.BadRequest,
81+
NotSupportedException => HttpStatusCode.BadRequest,
82+
FormatException => HttpStatusCode.BadRequest,
83+
JsonException => HttpStatusCode.BadRequest,
84+
XmlException => HttpStatusCode.BadRequest,
85+
86+
UnauthorizedAccessException => HttpStatusCode.Unauthorized,
87+
88+
SecurityException => HttpStatusCode.Forbidden,
89+
90+
FileNotFoundException => HttpStatusCode.NotFound,
91+
DirectoryNotFoundException => HttpStatusCode.NotFound,
92+
KeyNotFoundException => HttpStatusCode.NotFound,
93+
94+
TimeoutException => HttpStatusCode.RequestTimeout,
95+
TaskCanceledException => HttpStatusCode.RequestTimeout,
96+
OperationCanceledException => HttpStatusCode.RequestTimeout,
97+
98+
InvalidDataException => HttpStatusCode.Conflict,
99+
100+
NotImplementedException => HttpStatusCode.NotImplemented,
101+
NotFiniteNumberException => HttpStatusCode.InternalServerError,
102+
OutOfMemoryException => HttpStatusCode.InternalServerError,
103+
StackOverflowException => HttpStatusCode.InternalServerError,
104+
ThreadAbortException => HttpStatusCode.InternalServerError,
105+
106+
_ => HttpStatusCode.InternalServerError
107+
};
108+
}
109+
}

ManagedCode.Communication.Extensions/Extensions/CommunicationAppBuilderExtensions.cs

Lines changed: 13 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,14 @@ public static IApplicationBuilder UseCommunication(this IApplicationBuilder app)
1014
if (app == null)
1115
throw new ArgumentNullException(nameof(app));
1216

13-
return app.UseMiddleware<CommunicationMiddleware>();
17+
var serviceProvider = app.ApplicationServices;
18+
var exceptionFilter = serviceProvider.GetRequiredService<ExceptionFilterBase>();
19+
var modelValidationFilter = serviceProvider.GetRequiredService<ModelValidationFilterBase>();
20+
21+
var mvcOptions = serviceProvider.GetRequiredService<IOptions<MvcOptions>>();
22+
mvcOptions.Value.Filters.Add(exceptionFilter);
23+
mvcOptions.Value.Filters.Add(modelValidationFilter);
24+
25+
return app;
1426
}
1527
}
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
}

ManagedCode.Communication.Extensions/Extensions/ServiceCollectionExtensions.cs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Diagnostics;
44
using Microsoft.AspNetCore.Http;
5+
using Microsoft.AspNetCore.SignalR;
56
using Microsoft.AspNetCore.WebUtilities;
67
using Microsoft.Extensions.DependencyInjection;
78
using Microsoft.Extensions.Hosting;
@@ -27,11 +28,11 @@ public static IHostApplicationBuilder AddCommunication(this IHostApplicationBuil
2728
public static class ServiceCollectionExtensions
2829
{
2930

30-
public static IServiceCollection AddCommunication(this IServiceCollection services, Action<CommunicationOptions> options)
31+
public static IServiceCollection AddCommunication(this IServiceCollection services, Action<CommunicationOptions>? configure = null)
3132
{
32-
services.AddOptions<CommunicationOptions>()
33-
.Configure(options);
34-
33+
if (configure != null)
34+
services.Configure(configure);
35+
3536
return services;
3637
}
3738

@@ -63,4 +64,28 @@ public static IServiceCollection AddCommunicationExceptionHandler(this IServiceC
6364
services.AddExceptionHandler<CommunicationExceptionHandler>();
6465
return services;
6566
}
67+
68+
public static IServiceCollection AddCommunicationFilters<TExceptionFilter, TModelValidationFilter, THubExceptionFilter>(
69+
this IServiceCollection services)
70+
where TExceptionFilter : ExceptionFilterBase
71+
where TModelValidationFilter : ModelValidationFilterBase
72+
where THubExceptionFilter : HubExceptionFilterBase
73+
{
74+
services.AddScoped<TExceptionFilter>();
75+
services.AddScoped<TModelValidationFilter>();
76+
services.AddScoped<THubExceptionFilter>();
77+
78+
services.AddControllers(options =>
79+
{
80+
options.Filters.Add<TExceptionFilter>();
81+
options.Filters.Add<TModelValidationFilter>();
82+
});
83+
84+
services.Configure<HubOptions>(options =>
85+
{
86+
options.AddFilter<THubExceptionFilter>();
87+
});
88+
89+
return services;
90+
}
6691
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using System.Threading;
4+
using Microsoft.AspNetCore.SignalR;
5+
using Microsoft.Extensions.Logging;
6+
using ManagedCode.Communication;
7+
8+
namespace ManagedCode.Communication.Extensions;
9+
10+
public abstract class HubExceptionFilterBase(ILogger logger) : IHubFilter
11+
{
12+
protected readonly ILogger Logger = logger ?? throw new ArgumentNullException(nameof(logger));
13+
14+
public async ValueTask<object?> InvokeMethodAsync(HubInvocationContext invocationContext,
15+
Func<HubInvocationContext, ValueTask<object?>> next)
16+
{
17+
try
18+
{
19+
return await next(invocationContext);
20+
}
21+
catch (Exception ex)
22+
{
23+
Logger.LogError(ex, invocationContext.Hub.GetType().Name + "." + invocationContext.HubMethodName);
24+
25+
var problem = new Problem
26+
{
27+
Title = ex.GetType().Name,
28+
Detail = ex.Message,
29+
Status = GetStatusCodeForException(ex),
30+
Instance = invocationContext.Hub.Context.ConnectionId,
31+
Extensions =
32+
{
33+
["hubMethod"] = invocationContext.HubMethodName,
34+
["hubType"] = invocationContext.Hub.GetType().Name
35+
}
36+
};
37+
38+
return Result<Problem>.Fail(ex.Message, problem);
39+
}
40+
}
41+
42+
protected virtual int GetStatusCodeForException(Exception exception)
43+
{
44+
return exception switch
45+
{
46+
ArgumentException or ArgumentNullException => 400,
47+
UnauthorizedAccessException => 401,
48+
InvalidOperationException => 400,
49+
NotSupportedException => 400,
50+
TimeoutException => 408,
51+
TaskCanceledException => 408,
52+
_ => 500
53+
};
54+
}
55+
}

0 commit comments

Comments
 (0)