From 7d57c1773d64a6385730964ba50c79398ac77606 Mon Sep 17 00:00:00 2001 From: ksemenenko Date: Mon, 4 Aug 2025 10:02:43 +0200 Subject: [PATCH 1/6] wip --- Directory.Build.props | 4 +- .../CommunicationMiddleware.cs | 20 ++---- .../Extensions/ServiceCollectionExtensions.cs | 58 +++++++++++++++- ManagedCode.Communication.Extensions/sdf.cs | 57 ++++++++++++++++ .../CollectionResultT/CollectionResult.cs | 13 ++-- ManagedCode.Communication/Error.cs | 67 +++++++++++++++++++ ManagedCode.Communication/IResultError.cs | 4 +- ManagedCode.Communication/Result/Result.cs | 12 ++-- ManagedCode.Communication/ResultT/Result.cs | 13 ++-- 9 files changed, 213 insertions(+), 35 deletions(-) create mode 100644 ManagedCode.Communication.Extensions/sdf.cs diff --git a/Directory.Build.props b/Directory.Build.props index b65424c..4b15495 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -24,8 +24,8 @@ https://github.com/managedcode/Communication https://github.com/managedcode/Communication Managed Code - Communication - 9.0.0 - 9.0.0 + 9.0.1 + 9.0.1 diff --git a/ManagedCode.Communication.Extensions/CommunicationMiddleware.cs b/ManagedCode.Communication.Extensions/CommunicationMiddleware.cs index 5c0394e..1ad148c 100644 --- a/ManagedCode.Communication.Extensions/CommunicationMiddleware.cs +++ b/ManagedCode.Communication.Extensions/CommunicationMiddleware.cs @@ -8,29 +8,17 @@ namespace ManagedCode.Communication.Extensions; -public class CommunicationMiddleware +public class CommunicationMiddleware(ILogger logger, RequestDelegate next, IOptions options) { - private readonly ILogger _logger; - private readonly RequestDelegate _next; - private readonly IOptions _options; - - public CommunicationMiddleware(ILogger logger, RequestDelegate next, - IOptions options) - { - _logger = logger; - _next = next; - _options = options; - } - public async Task Invoke(HttpContext httpContext) { try { - await _next(httpContext); + await next(httpContext); } catch (Exception ex) { - _logger.LogError(ex, httpContext.Request.Method + "::" + httpContext.Request.Path); + logger.LogError(ex, httpContext.Request.Method + "::" + httpContext.Request.Path); if (httpContext.Response.HasStarted) throw; @@ -43,7 +31,7 @@ public async Task Invoke(HttpContext httpContext) httpContext.Response.ContentType = "application/json; charset=utf-8"; httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; - if (_options.Value.ShowErrorDetails) + if (options.Value.ShowErrorDetails) await httpContext.Response.WriteAsJsonAsync(Result.Fail(HttpStatusCode.InternalServerError, ex.Message)); else diff --git a/ManagedCode.Communication.Extensions/Extensions/ServiceCollectionExtensions.cs b/ManagedCode.Communication.Extensions/Extensions/ServiceCollectionExtensions.cs index c935f2e..3543b03 100644 --- a/ManagedCode.Communication.Extensions/Extensions/ServiceCollectionExtensions.cs +++ b/ManagedCode.Communication.Extensions/Extensions/ServiceCollectionExtensions.cs @@ -1,14 +1,66 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; namespace ManagedCode.Communication.Extensions.Extensions; + +public static class HostApplicationBuilderExtensions +{ + public static IHostApplicationBuilder AddCommunication(this IHostApplicationBuilder builder) + { + builder.Services.AddCommunication(options => options.ShowErrorDetails = builder.Environment.IsDevelopment()); + return builder; + } + + public static IHostApplicationBuilder AddCommunication(this IHostApplicationBuilder builder, Action config) + { + builder.Services.AddCommunication(config); + return builder; + } +} + public static class ServiceCollectionExtensions { - public static IServiceCollection AddCommunication(this IServiceCollection services, - Action options) + + public static IServiceCollection AddCommunication(this IServiceCollection services, Action options) + { + services.AddOptions() + .Configure(options); + + return services; + } + + + + public static IServiceCollection AddDefaultProblemDetails(this IServiceCollection services) { - services.AddOptions().Configure(options); + services.AddProblemDetails(options => + { + options.CustomizeProblemDetails = context => + { + var statusCode = context.ProblemDetails.Status.GetValueOrDefault(StatusCodes.Status500InternalServerError); + + context.ProblemDetails.Type ??= $"https://httpstatuses.io/{statusCode}"; + context.ProblemDetails.Title ??= ReasonPhrases.GetReasonPhrase(statusCode); + context.ProblemDetails.Instance ??= context.HttpContext.Request.Path; + context.ProblemDetails.Extensions.TryAdd("traceId", Activity.Current?.Id ?? context.HttpContext.TraceIdentifier); + }; + }); + + return services; + } + + public static IServiceCollection AddCommunicationExceptionHandler(this IServiceCollection services) + { + // Ensures that the ProblemDetails service is registered. + services.AddProblemDetails(); + + services.AddExceptionHandler(); return services; } } \ No newline at end of file diff --git a/ManagedCode.Communication.Extensions/sdf.cs b/ManagedCode.Communication.Extensions/sdf.cs new file mode 100644 index 0000000..e520b9c --- /dev/null +++ b/ManagedCode.Communication.Extensions/sdf.cs @@ -0,0 +1,57 @@ +using System; +using System.Diagnostics; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using ManagedCode.Communication.Extensions.Extensions; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace ManagedCode.Communication.Extensions; + +internal class CommunicationExceptionHandler(IProblemDetailsService problemDetailsService, IWebHostEnvironment webHostEnvironment) : IExceptionHandler +{ + public async ValueTask TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken) + { + var problemDetails = new ProblemDetails + { + Status = httpContext.Response.StatusCode, + Title = exception.GetType().FullName, + Detail = exception.Message, + Instance = httpContext.Request.Path, + Extensions = + { + ["traceId"] = Activity.Current?.Id ?? httpContext.TraceIdentifier + } + }; + + if (exception.InnerException is not null) + { + problemDetails.Extensions["innerException"] = new + { + Title = exception.InnerException.GetType().FullName, + Detail = exception.InnerException.Message + }; + } + + if (webHostEnvironment.IsDevelopment()) + { + problemDetails.Extensions["stackTrace"] = exception.StackTrace; + } + + await problemDetailsService.WriteAsync(new() + { + HttpContext = httpContext, + AdditionalMetadata = httpContext.Features.Get()?.Endpoint?.Metadata, + ProblemDetails = problemDetails, + Exception = exception + }); + + return true; + } +} \ No newline at end of file diff --git a/ManagedCode.Communication/CollectionResultT/CollectionResult.cs b/ManagedCode.Communication/CollectionResultT/CollectionResult.cs index 5298375..d5937b4 100644 --- a/ManagedCode.Communication/CollectionResultT/CollectionResult.cs +++ b/ManagedCode.Communication/CollectionResultT/CollectionResult.cs @@ -44,14 +44,18 @@ public void AddError(Error error) } } - public void ThrowIfFail() + [MemberNotNullWhen(false, nameof(Collection))] + public bool ThrowIfFail() { + if(IsSuccess) + return false; + if (Errors?.Any() is not true) { if(IsFailed) throw new Exception(nameof(IsFailed)); - return; + return false; } var exceptions = Errors.Select(s => s.Exception() ?? new Exception(StringExtension.JoinFilter(';', s.ErrorCode, s.Message))); @@ -62,14 +66,15 @@ public void ThrowIfFail() throw new AggregateException(exceptions); } - public void ThrowIfFailWithStackPreserved() + [MemberNotNullWhen(false, nameof(Collection))] + public bool ThrowIfFailWithStackPreserved() { if (Errors?.Any() is not true) { if (IsFailed) throw new Exception(nameof(IsFailed)); - return; + return false; } var exceptions = Errors.Select(s => s.ExceptionInfo() ?? ExceptionDispatchInfo.Capture(new Exception(StringExtension.JoinFilter(';', s.ErrorCode, s.Message)))); diff --git a/ManagedCode.Communication/Error.cs b/ManagedCode.Communication/Error.cs index a2cc36a..9df282c 100644 --- a/ManagedCode.Communication/Error.cs +++ b/ManagedCode.Communication/Error.cs @@ -1,10 +1,77 @@ using System; +using System.Collections.Generic; using System.Runtime.ExceptionServices; using System.Text.Json.Serialization; namespace ManagedCode.Communication; +/// +/// A machine-readable format for specifying errors in HTTP API responses based on . +/// +public class Problem +{ + /// + /// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when + /// dereferenced, it provide human-readable documentation for the problem type + /// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be + /// "about:blank". + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyOrder(-5)] + [JsonPropertyName("type")] + public string? Type { get; set; } + + /// + /// A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to occurrence + /// of the problem, except for purposes of localization(e.g., using proactive content negotiation; + /// see[RFC7231], Section 3.4). + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyOrder(-4)] + [JsonPropertyName("title")] + public string? Title { get; set; } + + /// + /// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyOrder(-3)] + [JsonPropertyName("status")] + public int? Status { get; set; } + + /// + /// A human-readable explanation specific to this occurrence of the problem. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyOrder(-2)] + [JsonPropertyName("detail")] + public string? Detail { get; set; } + + /// + /// A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyOrder(-1)] + [JsonPropertyName("instance")] + public string? Instance { get; set; } + + /// + /// Gets the for extension members. + /// + /// Problem type definitions MAY extend the problem details object with additional members. Extension members appear in the same namespace as + /// other members of a problem type. + /// + /// + /// + /// The round-tripping behavior for is determined by the implementation of the Input \ Output formatters. + /// In particular, complex types or collection types may not round-trip to the original type when using the built-in JSON or XML formatters. + /// + [JsonExtensionData] + public IDictionary Extensions { get; set; } = new Dictionary(StringComparer.Ordinal); +} + + public struct Error { diff --git a/ManagedCode.Communication/IResultError.cs b/ManagedCode.Communication/IResultError.cs index 0736806..d1b8e91 100644 --- a/ManagedCode.Communication/IResultError.cs +++ b/ManagedCode.Communication/IResultError.cs @@ -22,12 +22,12 @@ public interface IResultError /// /// Throws an exception if the result indicates a failure. /// - void ThrowIfFail(); + bool ThrowIfFail(); /// /// Throws an exception with stack trace preserved if the result indicates a failure. /// - void ThrowIfFailWithStackPreserved(); + bool ThrowIfFailWithStackPreserved(); /// /// Gets the error code as a specific enumeration type. diff --git a/ManagedCode.Communication/Result/Result.cs b/ManagedCode.Communication/Result/Result.cs index 0c4b8a8..adfdcf6 100644 --- a/ManagedCode.Communication/Result/Result.cs +++ b/ManagedCode.Communication/Result/Result.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.ExceptionServices; using System.Text.Json.Serialization; @@ -68,14 +69,17 @@ public void AddError(Error error) /// /// Throws an exception if the result indicates a failure. /// - public void ThrowIfFail() + public bool ThrowIfFail() { + if(IsSuccess) + return false; + if (Errors?.Any() is not true) { if(IsFailed) throw new Exception(nameof(IsFailed)); - return; + return false; } var exceptions = Errors.Select(s => s.Exception() ?? new Exception(StringExtension.JoinFilter(';', s.ErrorCode, s.Message))); @@ -88,14 +92,14 @@ public void ThrowIfFail() /// /// Throws an exception with stack trace preserved if the result indicates a failure. /// - public void ThrowIfFailWithStackPreserved() + public bool ThrowIfFailWithStackPreserved() { if (Errors?.Any() is not true) { if (IsFailed) throw new Exception(nameof(IsFailed)); - return; + return false; } var exceptions = Errors.Select(s => s.ExceptionInfo() ?? ExceptionDispatchInfo.Capture(new Exception(StringExtension.JoinFilter(';', s.ErrorCode, s.Message)))); diff --git a/ManagedCode.Communication/ResultT/Result.cs b/ManagedCode.Communication/ResultT/Result.cs index 3237fc1..bbf79f0 100644 --- a/ManagedCode.Communication/ResultT/Result.cs +++ b/ManagedCode.Communication/ResultT/Result.cs @@ -55,14 +55,18 @@ public void AddError(Error error) /// /// Throws an exception if the result is a failure. /// - public void ThrowIfFail() + [MemberNotNullWhen(false, nameof(Value))] + public bool ThrowIfFail() { + if(IsSuccess) + return false; + if (Errors?.Any() is not true) { if(IsFailed) throw new Exception(nameof(IsFailed)); - return; + return false; } var exceptions = Errors.Select(s => s.Exception() ?? new Exception(StringExtension.JoinFilter(';', s.ErrorCode, s.Message))); @@ -76,14 +80,15 @@ public void ThrowIfFail() /// /// Throws an exception with stack trace preserved if the result indicates a failure. /// - public void ThrowIfFailWithStackPreserved() + [MemberNotNullWhen(false, nameof(Value))] + public bool ThrowIfFailWithStackPreserved() { if (Errors?.Any() is not true) { if (IsFailed) throw new Exception(nameof(IsFailed)); - return; + return false; } var exceptions = Errors.Select(s => s.ExceptionInfo() ?? ExceptionDispatchInfo.Capture(new Exception(StringExtension.JoinFilter(';', s.ErrorCode, s.Message)))); From 7c358e0da6c29791f5437b8fbb7f70d6c7281146 Mon Sep 17 00:00:00 2001 From: VladKovtun99 Date: Mon, 4 Aug 2025 14:01:20 +0200 Subject: [PATCH 2/6] Add exception and model validation filters for improved error handling --- .../CommunicationHubFilter.cs | 39 ------- .../CommunicationMiddleware.cs | 42 ------- .../ExceptionFilterBase.cs | 109 ++++++++++++++++++ .../CommunicationAppBuilderExtensions.cs | 14 ++- .../Extensions/HubOptionsExtensions.cs | 8 +- .../Extensions/ServiceCollectionExtensions.cs | 33 +++++- .../HubExceptionFilterBase.cs | 55 +++++++++ .../ModelValidationFilterBase.cs | 47 ++++++++ .../Common/TestApp/HttpHostProgram.cs | 11 +- .../Common/TestApp/TestClusterApplication.cs | 3 +- .../Common/TestApp/TestExceptionFilter.cs | 6 + .../Common/TestApp/TestHubExceptionFilter.cs | 6 + .../TestApp/TestModelValidationFilter.cs | 6 + .../ControllerTests/MiddlewareTests.cs | 35 +++--- .../OrleansTests/GrainClientTests.cs | 1 + ManagedCode.Communication/Error.cs | 70 ----------- ManagedCode.Communication/Problem.cs | 70 +++++++++++ 17 files changed, 371 insertions(+), 184 deletions(-) delete mode 100644 ManagedCode.Communication.Extensions/CommunicationHubFilter.cs delete mode 100644 ManagedCode.Communication.Extensions/CommunicationMiddleware.cs create mode 100644 ManagedCode.Communication.Extensions/ExceptionFilterBase.cs create mode 100644 ManagedCode.Communication.Extensions/HubExceptionFilterBase.cs create mode 100644 ManagedCode.Communication.Extensions/ModelValidationFilterBase.cs create mode 100644 ManagedCode.Communication.Tests/Common/TestApp/TestExceptionFilter.cs create mode 100644 ManagedCode.Communication.Tests/Common/TestApp/TestHubExceptionFilter.cs create mode 100644 ManagedCode.Communication.Tests/Common/TestApp/TestModelValidationFilter.cs create mode 100644 ManagedCode.Communication/Problem.cs diff --git a/ManagedCode.Communication.Extensions/CommunicationHubFilter.cs b/ManagedCode.Communication.Extensions/CommunicationHubFilter.cs deleted file mode 100644 index 2a4b87e..0000000 --- a/ManagedCode.Communication.Extensions/CommunicationHubFilter.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Net; -using System.Threading.Tasks; -using ManagedCode.Communication.Extensions.Extensions; -using Microsoft.AspNetCore.SignalR; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace ManagedCode.Communication.Extensions; - -public class CommunicationHubFilter : IHubFilter -{ - private readonly ILogger _logger; - private readonly IOptions _options; - - public CommunicationHubFilter(ILogger logger, IOptions options) - { - _logger = logger; - _options = options; - } - - public async ValueTask InvokeMethodAsync(HubInvocationContext invocationContext, - Func> next) - { - try - { - return await next(invocationContext); - } - catch (Exception ex) - { - _logger.LogError(ex, invocationContext.Hub.GetType().Name + "." + invocationContext.HubMethodName); - - if (_options.Value.ShowErrorDetails) - return Result.Fail(HttpStatusCode.InternalServerError, ex.Message); - - return Result.Fail(HttpStatusCode.InternalServerError, nameof(HttpStatusCode.InternalServerError)); - } - } -} \ No newline at end of file diff --git a/ManagedCode.Communication.Extensions/CommunicationMiddleware.cs b/ManagedCode.Communication.Extensions/CommunicationMiddleware.cs deleted file mode 100644 index 1ad148c..0000000 --- a/ManagedCode.Communication.Extensions/CommunicationMiddleware.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Net; -using System.Threading.Tasks; -using ManagedCode.Communication.Extensions.Extensions; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace ManagedCode.Communication.Extensions; - -public class CommunicationMiddleware(ILogger logger, RequestDelegate next, IOptions options) -{ - public async Task Invoke(HttpContext httpContext) - { - try - { - await next(httpContext); - } - catch (Exception ex) - { - logger.LogError(ex, httpContext.Request.Method + "::" + httpContext.Request.Path); - - if (httpContext.Response.HasStarted) - throw; - - httpContext.Response.Headers.CacheControl = "no-cache,no-store"; - httpContext.Response.Headers.Pragma = "no-cache"; - httpContext.Response.Headers.Expires = "-1"; - httpContext.Response.Headers.ETag = default; - - httpContext.Response.ContentType = "application/json; charset=utf-8"; - httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; - - if (options.Value.ShowErrorDetails) - await httpContext.Response.WriteAsJsonAsync(Result.Fail(HttpStatusCode.InternalServerError, - ex.Message)); - else - await httpContext.Response.WriteAsJsonAsync(Result.Fail(HttpStatusCode.InternalServerError, - nameof(HttpStatusCode.InternalServerError))); - } - } -} \ No newline at end of file diff --git a/ManagedCode.Communication.Extensions/ExceptionFilterBase.cs b/ManagedCode.Communication.Extensions/ExceptionFilterBase.cs new file mode 100644 index 0000000..0c54b99 --- /dev/null +++ b/ManagedCode.Communication.Extensions/ExceptionFilterBase.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Security; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Logging; + +namespace ManagedCode.Communication.Extensions; + +public abstract class ExceptionFilterBase(ILogger logger) : IExceptionFilter +{ + protected readonly ILogger Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + public virtual void OnException(ExceptionContext context) + { + try + { + var exception = context.Exception; + var actionName = context.ActionDescriptor.DisplayName; + var controllerName = context.ActionDescriptor.RouteValues["controller"] ?? "Unknown"; + + Logger.LogError(exception, "Unhandled exception in {ControllerName}.{ActionName}", + controllerName, actionName); + + var statusCode = GetStatusCodeForException(exception); + var problem = new Problem() + { + Title = exception.GetType().Name, + Detail = exception.Message, + Status = (int)statusCode, + Instance = context.HttpContext.Request.Path, + Extensions = + { + ["traceId"] = context.HttpContext.TraceIdentifier + } + }; + + var result = Result.Fail(exception.Message, problem); + + context.Result = new ObjectResult(result) + { + StatusCode = (int)statusCode + }; + + context.ExceptionHandled = true; + + Logger.LogInformation("Exception handled by {FilterType} for {ControllerName}.{ActionName}", + GetType().Name, controllerName, actionName); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error occurred while handling exception in {FilterType}", GetType().Name); + + var fallbackProblem = new Problem + { + Title = "An unexpected error occurred", + Status = (int)HttpStatusCode.InternalServerError, + Instance = context.HttpContext.Request.Path + }; + + context.Result = new ObjectResult(Result.Fail("An unexpected error occurred", fallbackProblem)) + { + StatusCode = (int)HttpStatusCode.InternalServerError + }; + context.ExceptionHandled = true; + } + } + + protected virtual HttpStatusCode GetStatusCodeForException(Exception exception) + { + return exception switch + { + ArgumentException => HttpStatusCode.BadRequest, + InvalidOperationException => HttpStatusCode.BadRequest, + NotSupportedException => HttpStatusCode.BadRequest, + FormatException => HttpStatusCode.BadRequest, + JsonException => HttpStatusCode.BadRequest, + XmlException => HttpStatusCode.BadRequest, + + UnauthorizedAccessException => HttpStatusCode.Unauthorized, + + SecurityException => HttpStatusCode.Forbidden, + + FileNotFoundException => HttpStatusCode.NotFound, + DirectoryNotFoundException => HttpStatusCode.NotFound, + KeyNotFoundException => HttpStatusCode.NotFound, + + TimeoutException => HttpStatusCode.RequestTimeout, + TaskCanceledException => HttpStatusCode.RequestTimeout, + OperationCanceledException => HttpStatusCode.RequestTimeout, + + InvalidDataException => HttpStatusCode.Conflict, + + NotImplementedException => HttpStatusCode.NotImplemented, + NotFiniteNumberException => HttpStatusCode.InternalServerError, + OutOfMemoryException => HttpStatusCode.InternalServerError, + StackOverflowException => HttpStatusCode.InternalServerError, + ThreadAbortException => HttpStatusCode.InternalServerError, + + _ => HttpStatusCode.InternalServerError + }; + } +} \ No newline at end of file diff --git a/ManagedCode.Communication.Extensions/Extensions/CommunicationAppBuilderExtensions.cs b/ManagedCode.Communication.Extensions/Extensions/CommunicationAppBuilderExtensions.cs index 93a788a..7e610e4 100644 --- a/ManagedCode.Communication.Extensions/Extensions/CommunicationAppBuilderExtensions.cs +++ b/ManagedCode.Communication.Extensions/Extensions/CommunicationAppBuilderExtensions.cs @@ -1,5 +1,9 @@ using System; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using ManagedCode.Communication.Extensions; namespace ManagedCode.Communication.Extensions.Extensions; @@ -10,6 +14,14 @@ public static IApplicationBuilder UseCommunication(this IApplicationBuilder app) if (app == null) throw new ArgumentNullException(nameof(app)); - return app.UseMiddleware(); + var serviceProvider = app.ApplicationServices; + var exceptionFilter = serviceProvider.GetRequiredService(); + var modelValidationFilter = serviceProvider.GetRequiredService(); + + var mvcOptions = serviceProvider.GetRequiredService>(); + mvcOptions.Value.Filters.Add(exceptionFilter); + mvcOptions.Value.Filters.Add(modelValidationFilter); + + return app; } } \ No newline at end of file diff --git a/ManagedCode.Communication.Extensions/Extensions/HubOptionsExtensions.cs b/ManagedCode.Communication.Extensions/Extensions/HubOptionsExtensions.cs index 9014a76..6e7467c 100644 --- a/ManagedCode.Communication.Extensions/Extensions/HubOptionsExtensions.cs +++ b/ManagedCode.Communication.Extensions/Extensions/HubOptionsExtensions.cs @@ -1,11 +1,15 @@ +using System; using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.DependencyInjection; +using ManagedCode.Communication.Extensions; namespace ManagedCode.Communication.Extensions.Extensions; public static class HubOptionsExtensions { - public static void AddCommunicationHubFilter(this HubOptions result) + public static void AddCommunicationHubFilter(this HubOptions result, IServiceProvider serviceProvider) { - result.AddFilter(); + var hubFilter = serviceProvider.GetRequiredService(); + result.AddFilter(hubFilter); } } \ No newline at end of file diff --git a/ManagedCode.Communication.Extensions/Extensions/ServiceCollectionExtensions.cs b/ManagedCode.Communication.Extensions/Extensions/ServiceCollectionExtensions.cs index 3543b03..1f97b1c 100644 --- a/ManagedCode.Communication.Extensions/Extensions/ServiceCollectionExtensions.cs +++ b/ManagedCode.Communication.Extensions/Extensions/ServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -27,11 +28,11 @@ public static IHostApplicationBuilder AddCommunication(this IHostApplicationBuil public static class ServiceCollectionExtensions { - public static IServiceCollection AddCommunication(this IServiceCollection services, Action options) + public static IServiceCollection AddCommunication(this IServiceCollection services, Action? configure = null) { - services.AddOptions() - .Configure(options); - + if (configure != null) + services.Configure(configure); + return services; } @@ -63,4 +64,28 @@ public static IServiceCollection AddCommunicationExceptionHandler(this IServiceC services.AddExceptionHandler(); return services; } + + public static IServiceCollection AddCommunicationFilters( + this IServiceCollection services) + where TExceptionFilter : ExceptionFilterBase + where TModelValidationFilter : ModelValidationFilterBase + where THubExceptionFilter : HubExceptionFilterBase + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddControllers(options => + { + options.Filters.Add(); + options.Filters.Add(); + }); + + services.Configure(options => + { + options.AddFilter(); + }); + + return services; + } } \ No newline at end of file diff --git a/ManagedCode.Communication.Extensions/HubExceptionFilterBase.cs b/ManagedCode.Communication.Extensions/HubExceptionFilterBase.cs new file mode 100644 index 0000000..bbcb8ef --- /dev/null +++ b/ManagedCode.Communication.Extensions/HubExceptionFilterBase.cs @@ -0,0 +1,55 @@ +using System; +using System.Threading.Tasks; +using System.Threading; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging; +using ManagedCode.Communication; + +namespace ManagedCode.Communication.Extensions; + +public abstract class HubExceptionFilterBase(ILogger logger) : IHubFilter +{ + protected readonly ILogger Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + public async ValueTask InvokeMethodAsync(HubInvocationContext invocationContext, + Func> next) + { + try + { + return await next(invocationContext); + } + catch (Exception ex) + { + Logger.LogError(ex, invocationContext.Hub.GetType().Name + "." + invocationContext.HubMethodName); + + var problem = new Problem + { + Title = ex.GetType().Name, + Detail = ex.Message, + Status = GetStatusCodeForException(ex), + Instance = invocationContext.Hub.Context.ConnectionId, + Extensions = + { + ["hubMethod"] = invocationContext.HubMethodName, + ["hubType"] = invocationContext.Hub.GetType().Name + } + }; + + return Result.Fail(ex.Message, problem); + } + } + + protected virtual int GetStatusCodeForException(Exception exception) + { + return exception switch + { + ArgumentException or ArgumentNullException => 400, + UnauthorizedAccessException => 401, + InvalidOperationException => 400, + NotSupportedException => 400, + TimeoutException => 408, + TaskCanceledException => 408, + _ => 500 + }; + } +} \ No newline at end of file diff --git a/ManagedCode.Communication.Extensions/ModelValidationFilterBase.cs b/ManagedCode.Communication.Extensions/ModelValidationFilterBase.cs new file mode 100644 index 0000000..619c2d6 --- /dev/null +++ b/ManagedCode.Communication.Extensions/ModelValidationFilterBase.cs @@ -0,0 +1,47 @@ +using System; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Logging; +using ManagedCode.Communication; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace ManagedCode.Communication.Extensions; + +public abstract class ModelValidationFilterBase(ILogger logger) : IActionFilter +{ + + public void OnActionExecuting(ActionExecutingContext context) + { + if (!context.ModelState.IsValid) + { + logger.LogWarning("Model validation failed for {ActionName}", + context.ActionDescriptor.DisplayName); + + var problem = new Problem + { + Title = "Validation failed", + Status = 400, + Instance = context.HttpContext.Request.Path, + Extensions = + { + ["validationErrors"] = context.ModelState + .Where(x => x.Value?.Errors.Count > 0) + .ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value?.Errors.Select(e => e.ErrorMessage).ToArray() ?? [] + ) + } + }; + + var result = Result.Fail(problem); + + context.Result = new BadRequestObjectResult(result); + } + } + + public void OnActionExecuted(ActionExecutedContext context) + { + // Not needed for this filter + } +} \ No newline at end of file diff --git a/ManagedCode.Communication.Tests/Common/TestApp/HttpHostProgram.cs b/ManagedCode.Communication.Tests/Common/TestApp/HttpHostProgram.cs index 40cfa52..f3659be 100644 --- a/ManagedCode.Communication.Tests/Common/TestApp/HttpHostProgram.cs +++ b/ManagedCode.Communication.Tests/Common/TestApp/HttpHostProgram.cs @@ -1,11 +1,10 @@ -using ManagedCode.Communication.Extensions; using ManagedCode.Communication.Extensions.Extensions; +using ManagedCode.Communication.Tests.TestApp; using ManagedCode.Communication.Tests.TestApp.Controllers; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; -namespace ManagedCode.Communication.Tests.TestApp; +namespace ManagedCode.Communication.Tests.Common.TestApp; public class HttpHostProgram { @@ -18,8 +17,12 @@ public static void Main(string[] args) option.ShowErrorDetails = true; }); + builder.Services.AddCommunicationFilters(); + builder.Services.AddControllers(); - builder.Services.AddSignalR(options => options.AddCommunicationHubFilter()); + builder.Services.AddSignalR(options => + { + }); var app = builder.Build(); diff --git a/ManagedCode.Communication.Tests/Common/TestApp/TestClusterApplication.cs b/ManagedCode.Communication.Tests/Common/TestApp/TestClusterApplication.cs index 02e2bbe..cf82359 100644 --- a/ManagedCode.Communication.Tests/Common/TestApp/TestClusterApplication.cs +++ b/ManagedCode.Communication.Tests/Common/TestApp/TestClusterApplication.cs @@ -5,13 +5,12 @@ using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Orleans.Hosting; using Orleans.TestingHost; using Xunit; -namespace ManagedCode.Communication.Tests.TestApp; +namespace ManagedCode.Communication.Tests.Common.TestApp; [CollectionDefinition(nameof(TestClusterApplication))] public class TestClusterApplication : WebApplicationFactory, ICollectionFixture diff --git a/ManagedCode.Communication.Tests/Common/TestApp/TestExceptionFilter.cs b/ManagedCode.Communication.Tests/Common/TestApp/TestExceptionFilter.cs new file mode 100644 index 0000000..2fd2a7a --- /dev/null +++ b/ManagedCode.Communication.Tests/Common/TestApp/TestExceptionFilter.cs @@ -0,0 +1,6 @@ +using ManagedCode.Communication.Extensions; +using Microsoft.Extensions.Logging; + +namespace ManagedCode.Communication.Tests.Common.TestApp; + +public class TestExceptionFilter(ILogger logger) : ExceptionFilterBase(logger); \ No newline at end of file diff --git a/ManagedCode.Communication.Tests/Common/TestApp/TestHubExceptionFilter.cs b/ManagedCode.Communication.Tests/Common/TestApp/TestHubExceptionFilter.cs new file mode 100644 index 0000000..bdd2985 --- /dev/null +++ b/ManagedCode.Communication.Tests/Common/TestApp/TestHubExceptionFilter.cs @@ -0,0 +1,6 @@ +using ManagedCode.Communication.Extensions; +using Microsoft.Extensions.Logging; + +namespace ManagedCode.Communication.Tests.Common.TestApp; + +public class TestHubExceptionFilter(ILogger logger) : HubExceptionFilterBase(logger); \ No newline at end of file diff --git a/ManagedCode.Communication.Tests/Common/TestApp/TestModelValidationFilter.cs b/ManagedCode.Communication.Tests/Common/TestApp/TestModelValidationFilter.cs new file mode 100644 index 0000000..9a9c624 --- /dev/null +++ b/ManagedCode.Communication.Tests/Common/TestApp/TestModelValidationFilter.cs @@ -0,0 +1,6 @@ +using ManagedCode.Communication.Extensions; +using Microsoft.Extensions.Logging; + +namespace ManagedCode.Communication.Tests.Common.TestApp; + +public class TestModelValidationFilter(ILogger logger) : ModelValidationFilterBase(logger); \ No newline at end of file diff --git a/ManagedCode.Communication.Tests/ControllerTests/MiddlewareTests.cs b/ManagedCode.Communication.Tests/ControllerTests/MiddlewareTests.cs index 2b1023c..e6c777b 100644 --- a/ManagedCode.Communication.Tests/ControllerTests/MiddlewareTests.cs +++ b/ManagedCode.Communication.Tests/ControllerTests/MiddlewareTests.cs @@ -2,6 +2,7 @@ using System.Net.Http.Json; using System.Threading.Tasks; using FluentAssertions; +using ManagedCode.Communication.Tests.Common.TestApp; using ManagedCode.Communication.Tests.TestApp; using ManagedCode.Communication.Tests.TestApp.Controllers; using ManagedCode.Communication.Tests.TestApp.Grains; @@ -12,43 +13,36 @@ namespace ManagedCode.Communication.Tests.ControllerTests; [Collection(nameof(TestClusterApplication))] -public class MiddlewareTests +public class MiddlewareTests(ITestOutputHelper outputHelper, TestClusterApplication application) { - private readonly ITestOutputHelper _outputHelper; - private readonly TestClusterApplication _application; - - public MiddlewareTests(ITestOutputHelper outputHelper, TestClusterApplication application) - { - _outputHelper = outputHelper; - _application = application; - } + private readonly ITestOutputHelper _outputHelper = outputHelper; [Fact] public async Task ValidationException() { - var response = await _application.CreateClient().GetAsync($"test/test1"); + var response = await application.CreateClient().GetAsync($"test/test1"); response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); var content = await response.Content.ReadAsStringAsync(); - var result = await response.Content.ReadFromJsonAsync(); + var result = await response.Content.ReadFromJsonAsync>(); result.IsFailed.Should().BeTrue(); - result.GetError().Value.Message.Should().Be("ValidationException"); + result.GetError()?.Message.Should().Be("ValidationException"); } [Fact] public async Task InvalidDataException() { - var response = await _application.CreateClient().GetAsync($"test/test2"); - response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + var response = await application.CreateClient().GetAsync($"test/test2"); + response.StatusCode.Should().Be(HttpStatusCode.Conflict); var content = await response.Content.ReadAsStringAsync(); - var result = await response.Content.ReadFromJsonAsync>(); + var result = await response.Content.ReadFromJsonAsync>(); result.IsFailed.Should().BeTrue(); - result.GetError().Value.Message.Should().Be("InvalidDataException"); + result.GetError()?.Message.Should().Be("InvalidDataException"); } [Fact] public async Task ValidationExceptionSginalR() { - var connection = _application.CreateSignalRClient(nameof(TestHub)); + var connection = application.CreateSignalRClient(nameof(TestHub)); await connection.StartAsync(); connection.State.Should().Be(HubConnectionState.Connected); var result = await connection.InvokeAsync>("DoTest"); @@ -59,11 +53,12 @@ public async Task ValidationExceptionSginalR() [Fact] public async Task InvalidDataExceptionSignalR() { - var connection = _application.CreateSignalRClient(nameof(TestHub)); + var connection = application.CreateSignalRClient(nameof(TestHub)); await connection.StartAsync(); connection.State.Should().Be(HubConnectionState.Connected); - var result = await connection.InvokeAsync>("Throw"); + var result = await connection.InvokeAsync>("Throw"); result.IsFailed.Should().BeTrue(); - result.GetError().Value.Message.Should().Be("InvalidDataException"); + result.GetError().Should().NotBeNull(); + result.GetError()!.Value.Message.Should().Be("InvalidDataException"); } } \ No newline at end of file diff --git a/ManagedCode.Communication.Tests/OrleansTests/GrainClientTests.cs b/ManagedCode.Communication.Tests/OrleansTests/GrainClientTests.cs index ee28075..0105e53 100644 --- a/ManagedCode.Communication.Tests/OrleansTests/GrainClientTests.cs +++ b/ManagedCode.Communication.Tests/OrleansTests/GrainClientTests.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using FluentAssertions; +using ManagedCode.Communication.Tests.Common.TestApp; using ManagedCode.Communication.Tests.TestApp; using ManagedCode.Communication.Tests.TestApp.Grains; using Xunit; diff --git a/ManagedCode.Communication/Error.cs b/ManagedCode.Communication/Error.cs index 9df282c..4eb10f2 100644 --- a/ManagedCode.Communication/Error.cs +++ b/ManagedCode.Communication/Error.cs @@ -1,78 +1,8 @@ using System; -using System.Collections.Generic; using System.Runtime.ExceptionServices; -using System.Text.Json.Serialization; namespace ManagedCode.Communication; - -/// -/// A machine-readable format for specifying errors in HTTP API responses based on . -/// -public class Problem -{ - /// - /// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when - /// dereferenced, it provide human-readable documentation for the problem type - /// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be - /// "about:blank". - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyOrder(-5)] - [JsonPropertyName("type")] - public string? Type { get; set; } - - /// - /// A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to occurrence - /// of the problem, except for purposes of localization(e.g., using proactive content negotiation; - /// see[RFC7231], Section 3.4). - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyOrder(-4)] - [JsonPropertyName("title")] - public string? Title { get; set; } - - /// - /// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyOrder(-3)] - [JsonPropertyName("status")] - public int? Status { get; set; } - - /// - /// A human-readable explanation specific to this occurrence of the problem. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyOrder(-2)] - [JsonPropertyName("detail")] - public string? Detail { get; set; } - - /// - /// A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyOrder(-1)] - [JsonPropertyName("instance")] - public string? Instance { get; set; } - - /// - /// Gets the for extension members. - /// - /// Problem type definitions MAY extend the problem details object with additional members. Extension members appear in the same namespace as - /// other members of a problem type. - /// - /// - /// - /// The round-tripping behavior for is determined by the implementation of the Input \ Output formatters. - /// In particular, complex types or collection types may not round-trip to the original type when using the built-in JSON or XML formatters. - /// - [JsonExtensionData] - public IDictionary Extensions { get; set; } = new Dictionary(StringComparer.Ordinal); -} - - - public struct Error { internal Error(string message, string? errorCode = default) diff --git a/ManagedCode.Communication/Problem.cs b/ManagedCode.Communication/Problem.cs new file mode 100644 index 0000000..49b4493 --- /dev/null +++ b/ManagedCode.Communication/Problem.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ManagedCode.Communication; + +/// +/// A machine-readable format for specifying errors in HTTP API responses based on . +/// +public class Problem +{ + /// + /// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when + /// dereferenced, it provide human-readable documentation for the problem type + /// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be + /// "about:blank". + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyOrder(-5)] + [JsonPropertyName("type")] + public string? Type { get; set; } + + /// + /// A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to occurrence + /// of the problem, except for purposes of localization(e.g., using proactive content negotiation; + /// see[RFC7231], Section 3.4). + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyOrder(-4)] + [JsonPropertyName("title")] + public string? Title { get; set; } + + /// + /// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyOrder(-3)] + [JsonPropertyName("status")] + public int? Status { get; set; } + + /// + /// A human-readable explanation specific to this occurrence of the problem. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyOrder(-2)] + [JsonPropertyName("detail")] + public string? Detail { get; set; } + + /// + /// A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyOrder(-1)] + [JsonPropertyName("instance")] + public string? Instance { get; set; } + + /// + /// Gets the for extension members. + /// + /// Problem type definitions MAY extend the problem details object with additional members. Extension members appear in the same namespace as + /// other members of a problem type. + /// + /// + /// + /// The round-tripping behavior for is determined by the implementation of the Input \ Output formatters. + /// In particular, complex types or collection types may not round-trip to the original type when using the built-in JSON or XML formatters. + /// + [JsonExtensionData] + public IDictionary Extensions { get; set; } = new Dictionary(StringComparer.Ordinal); +} \ No newline at end of file From 59ce1dcc810643fc04f96dee38e554c8a859e65d Mon Sep 17 00:00:00 2001 From: VladKovtun99 Date: Mon, 4 Aug 2025 14:17:01 +0200 Subject: [PATCH 3/6] Remove manual filter registration for backward compatibility; use AddCommunicationFilters() instead --- .../Extensions/CommunicationAppBuilderExtensions.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/ManagedCode.Communication.Extensions/Extensions/CommunicationAppBuilderExtensions.cs b/ManagedCode.Communication.Extensions/Extensions/CommunicationAppBuilderExtensions.cs index 7e610e4..60558fd 100644 --- a/ManagedCode.Communication.Extensions/Extensions/CommunicationAppBuilderExtensions.cs +++ b/ManagedCode.Communication.Extensions/Extensions/CommunicationAppBuilderExtensions.cs @@ -14,13 +14,9 @@ public static IApplicationBuilder UseCommunication(this IApplicationBuilder app) if (app == null) throw new ArgumentNullException(nameof(app)); - var serviceProvider = app.ApplicationServices; - var exceptionFilter = serviceProvider.GetRequiredService(); - var modelValidationFilter = serviceProvider.GetRequiredService(); - - var mvcOptions = serviceProvider.GetRequiredService>(); - mvcOptions.Value.Filters.Add(exceptionFilter); - mvcOptions.Value.Filters.Add(modelValidationFilter); + // Note: Filters are now registered automatically via AddCommunicationFilters() in ConfigureServices + // This method is kept for backward compatibility but no longer performs filter registration + // Use AddCommunicationFilters() instead return app; } From 7080b936a6ebe8ea0322594118c6d73837bc14d5 Mon Sep 17 00:00:00 2001 From: VladKovtun99 Date: Mon, 4 Aug 2025 14:53:34 +0200 Subject: [PATCH 4/6] Refactor error handling and status code management; introduce ProblemConstants for better maintainability --- .../Constants/ProblemConstants.cs | 59 +++++++++++++++++++ .../ExceptionFilterBase.cs | 55 +++-------------- .../CommunicationAppBuilderExtensions.cs | 4 -- .../Extensions/ServiceCollectionExtensions.cs | 3 +- .../Helpers/HttpStatusCodeHelper.cs | 49 +++++++++++++++ .../HubExceptionFilterBase.cs | 22 ++----- .../ModelValidationFilterBase.cs | 10 ++-- ManagedCode.Communication.Extensions/sdf.cs | 7 ++- .../CollectionResultT/CollectionResult.cs | 2 +- ManagedCode.Communication/Problem.cs | 5 +- ManagedCode.Communication/Result/Result.cs | 2 +- ManagedCode.Communication/ResultT/Result.cs | 2 +- 12 files changed, 137 insertions(+), 83 deletions(-) create mode 100644 ManagedCode.Communication.Extensions/Constants/ProblemConstants.cs create mode 100644 ManagedCode.Communication.Extensions/Helpers/HttpStatusCodeHelper.cs diff --git a/ManagedCode.Communication.Extensions/Constants/ProblemConstants.cs b/ManagedCode.Communication.Extensions/Constants/ProblemConstants.cs new file mode 100644 index 0000000..6cc9000 --- /dev/null +++ b/ManagedCode.Communication.Extensions/Constants/ProblemConstants.cs @@ -0,0 +1,59 @@ +namespace ManagedCode.Communication.Extensions.Constants; + +/// +/// Constants for Problem details to avoid string literals throughout the codebase. +/// +public static class ProblemConstants +{ + /// + /// Problem titles + /// + public static class Titles + { + /// + /// Title for validation failure problems + /// + public const string ValidationFailed = "Validation failed"; + + /// + /// Title for unexpected error problems + /// + public const string UnexpectedError = "An unexpected error occurred"; + } + + /// + /// Problem extension keys + /// + public static class ExtensionKeys + { + /// + /// Key for validation errors in problem extensions + /// + public const string ValidationErrors = "validationErrors"; + + /// + /// Key for trace ID in problem extensions + /// + public const string TraceId = "traceId"; + + /// + /// Key for hub method name in problem extensions + /// + public const string HubMethod = "hubMethod"; + + /// + /// Key for hub type name in problem extensions + /// + public const string HubType = "hubType"; + + /// + /// Key for inner exception in problem extensions + /// + public const string InnerException = "innerException"; + + /// + /// Key for stack trace in problem extensions + /// + public const string StackTrace = "stackTrace"; + } +} \ No newline at end of file diff --git a/ManagedCode.Communication.Extensions/ExceptionFilterBase.cs b/ManagedCode.Communication.Extensions/ExceptionFilterBase.cs index 0c54b99..e563d1e 100644 --- a/ManagedCode.Communication.Extensions/ExceptionFilterBase.cs +++ b/ManagedCode.Communication.Extensions/ExceptionFilterBase.cs @@ -1,15 +1,10 @@ using System; -using System.Collections.Generic; -using System.IO; using System.Net; -using System.Security; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using System.Xml; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Logging; +using static ManagedCode.Communication.Extensions.Helpers.HttpStatusCodeHelper; +using static ManagedCode.Communication.Extensions.Constants.ProblemConstants; namespace ManagedCode.Communication.Extensions; @@ -29,15 +24,16 @@ public virtual void OnException(ExceptionContext context) controllerName, actionName); var statusCode = GetStatusCodeForException(exception); + var problem = new Problem() { Title = exception.GetType().Name, Detail = exception.Message, - Status = (int)statusCode, + Status = statusCode, Instance = context.HttpContext.Request.Path, Extensions = { - ["traceId"] = context.HttpContext.TraceIdentifier + [ExtensionKeys.TraceId] = context.HttpContext.TraceIdentifier } }; @@ -59,51 +55,16 @@ public virtual void OnException(ExceptionContext context) var fallbackProblem = new Problem { - Title = "An unexpected error occurred", - Status = (int)HttpStatusCode.InternalServerError, + Title = Titles.UnexpectedError, + Status = HttpStatusCode.InternalServerError, Instance = context.HttpContext.Request.Path }; - context.Result = new ObjectResult(Result.Fail("An unexpected error occurred", fallbackProblem)) + context.Result = new ObjectResult(Result.Fail(Titles.UnexpectedError, fallbackProblem)) { StatusCode = (int)HttpStatusCode.InternalServerError }; context.ExceptionHandled = true; } } - - protected virtual HttpStatusCode GetStatusCodeForException(Exception exception) - { - return exception switch - { - ArgumentException => HttpStatusCode.BadRequest, - InvalidOperationException => HttpStatusCode.BadRequest, - NotSupportedException => HttpStatusCode.BadRequest, - FormatException => HttpStatusCode.BadRequest, - JsonException => HttpStatusCode.BadRequest, - XmlException => HttpStatusCode.BadRequest, - - UnauthorizedAccessException => HttpStatusCode.Unauthorized, - - SecurityException => HttpStatusCode.Forbidden, - - FileNotFoundException => HttpStatusCode.NotFound, - DirectoryNotFoundException => HttpStatusCode.NotFound, - KeyNotFoundException => HttpStatusCode.NotFound, - - TimeoutException => HttpStatusCode.RequestTimeout, - TaskCanceledException => HttpStatusCode.RequestTimeout, - OperationCanceledException => HttpStatusCode.RequestTimeout, - - InvalidDataException => HttpStatusCode.Conflict, - - NotImplementedException => HttpStatusCode.NotImplemented, - NotFiniteNumberException => HttpStatusCode.InternalServerError, - OutOfMemoryException => HttpStatusCode.InternalServerError, - StackOverflowException => HttpStatusCode.InternalServerError, - ThreadAbortException => HttpStatusCode.InternalServerError, - - _ => HttpStatusCode.InternalServerError - }; - } } \ No newline at end of file diff --git a/ManagedCode.Communication.Extensions/Extensions/CommunicationAppBuilderExtensions.cs b/ManagedCode.Communication.Extensions/Extensions/CommunicationAppBuilderExtensions.cs index 60558fd..ce6fad0 100644 --- a/ManagedCode.Communication.Extensions/Extensions/CommunicationAppBuilderExtensions.cs +++ b/ManagedCode.Communication.Extensions/Extensions/CommunicationAppBuilderExtensions.cs @@ -14,10 +14,6 @@ public static IApplicationBuilder UseCommunication(this IApplicationBuilder app) if (app == null) throw new ArgumentNullException(nameof(app)); - // Note: Filters are now registered automatically via AddCommunicationFilters() in ConfigureServices - // This method is kept for backward compatibility but no longer performs filter registration - // Use AddCommunicationFilters() instead - return app; } } \ No newline at end of file diff --git a/ManagedCode.Communication.Extensions/Extensions/ServiceCollectionExtensions.cs b/ManagedCode.Communication.Extensions/Extensions/ServiceCollectionExtensions.cs index 1f97b1c..5b16278 100644 --- a/ManagedCode.Communication.Extensions/Extensions/ServiceCollectionExtensions.cs +++ b/ManagedCode.Communication.Extensions/Extensions/ServiceCollectionExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using ManagedCode.Communication.Extensions.Constants; namespace ManagedCode.Communication.Extensions.Extensions; @@ -49,7 +50,7 @@ public static IServiceCollection AddDefaultProblemDetails(this IServiceCollectio context.ProblemDetails.Type ??= $"https://httpstatuses.io/{statusCode}"; context.ProblemDetails.Title ??= ReasonPhrases.GetReasonPhrase(statusCode); context.ProblemDetails.Instance ??= context.HttpContext.Request.Path; - context.ProblemDetails.Extensions.TryAdd("traceId", Activity.Current?.Id ?? context.HttpContext.TraceIdentifier); + context.ProblemDetails.Extensions.TryAdd(ProblemConstants.ExtensionKeys.TraceId, Activity.Current?.Id ?? context.HttpContext.TraceIdentifier); }; }); diff --git a/ManagedCode.Communication.Extensions/Helpers/HttpStatusCodeHelper.cs b/ManagedCode.Communication.Extensions/Helpers/HttpStatusCodeHelper.cs new file mode 100644 index 0000000..4fe6e6e --- /dev/null +++ b/ManagedCode.Communication.Extensions/Helpers/HttpStatusCodeHelper.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Security; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; + +namespace ManagedCode.Communication.Extensions.Helpers; + +public static class HttpStatusCodeHelper +{ + public static HttpStatusCode GetStatusCodeForException(Exception exception) + { + return exception switch + { + ArgumentException => HttpStatusCode.BadRequest, + InvalidOperationException => HttpStatusCode.BadRequest, + NotSupportedException => HttpStatusCode.BadRequest, + FormatException => HttpStatusCode.BadRequest, + JsonException => HttpStatusCode.BadRequest, + XmlException => HttpStatusCode.BadRequest, + + UnauthorizedAccessException => HttpStatusCode.Unauthorized, + + SecurityException => HttpStatusCode.Forbidden, + + FileNotFoundException => HttpStatusCode.NotFound, + DirectoryNotFoundException => HttpStatusCode.NotFound, + KeyNotFoundException => HttpStatusCode.NotFound, + + TimeoutException => HttpStatusCode.RequestTimeout, + TaskCanceledException => HttpStatusCode.RequestTimeout, + OperationCanceledException => HttpStatusCode.RequestTimeout, + + InvalidDataException => HttpStatusCode.Conflict, + + NotImplementedException => HttpStatusCode.NotImplemented, + NotFiniteNumberException => HttpStatusCode.InternalServerError, + OutOfMemoryException => HttpStatusCode.InternalServerError, + StackOverflowException => HttpStatusCode.InternalServerError, + ThreadAbortException => HttpStatusCode.InternalServerError, + + _ => HttpStatusCode.InternalServerError + }; + } +} \ No newline at end of file diff --git a/ManagedCode.Communication.Extensions/HubExceptionFilterBase.cs b/ManagedCode.Communication.Extensions/HubExceptionFilterBase.cs index bbcb8ef..b6c01d9 100644 --- a/ManagedCode.Communication.Extensions/HubExceptionFilterBase.cs +++ b/ManagedCode.Communication.Extensions/HubExceptionFilterBase.cs @@ -1,9 +1,9 @@ using System; using System.Threading.Tasks; -using System.Threading; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; -using ManagedCode.Communication; +using static ManagedCode.Communication.Extensions.Helpers.HttpStatusCodeHelper; +using static ManagedCode.Communication.Extensions.Constants.ProblemConstants; namespace ManagedCode.Communication.Extensions; @@ -30,26 +30,12 @@ public abstract class HubExceptionFilterBase(ILogger logger) : IHubFilter Instance = invocationContext.Hub.Context.ConnectionId, Extensions = { - ["hubMethod"] = invocationContext.HubMethodName, - ["hubType"] = invocationContext.Hub.GetType().Name + [ExtensionKeys.HubMethod] = invocationContext.HubMethodName, + [ExtensionKeys.HubType] = invocationContext.Hub.GetType().Name } }; return Result.Fail(ex.Message, problem); } } - - protected virtual int GetStatusCodeForException(Exception exception) - { - return exception switch - { - ArgumentException or ArgumentNullException => 400, - UnauthorizedAccessException => 401, - InvalidOperationException => 400, - NotSupportedException => 400, - TimeoutException => 408, - TaskCanceledException => 408, - _ => 500 - }; - } } \ No newline at end of file diff --git a/ManagedCode.Communication.Extensions/ModelValidationFilterBase.cs b/ManagedCode.Communication.Extensions/ModelValidationFilterBase.cs index 619c2d6..96da6e7 100644 --- a/ManagedCode.Communication.Extensions/ModelValidationFilterBase.cs +++ b/ManagedCode.Communication.Extensions/ModelValidationFilterBase.cs @@ -1,10 +1,10 @@ using System; using System.Linq; +using System.Net; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Logging; -using ManagedCode.Communication; -using Microsoft.AspNetCore.Mvc.ModelBinding; +using static ManagedCode.Communication.Extensions.Constants.ProblemConstants; namespace ManagedCode.Communication.Extensions; @@ -20,12 +20,12 @@ public void OnActionExecuting(ActionExecutingContext context) var problem = new Problem { - Title = "Validation failed", - Status = 400, + Title = Titles.ValidationFailed, + Status = HttpStatusCode.BadRequest, Instance = context.HttpContext.Request.Path, Extensions = { - ["validationErrors"] = context.ModelState + [ExtensionKeys.ValidationErrors] = context.ModelState .Where(x => x.Value?.Errors.Count > 0) .ToDictionary( kvp => kvp.Key, diff --git a/ManagedCode.Communication.Extensions/sdf.cs b/ManagedCode.Communication.Extensions/sdf.cs index e520b9c..cc62d67 100644 --- a/ManagedCode.Communication.Extensions/sdf.cs +++ b/ManagedCode.Communication.Extensions/sdf.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using ManagedCode.Communication.Extensions.Constants; namespace ManagedCode.Communication.Extensions; @@ -26,13 +27,13 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e Instance = httpContext.Request.Path, Extensions = { - ["traceId"] = Activity.Current?.Id ?? httpContext.TraceIdentifier + [ProblemConstants.ExtensionKeys.TraceId] = Activity.Current?.Id ?? httpContext.TraceIdentifier } }; if (exception.InnerException is not null) { - problemDetails.Extensions["innerException"] = new + problemDetails.Extensions[ProblemConstants.ExtensionKeys.InnerException] = new { Title = exception.InnerException.GetType().FullName, Detail = exception.InnerException.Message @@ -41,7 +42,7 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e if (webHostEnvironment.IsDevelopment()) { - problemDetails.Extensions["stackTrace"] = exception.StackTrace; + problemDetails.Extensions[ProblemConstants.ExtensionKeys.StackTrace] = exception.StackTrace; } await problemDetailsService.WriteAsync(new() diff --git a/ManagedCode.Communication/CollectionResultT/CollectionResult.cs b/ManagedCode.Communication/CollectionResultT/CollectionResult.cs index d5937b4..3c23780 100644 --- a/ManagedCode.Communication/CollectionResultT/CollectionResult.cs +++ b/ManagedCode.Communication/CollectionResultT/CollectionResult.cs @@ -47,7 +47,7 @@ public void AddError(Error error) [MemberNotNullWhen(false, nameof(Collection))] public bool ThrowIfFail() { - if(IsSuccess) + if (IsSuccess) return false; if (Errors?.Any() is not true) diff --git a/ManagedCode.Communication/Problem.cs b/ManagedCode.Communication/Problem.cs index 49b4493..d2dfc1f 100644 --- a/ManagedCode.Communication/Problem.cs +++ b/ManagedCode.Communication/Problem.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Net; using System.Text.Json.Serialization; namespace ManagedCode.Communication; @@ -33,10 +34,10 @@ public class Problem /// /// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem. /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [JsonPropertyOrder(-3)] [JsonPropertyName("status")] - public int? Status { get; set; } + public HttpStatusCode Status { get; set; } /// /// A human-readable explanation specific to this occurrence of the problem. diff --git a/ManagedCode.Communication/Result/Result.cs b/ManagedCode.Communication/Result/Result.cs index adfdcf6..6f9bdfd 100644 --- a/ManagedCode.Communication/Result/Result.cs +++ b/ManagedCode.Communication/Result/Result.cs @@ -71,7 +71,7 @@ public void AddError(Error error) /// public bool ThrowIfFail() { - if(IsSuccess) + if (IsSuccess) return false; if (Errors?.Any() is not true) diff --git a/ManagedCode.Communication/ResultT/Result.cs b/ManagedCode.Communication/ResultT/Result.cs index bbf79f0..87b6f4d 100644 --- a/ManagedCode.Communication/ResultT/Result.cs +++ b/ManagedCode.Communication/ResultT/Result.cs @@ -58,7 +58,7 @@ public void AddError(Error error) [MemberNotNullWhen(false, nameof(Value))] public bool ThrowIfFail() { - if(IsSuccess) + if (IsSuccess) return false; if (Errors?.Any() is not true) From 4ed3e913e7483f73604ea9900d442597ca1f6090 Mon Sep 17 00:00:00 2001 From: VladKovtun99 Date: Mon, 4 Aug 2025 14:57:05 +0200 Subject: [PATCH 5/6] Refactor ProblemConstants usage in ServiceCollectionExtensions; streamline extension key references --- .../Extensions/ServiceCollectionExtensions.cs | 13 +---- ManagedCode.Communication.Extensions/sdf.cs | 58 ------------------- 2 files changed, 2 insertions(+), 69 deletions(-) delete mode 100644 ManagedCode.Communication.Extensions/sdf.cs diff --git a/ManagedCode.Communication.Extensions/Extensions/ServiceCollectionExtensions.cs b/ManagedCode.Communication.Extensions/Extensions/ServiceCollectionExtensions.cs index 5b16278..f130edc 100644 --- a/ManagedCode.Communication.Extensions/Extensions/ServiceCollectionExtensions.cs +++ b/ManagedCode.Communication.Extensions/Extensions/ServiceCollectionExtensions.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using ManagedCode.Communication.Extensions.Constants; +using static ManagedCode.Communication.Extensions.Constants.ProblemConstants; namespace ManagedCode.Communication.Extensions.Extensions; @@ -50,22 +50,13 @@ public static IServiceCollection AddDefaultProblemDetails(this IServiceCollectio context.ProblemDetails.Type ??= $"https://httpstatuses.io/{statusCode}"; context.ProblemDetails.Title ??= ReasonPhrases.GetReasonPhrase(statusCode); context.ProblemDetails.Instance ??= context.HttpContext.Request.Path; - context.ProblemDetails.Extensions.TryAdd(ProblemConstants.ExtensionKeys.TraceId, Activity.Current?.Id ?? context.HttpContext.TraceIdentifier); + context.ProblemDetails.Extensions.TryAdd(ExtensionKeys.TraceId, Activity.Current?.Id ?? context.HttpContext.TraceIdentifier); }; }); return services; } - public static IServiceCollection AddCommunicationExceptionHandler(this IServiceCollection services) - { - // Ensures that the ProblemDetails service is registered. - services.AddProblemDetails(); - - services.AddExceptionHandler(); - return services; - } - public static IServiceCollection AddCommunicationFilters( this IServiceCollection services) where TExceptionFilter : ExceptionFilterBase diff --git a/ManagedCode.Communication.Extensions/sdf.cs b/ManagedCode.Communication.Extensions/sdf.cs deleted file mode 100644 index cc62d67..0000000 --- a/ManagedCode.Communication.Extensions/sdf.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Diagnostics; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using ManagedCode.Communication.Extensions.Extensions; -using Microsoft.AspNetCore.Diagnostics; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using ManagedCode.Communication.Extensions.Constants; - -namespace ManagedCode.Communication.Extensions; - -internal class CommunicationExceptionHandler(IProblemDetailsService problemDetailsService, IWebHostEnvironment webHostEnvironment) : IExceptionHandler -{ - public async ValueTask TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken) - { - var problemDetails = new ProblemDetails - { - Status = httpContext.Response.StatusCode, - Title = exception.GetType().FullName, - Detail = exception.Message, - Instance = httpContext.Request.Path, - Extensions = - { - [ProblemConstants.ExtensionKeys.TraceId] = Activity.Current?.Id ?? httpContext.TraceIdentifier - } - }; - - if (exception.InnerException is not null) - { - problemDetails.Extensions[ProblemConstants.ExtensionKeys.InnerException] = new - { - Title = exception.InnerException.GetType().FullName, - Detail = exception.InnerException.Message - }; - } - - if (webHostEnvironment.IsDevelopment()) - { - problemDetails.Extensions[ProblemConstants.ExtensionKeys.StackTrace] = exception.StackTrace; - } - - await problemDetailsService.WriteAsync(new() - { - HttpContext = httpContext, - AdditionalMetadata = httpContext.Features.Get()?.Endpoint?.Metadata, - ProblemDetails = problemDetails, - Exception = exception - }); - - return true; - } -} \ No newline at end of file From 8cbaa33a4d9e08ace10d643502d32acfafa5ee9c Mon Sep 17 00:00:00 2001 From: VladKovtun99 Date: Mon, 4 Aug 2025 15:04:20 +0200 Subject: [PATCH 6/6] Use NullLogger --- ManagedCode.Communication.Extensions/ExceptionFilterBase.cs | 5 +++-- .../HubExceptionFilterBase.cs | 5 +++-- .../Common/TestApp/TestExceptionFilter.cs | 3 +-- .../Common/TestApp/TestHubExceptionFilter.cs | 3 +-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ManagedCode.Communication.Extensions/ExceptionFilterBase.cs b/ManagedCode.Communication.Extensions/ExceptionFilterBase.cs index e563d1e..0467b71 100644 --- a/ManagedCode.Communication.Extensions/ExceptionFilterBase.cs +++ b/ManagedCode.Communication.Extensions/ExceptionFilterBase.cs @@ -3,14 +3,15 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using static ManagedCode.Communication.Extensions.Helpers.HttpStatusCodeHelper; using static ManagedCode.Communication.Extensions.Constants.ProblemConstants; namespace ManagedCode.Communication.Extensions; -public abstract class ExceptionFilterBase(ILogger logger) : IExceptionFilter +public abstract class ExceptionFilterBase : IExceptionFilter { - protected readonly ILogger Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + protected readonly ILogger Logger = NullLogger.Instance; public virtual void OnException(ExceptionContext context) { diff --git a/ManagedCode.Communication.Extensions/HubExceptionFilterBase.cs b/ManagedCode.Communication.Extensions/HubExceptionFilterBase.cs index b6c01d9..b7c05b6 100644 --- a/ManagedCode.Communication.Extensions/HubExceptionFilterBase.cs +++ b/ManagedCode.Communication.Extensions/HubExceptionFilterBase.cs @@ -2,14 +2,15 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using static ManagedCode.Communication.Extensions.Helpers.HttpStatusCodeHelper; using static ManagedCode.Communication.Extensions.Constants.ProblemConstants; namespace ManagedCode.Communication.Extensions; -public abstract class HubExceptionFilterBase(ILogger logger) : IHubFilter +public abstract class HubExceptionFilterBase : IHubFilter { - protected readonly ILogger Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + protected readonly ILogger Logger = NullLogger.Instance; public async ValueTask InvokeMethodAsync(HubInvocationContext invocationContext, Func> next) diff --git a/ManagedCode.Communication.Tests/Common/TestApp/TestExceptionFilter.cs b/ManagedCode.Communication.Tests/Common/TestApp/TestExceptionFilter.cs index 2fd2a7a..ddcbf91 100644 --- a/ManagedCode.Communication.Tests/Common/TestApp/TestExceptionFilter.cs +++ b/ManagedCode.Communication.Tests/Common/TestApp/TestExceptionFilter.cs @@ -1,6 +1,5 @@ using ManagedCode.Communication.Extensions; -using Microsoft.Extensions.Logging; namespace ManagedCode.Communication.Tests.Common.TestApp; -public class TestExceptionFilter(ILogger logger) : ExceptionFilterBase(logger); \ No newline at end of file +public class TestExceptionFilter : ExceptionFilterBase; \ No newline at end of file diff --git a/ManagedCode.Communication.Tests/Common/TestApp/TestHubExceptionFilter.cs b/ManagedCode.Communication.Tests/Common/TestApp/TestHubExceptionFilter.cs index bdd2985..e8e25b0 100644 --- a/ManagedCode.Communication.Tests/Common/TestApp/TestHubExceptionFilter.cs +++ b/ManagedCode.Communication.Tests/Common/TestApp/TestHubExceptionFilter.cs @@ -1,6 +1,5 @@ using ManagedCode.Communication.Extensions; -using Microsoft.Extensions.Logging; namespace ManagedCode.Communication.Tests.Common.TestApp; -public class TestHubExceptionFilter(ILogger logger) : HubExceptionFilterBase(logger); \ No newline at end of file +public class TestHubExceptionFilter : HubExceptionFilterBase; \ No newline at end of file