From 4bb410a0a9b9c0e174f7a25778ed089ba46626e6 Mon Sep 17 00:00:00 2001 From: Jimmy Bogard Date: Mon, 30 Dec 2019 10:04:52 -0600 Subject: [PATCH 1/2] Adding generic/base exception handlers --- Directory.Build.props | 2 +- ...sions.Microsoft.DependencyInjection.csproj | 2 +- .../Registration/ServiceRegistrar.cs | 8 +-- src/TestApp/Ping.cs | 1 + src/TestApp/PingHandler.cs | 7 +++ src/TestApp/PingPongExceptionHandlers.cs | 17 ++++++ .../Handlers.cs | 13 ++++- ...Microsoft.DependencyInjection.Tests.csproj | 2 +- .../PipelineTests.cs | 54 +++++++++++++++++++ 9 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 src/TestApp/PingPongExceptionHandlers.cs diff --git a/Directory.Build.props b/Directory.Build.props index f40defb..da4a861 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,6 +2,6 @@ Jimmy Bogard latest - 7.0.0 + 8.0.0 diff --git a/src/MediatR.Extensions.Microsoft.DependencyInjection/MediatR.Extensions.Microsoft.DependencyInjection.csproj b/src/MediatR.Extensions.Microsoft.DependencyInjection/MediatR.Extensions.Microsoft.DependencyInjection.csproj index 87af5fe..c7a5be0 100644 --- a/src/MediatR.Extensions.Microsoft.DependencyInjection/MediatR.Extensions.Microsoft.DependencyInjection.csproj +++ b/src/MediatR.Extensions.Microsoft.DependencyInjection/MediatR.Extensions.Microsoft.DependencyInjection.csproj @@ -21,7 +21,7 @@ - + diff --git a/src/MediatR.Extensions.Microsoft.DependencyInjection/Registration/ServiceRegistrar.cs b/src/MediatR.Extensions.Microsoft.DependencyInjection/Registration/ServiceRegistrar.cs index 6c9b260..c84361a 100644 --- a/src/MediatR.Extensions.Microsoft.DependencyInjection/Registration/ServiceRegistrar.cs +++ b/src/MediatR.Extensions.Microsoft.DependencyInjection/Registration/ServiceRegistrar.cs @@ -18,6 +18,7 @@ public static void AddMediatRClasses(IServiceCollection services, IEnumerable), services, assembliesToScan, true); ConnectImplementationsToTypesClosing(typeof(IRequestPreProcessor<>), services, assembliesToScan, true); ConnectImplementationsToTypesClosing(typeof(IRequestPostProcessor<,>), services, assembliesToScan, true); + ConnectImplementationsToTypesClosing(typeof(IRequestExceptionHandler<,,>), services, assembliesToScan, true); var multiOpenInterfaces = new[] { @@ -30,7 +31,7 @@ public static void AddMediatRClasses(IServiceCollection services, IEnumerable a.DefinedTypes) - .Where(type => Enumerable.Any(type.FindInterfacesThatClose(multiOpenInterface))) + .Where(type => type.FindInterfacesThatClose(multiOpenInterface).Any()) .Where(type => type.IsConcrete() && type.IsOpenGeneric()) .ToList(); @@ -59,7 +60,7 @@ private static void ConnectImplementationsToTypesClosing(Type openRequestInterfa var interfaces = new List(); foreach (var type in assembliesToScan.SelectMany(a => a.DefinedTypes).Where(t => !t.IsOpenGeneric())) { - var interfaceTypes = Enumerable.ToArray(type.FindInterfacesThatClose(openRequestInterface)); + var interfaceTypes = type.FindInterfacesThatClose(openRequestInterface).ToArray(); if (!interfaceTypes.Any()) continue; if (type.IsConcrete()) @@ -165,7 +166,7 @@ public static bool IsOpenGeneric(this Type type) public static IEnumerable FindInterfacesThatClose(this Type pluggedType, Type templateType) { - return Enumerable.Distinct(FindInterfacesThatClosesCore(pluggedType, templateType)); + return FindInterfacesThatClosesCore(pluggedType, templateType).Distinct(); } private static IEnumerable FindInterfacesThatClosesCore(Type pluggedType, Type templateType) @@ -214,6 +215,7 @@ public static void AddRequiredServices(IServiceCollection services, MediatRServi services.AddTransient(p => p.GetService); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestPreProcessorBehavior<,>)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestPostProcessorBehavior<,>)); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestExceptionProcessorBehavior<,>)); services.Add(new ServiceDescriptor(typeof(IMediator), serviceConfiguration.MediatorImplementationType, serviceConfiguration.Lifetime)); } } diff --git a/src/TestApp/Ping.cs b/src/TestApp/Ping.cs index d203fb1..3b9a69c 100644 --- a/src/TestApp/Ping.cs +++ b/src/TestApp/Ping.cs @@ -5,5 +5,6 @@ namespace TestApp public class Ping : IRequest { public string Message { get; set; } + public bool Throw { get; set; } } } \ No newline at end of file diff --git a/src/TestApp/PingHandler.cs b/src/TestApp/PingHandler.cs index 1dd0744..529c2f4 100644 --- a/src/TestApp/PingHandler.cs +++ b/src/TestApp/PingHandler.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using System.Threading; using MediatR; @@ -18,6 +19,12 @@ public PingHandler(TextWriter writer) public async Task Handle(Ping request, CancellationToken cancellationToken) { await _writer.WriteLineAsync($"--- Handled Ping: {request.Message}"); + + if (request.Throw) + { + throw new ApplicationException("Requested to throw"); + } + return new Pong { Message = request.Message + " Pong" }; } } diff --git a/src/TestApp/PingPongExceptionHandlers.cs b/src/TestApp/PingPongExceptionHandlers.cs new file mode 100644 index 0000000..cd12f61 --- /dev/null +++ b/src/TestApp/PingPongExceptionHandlers.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MediatR.Pipeline; + +namespace TestApp +{ + public class PingPongExceptionHandlerForType : IRequestExceptionHandler + { + public Task Handle(Ping request, ApplicationException exception, RequestExceptionHandlerState state, CancellationToken cancellationToken) + { + state.SetHandled(new Pong { Message = exception.Message + " Handled by Type" }); + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/test/MediatR.Extensions.Microsoft.DependencyInjection.Tests/Handlers.cs b/test/MediatR.Extensions.Microsoft.DependencyInjection.Tests/Handlers.cs index e715e42..66c6edb 100644 --- a/test/MediatR.Extensions.Microsoft.DependencyInjection.Tests/Handlers.cs +++ b/test/MediatR.Extensions.Microsoft.DependencyInjection.Tests/Handlers.cs @@ -1,4 +1,6 @@ -namespace MediatR.Extensions.Microsoft.DependencyInjection.Tests +using System; + +namespace MediatR.Extensions.Microsoft.DependencyInjection.Tests { using System.Collections.Generic; using System.Threading; @@ -7,6 +9,7 @@ public class Ping : IRequest { public string Message { get; set; } + public Action ThrowAction { get; set; } } public class DerivedPing : Ping @@ -85,6 +88,9 @@ public PingHandler(Logger logger) public Task Handle(Ping message, CancellationToken cancellationToken) { _logger.Messages.Add("Handler"); + + message.ThrowAction?.Invoke(message); + return Task.FromResult(new Pong { Message = message.Message + " Pong" }); } } @@ -143,6 +149,11 @@ class InternalPingHandler : IRequestHandler class MyCustomMediator : IMediator { + public Task Send(object request, CancellationToken cancellationToken = new CancellationToken()) + { + throw new System.NotImplementedException(); + } + public Task Publish(object notification, CancellationToken cancellationToken = new CancellationToken()) { throw new System.NotImplementedException(); diff --git a/test/MediatR.Extensions.Microsoft.DependencyInjection.Tests/MediatR.Extensions.Microsoft.DependencyInjection.Tests.csproj b/test/MediatR.Extensions.Microsoft.DependencyInjection.Tests/MediatR.Extensions.Microsoft.DependencyInjection.Tests.csproj index 792e35f..e8fd32c 100644 --- a/test/MediatR.Extensions.Microsoft.DependencyInjection.Tests/MediatR.Extensions.Microsoft.DependencyInjection.Tests.csproj +++ b/test/MediatR.Extensions.Microsoft.DependencyInjection.Tests/MediatR.Extensions.Microsoft.DependencyInjection.Tests.csproj @@ -16,7 +16,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/MediatR.Extensions.Microsoft.DependencyInjection.Tests/PipelineTests.cs b/test/MediatR.Extensions.Microsoft.DependencyInjection.Tests/PipelineTests.cs index 6040331..37bb8be 100644 --- a/test/MediatR.Extensions.Microsoft.DependencyInjection.Tests/PipelineTests.cs +++ b/test/MediatR.Extensions.Microsoft.DependencyInjection.Tests/PipelineTests.cs @@ -231,6 +231,26 @@ public Task Process(Ping request, Pong response, CancellationToken cancellationT } } + public class PingPongExceptionHandlerForType : IRequestExceptionHandler + { + public Task Handle(Ping request, ApplicationException exception, RequestExceptionHandlerState state, CancellationToken cancellationToken) + { + state.SetHandled(new Pong { Message = exception.Message + " Handled by Specific Type" }); + + return Task.CompletedTask; + } + } + + public class PingPongGenericExceptionHandler : IRequestExceptionHandler + { + public Task Handle(Ping request, Exception exception, RequestExceptionHandlerState state, CancellationToken cancellationToken) + { + state.SetHandled(new Pong { Message = exception.Message + " Handled by Generic Type" }); + + return Task.CompletedTask; + } + } + [Fact] public async Task Should_wrap_with_behavior() { @@ -331,6 +351,40 @@ public async Task Should_pick_up_pre_and_post_processors() }); } + [Fact] + public async Task Should_pick_up_specific_exception_behaviors() + { + var output = new Logger(); + IServiceCollection services = new ServiceCollection(); + services.AddSingleton(output); + services.AddMediatR(typeof(Ping).GetTypeInfo().Assembly); + var provider = services.BuildServiceProvider(); + + var mediator = provider.GetService(); + + var response = await mediator.Send(new Ping {Message = "Ping", ThrowAction = msg => throw new ApplicationException(msg.Message + " Thrown")}); + + response.Message.ShouldBe("Ping Thrown Handled by Specific Type"); + + } + + [Fact] + public async Task Should_pick_up_base_exception_behaviors() + { + var output = new Logger(); + IServiceCollection services = new ServiceCollection(); + services.AddSingleton(output); + services.AddMediatR(typeof(Ping).GetTypeInfo().Assembly); + var provider = services.BuildServiceProvider(); + + var mediator = provider.GetService(); + + var response = await mediator.Send(new Ping {Message = "Ping", ThrowAction = msg => throw new Exception(msg.Message + " Thrown")}); + + response.Message.ShouldBe("Ping Thrown Handled by Generic Type"); + + } + [Fact(Skip = "MS DI does not support constrained generics yet, see https://github.com/aspnet/DependencyInjection/issues/471")] public async Task Should_handle_constrained_generics() { From 5c4bf2fd25fb1fea31b2352e17e2d9897dccba2b Mon Sep 17 00:00:00 2001 From: Jimmy Bogard Date: Mon, 30 Dec 2019 11:27:00 -0600 Subject: [PATCH 2/2] Updating to 8.0; adding more tests and docs --- README.md | 22 ++++- .../Registration/ServiceRegistrar.cs | 6 +- src/TestApp/PingPongExceptionHandlers.cs | 23 +++++ .../PipelineTests.cs | 87 +++++++++++++++++-- 4 files changed, 130 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index bbf2d52..04ef907 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,25 @@ or with an assembly: services.AddMediatR(typeof(Startup).GetTypeInfo().Assembly); ``` -Supports generic variance of handlers. +This registers: + +- `IMediator` as transient +- `IRequestHandler<>` concrete implementations as transient +- `INotificationHandler<>` concrete implementations as transient +- `IRequestPreProcessor<>` concrete implementations as transient +- `IRequestHandler<>` concrete implementations as transient +- `IRequestPostProcessor<,>` concrete implementations as transient +- `IRequestExceptionHandler<,,>` concrete implementations as transient + +This also registers open generic implementations for: + +- `INotificationHandler<>` +- `IRequestPreProcessor<>` +- `IRequestHandler<>` +- `IRequestPostProcessor<,>` +- `IRequestExceptionHandler<,,>` + +Keep in mind that the built-in container does not support constrained open generics. If you want this behavior, you will need to add any one of the conforming containers. To customize registration, such as lifecycle or the registration type: @@ -20,4 +38,4 @@ To customize registration, such as lifecycle or the registration type: services.AddMediatR(cfg => cfg.Using().AsSingleton(), typeof(Startup)); ``` -To register behaviors, pre- or post-processors, register them individually before or after calling `AddMediatR`. \ No newline at end of file +To register behaviors, register them individually before or after calling `AddMediatR`. \ No newline at end of file diff --git a/src/MediatR.Extensions.Microsoft.DependencyInjection/Registration/ServiceRegistrar.cs b/src/MediatR.Extensions.Microsoft.DependencyInjection/Registration/ServiceRegistrar.cs index c84361a..4276eab 100644 --- a/src/MediatR.Extensions.Microsoft.DependencyInjection/Registration/ServiceRegistrar.cs +++ b/src/MediatR.Extensions.Microsoft.DependencyInjection/Registration/ServiceRegistrar.cs @@ -19,12 +19,15 @@ public static void AddMediatRClasses(IServiceCollection services, IEnumerable), services, assembliesToScan, true); ConnectImplementationsToTypesClosing(typeof(IRequestPostProcessor<,>), services, assembliesToScan, true); ConnectImplementationsToTypesClosing(typeof(IRequestExceptionHandler<,,>), services, assembliesToScan, true); + ConnectImplementationsToTypesClosing(typeof(IRequestExceptionAction<,>), services, assembliesToScan, true); var multiOpenInterfaces = new[] { typeof(INotificationHandler<>), typeof(IRequestPreProcessor<>), - typeof(IRequestPostProcessor<,>) + typeof(IRequestPostProcessor<,>), + typeof(IRequestExceptionHandler<,,>), + typeof(IRequestExceptionAction<,>) }; foreach (var multiOpenInterface in multiOpenInterfaces) @@ -215,6 +218,7 @@ public static void AddRequiredServices(IServiceCollection services, MediatRServi services.AddTransient(p => p.GetService); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestPreProcessorBehavior<,>)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestPostProcessorBehavior<,>)); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestExceptionActionProcessorBehavior<,>)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestExceptionProcessorBehavior<,>)); services.Add(new ServiceDescriptor(typeof(IMediator), serviceConfiguration.MediatorImplementationType, serviceConfiguration.Lifetime)); } diff --git a/src/TestApp/PingPongExceptionHandlers.cs b/src/TestApp/PingPongExceptionHandlers.cs index cd12f61..e9b98a9 100644 --- a/src/TestApp/PingPongExceptionHandlers.cs +++ b/src/TestApp/PingPongExceptionHandlers.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Threading; using System.Threading.Tasks; using MediatR.Pipeline; @@ -14,4 +15,26 @@ public Task Handle(Ping request, ApplicationException exception, RequestExceptio return Task.CompletedTask; } } + + public class PingPongExceptionActionForType1 : IRequestExceptionAction + { + private readonly TextWriter _output; + + public PingPongExceptionActionForType1(TextWriter output) => _output = output; + + public Task Execute(Ping request, ApplicationException exception, CancellationToken cancellationToken) + => _output.WriteLineAsync("Logging exception 1"); + } + + public class PingPongExceptionActionForType2 : IRequestExceptionAction + { + private readonly TextWriter _output; + + public PingPongExceptionActionForType2(TextWriter output) => _output = output; + + public Task Execute(Ping request, ApplicationException exception, CancellationToken cancellationToken) + => _output.WriteLineAsync("Logging exception 2"); + } + + } \ No newline at end of file diff --git a/test/MediatR.Extensions.Microsoft.DependencyInjection.Tests/PipelineTests.cs b/test/MediatR.Extensions.Microsoft.DependencyInjection.Tests/PipelineTests.cs index 37bb8be..9e00ea0 100644 --- a/test/MediatR.Extensions.Microsoft.DependencyInjection.Tests/PipelineTests.cs +++ b/test/MediatR.Extensions.Microsoft.DependencyInjection.Tests/PipelineTests.cs @@ -231,6 +231,62 @@ public Task Process(Ping request, Pong response, CancellationToken cancellationT } } + public class PingPongGenericExceptionAction : IRequestExceptionAction + { + private readonly Logger _output; + + public PingPongGenericExceptionAction(Logger output) => _output = output; + + public Task Execute(Ping request, Exception exception, CancellationToken cancellationToken) + { + _output.Messages.Add("Logging generic exception"); + + return Task.CompletedTask; + } + } + + public class PingPongApplicationExceptionAction : IRequestExceptionAction + { + private readonly Logger _output; + + public PingPongApplicationExceptionAction(Logger output) => _output = output; + + public Task Execute(Ping request, ApplicationException exception, CancellationToken cancellationToken) + { + _output.Messages.Add("Logging ApplicationException exception"); + + return Task.CompletedTask; + } + } + + public class PingPongExceptionActionForType1 : IRequestExceptionAction + { + private readonly Logger _output; + + public PingPongExceptionActionForType1(Logger output) => _output = output; + + public Task Execute(Ping request, SystemException exception, CancellationToken cancellationToken) + { + _output.Messages.Add("Logging exception 1"); + + return Task.CompletedTask; + } + } + + public class PingPongExceptionActionForType2 : IRequestExceptionAction + { + private readonly Logger _output; + + public PingPongExceptionActionForType2(Logger output) => _output = output; + + public Task Execute(Ping request, SystemException exception, CancellationToken cancellationToken) + { + _output.Messages.Add("Logging exception 2"); + + return Task.CompletedTask; + } + } + public class PingPongExceptionHandlerForType : IRequestExceptionHandler { public Task Handle(Ping request, ApplicationException exception, RequestExceptionHandlerState state, CancellationToken cancellationToken) @@ -243,9 +299,13 @@ public Task Handle(Ping request, ApplicationException exception, RequestExceptio public class PingPongGenericExceptionHandler : IRequestExceptionHandler { + private readonly Logger _output; + + public PingPongGenericExceptionHandler(Logger output) => _output = output; + public Task Handle(Ping request, Exception exception, RequestExceptionHandlerState state, CancellationToken cancellationToken) { - state.SetHandled(new Pong { Message = exception.Message + " Handled by Generic Type" }); + _output.Messages.Add(exception.Message + " Logged by Generic Type"); return Task.CompletedTask; } @@ -365,11 +425,11 @@ public async Task Should_pick_up_specific_exception_behaviors() var response = await mediator.Send(new Ping {Message = "Ping", ThrowAction = msg => throw new ApplicationException(msg.Message + " Thrown")}); response.Message.ShouldBe("Ping Thrown Handled by Specific Type"); - + output.Messages.ShouldNotContain("Logging ApplicationException exception"); } [Fact] - public async Task Should_pick_up_base_exception_behaviors() + public void Should_pick_up_base_exception_behaviors() { var output = new Logger(); IServiceCollection services = new ServiceCollection(); @@ -379,10 +439,27 @@ public async Task Should_pick_up_base_exception_behaviors() var mediator = provider.GetService(); - var response = await mediator.Send(new Ping {Message = "Ping", ThrowAction = msg => throw new Exception(msg.Message + " Thrown")}); + Should.Throw(async () => await mediator.Send(new Ping {Message = "Ping", ThrowAction = msg => throw new Exception(msg.Message + " Thrown")})); + + output.Messages.ShouldContain("Ping Thrown Logged by Generic Type"); + output.Messages.ShouldContain("Logging generic exception"); + } + + [Fact] + public void Should_pick_up_exception_actions() + { + var output = new Logger(); + IServiceCollection services = new ServiceCollection(); + services.AddSingleton(output); + services.AddMediatR(typeof(Ping).GetTypeInfo().Assembly); + var provider = services.BuildServiceProvider(); + + var mediator = provider.GetService(); - response.Message.ShouldBe("Ping Thrown Handled by Generic Type"); + Should.Throw(async () => await mediator.Send(new Ping {Message = "Ping", ThrowAction = msg => throw new SystemException(msg.Message + " Thrown")})); + output.Messages.ShouldContain("Logging exception 1"); + output.Messages.ShouldContain("Logging exception 2"); } [Fact(Skip = "MS DI does not support constrained generics yet, see https://github.com/aspnet/DependencyInjection/issues/471")]