From 52ea126084aa83b07852ea21e76dd9c9c06913e1 Mon Sep 17 00:00:00 2001 From: zach painter Date: Sat, 3 Aug 2024 01:20:50 -0700 Subject: [PATCH 1/3] update wrapper --- src/MediatR/Wrappers/RequestHandlerWrapper.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/MediatR/Wrappers/RequestHandlerWrapper.cs b/src/MediatR/Wrappers/RequestHandlerWrapper.cs index 1550ecf8..dc213560 100644 --- a/src/MediatR/Wrappers/RequestHandlerWrapper.cs +++ b/src/MediatR/Wrappers/RequestHandlerWrapper.cs @@ -34,14 +34,14 @@ public class RequestHandlerWrapperImpl : RequestHandlerWrap public override Task Handle(IRequest request, IServiceProvider serviceProvider, CancellationToken cancellationToken) { - Task Handler() => serviceProvider.GetRequiredService>() - .Handle((TRequest) request, cancellationToken); + Task Handler(CancellationToken t = default) => serviceProvider.GetRequiredService>() + .Handle((TRequest) request, t == default ? cancellationToken : t); return serviceProvider .GetServices>() .Reverse() .Aggregate((RequestHandlerDelegate) Handler, - (next, pipeline) => () => pipeline.Handle((TRequest) request, next, cancellationToken))(); + (next, pipeline) => (t) => pipeline.Handle((TRequest) request, next, t == default ? cancellationToken : t))(); } } @@ -55,10 +55,10 @@ public class RequestHandlerWrapperImpl : RequestHandlerWrapper public override Task Handle(IRequest request, IServiceProvider serviceProvider, CancellationToken cancellationToken) { - async Task Handler() + async Task Handler(CancellationToken t = default) { await serviceProvider.GetRequiredService>() - .Handle((TRequest) request, cancellationToken); + .Handle((TRequest) request, t == default ? cancellationToken : t); return Unit.Value; } @@ -67,6 +67,6 @@ await serviceProvider.GetRequiredService>() .GetServices>() .Reverse() .Aggregate((RequestHandlerDelegate) Handler, - (next, pipeline) => () => pipeline.Handle((TRequest) request, next, cancellationToken))(); + (next, pipeline) => (t) => pipeline.Handle((TRequest) request, next, t == default ? cancellationToken : t))(); } } \ No newline at end of file From cca7861c091fdb4207d57e504fd646afe9917b62 Mon Sep 17 00:00:00 2001 From: zach painter Date: Sat, 3 Aug 2024 01:22:56 -0700 Subject: [PATCH 2/3] modify pipline behavior abstraction --- src/MediatR/IPipelineBehavior.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MediatR/IPipelineBehavior.cs b/src/MediatR/IPipelineBehavior.cs index be6b1cb3..142ec707 100644 --- a/src/MediatR/IPipelineBehavior.cs +++ b/src/MediatR/IPipelineBehavior.cs @@ -9,7 +9,7 @@ namespace MediatR; /// /// Response type /// Awaitable task returning a -public delegate Task RequestHandlerDelegate(); +public delegate Task RequestHandlerDelegate(CancellationToken t = default); /// /// Pipeline behavior to surround the inner handler. From 83dbba0c02d6964850a30268693b57c45d0feaad Mon Sep 17 00:00:00 2001 From: zach painter Date: Sat, 3 Aug 2024 01:25:16 -0700 Subject: [PATCH 3/3] add tests --- test/MediatR.Tests/SendTests.cs | 474 +++++++++++++++++++------------- 1 file changed, 277 insertions(+), 197 deletions(-) diff --git a/test/MediatR.Tests/SendTests.cs b/test/MediatR.Tests/SendTests.cs index 553c689c..e1ea1cfd 100644 --- a/test/MediatR.Tests/SendTests.cs +++ b/test/MediatR.Tests/SendTests.cs @@ -1,108 +1,113 @@ -using System.Threading; - -using System; -using System.Threading.Tasks; -using Shouldly; -using Xunit; -using Microsoft.Extensions.DependencyInjection; +using System.Threading; + +using System; +using System.Threading.Tasks; +using Shouldly; +using Xunit; +using Microsoft.Extensions.DependencyInjection; using System.Reflection; - -namespace MediatR.Tests; -public class SendTests -{ +using MediatR.Pipeline; + +namespace MediatR.Tests; +public class SendTests +{ private readonly IServiceProvider _serviceProvider; - private Dependency _dependency; + private Dependency _dependency; private readonly IMediator _mediator; public SendTests() { _dependency = new Dependency(); var services = new ServiceCollection(); - services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(typeof(Ping).Assembly)); + services.AddMediatR(cfg => + { + cfg.RegisterServicesFromAssemblies(typeof(Ping).Assembly); + cfg.AddOpenBehavior(typeof(TimeoutBehavior<,>), ServiceLifetime.Transient); + }); services.AddSingleton(_dependency); _serviceProvider = services.BuildServiceProvider(); _mediator = _serviceProvider.GetService()!; - } - - public class Ping : IRequest - { - public string? Message { get; set; } - } - - public class VoidPing : IRequest - { - } - - public class Pong - { - public string? Message { get; set; } - } - - public class PingHandler : IRequestHandler - { - public Task Handle(Ping request, CancellationToken cancellationToken) - { - return Task.FromResult(new Pong { Message = request.Message + " Pong" }); - } - } - - public class Dependency - { - public bool Called { get; set; } - public bool CalledSpecific { get; set; } - } - - public class VoidPingHandler : IRequestHandler - { - private readonly Dependency _dependency; - - public VoidPingHandler(Dependency dependency) => _dependency = dependency; - - public Task Handle(VoidPing request, CancellationToken cancellationToken) - { - _dependency.Called = true; - - return Task.CompletedTask; - } - } - - public class GenericPing : IRequest - where T : Pong - { - public T? Pong { get; set; } - } - - public class GenericPingHandler : IRequestHandler, T> - where T : Pong - { - private readonly Dependency _dependency; - - public GenericPingHandler(Dependency dependency) => _dependency = dependency; - - public Task Handle(GenericPing request, CancellationToken cancellationToken) - { - _dependency.Called = true; - request.Pong!.Message += " Pong"; - return Task.FromResult(request.Pong!); - } - } - - public class VoidGenericPing : IRequest - where T : Pong - { } - - public class VoidGenericPingHandler : IRequestHandler> - where T : Pong - { - private readonly Dependency _dependency; - public VoidGenericPingHandler(Dependency dependency) => _dependency = dependency; - - public Task Handle(VoidGenericPing request, CancellationToken cancellationToken) - { - _dependency.Called = true; - - return Task.CompletedTask; - } + } + + public class Ping : IRequest + { + public string? Message { get; set; } + } + + public class VoidPing : IRequest + { + } + + public class Pong + { + public string? Message { get; set; } + } + + public class PingHandler : IRequestHandler + { + public Task Handle(Ping request, CancellationToken cancellationToken) + { + return Task.FromResult(new Pong { Message = request.Message + " Pong" }); + } + } + + public class Dependency + { + public bool Called { get; set; } + public bool CalledSpecific { get; set; } + } + + public class VoidPingHandler : IRequestHandler + { + private readonly Dependency _dependency; + + public VoidPingHandler(Dependency dependency) => _dependency = dependency; + + public Task Handle(VoidPing request, CancellationToken cancellationToken) + { + _dependency.Called = true; + + return Task.CompletedTask; + } + } + + public class GenericPing : IRequest + where T : Pong + { + public T? Pong { get; set; } + } + + public class GenericPingHandler : IRequestHandler, T> + where T : Pong + { + private readonly Dependency _dependency; + + public GenericPingHandler(Dependency dependency) => _dependency = dependency; + + public Task Handle(GenericPing request, CancellationToken cancellationToken) + { + _dependency.Called = true; + request.Pong!.Message += " Pong"; + return Task.FromResult(request.Pong!); + } + } + + public class VoidGenericPing : IRequest + where T : Pong + { } + + public class VoidGenericPingHandler : IRequestHandler> + where T : Pong + { + private readonly Dependency _dependency; + public VoidGenericPingHandler(Dependency dependency) => _dependency = dependency; + + public Task Handle(VoidGenericPing request, CancellationToken cancellationToken) + { + _dependency.Called = true; + + return Task.CompletedTask; + } } public class PongExtension : Pong @@ -123,22 +128,22 @@ public Task Handle(VoidGenericPing request, CancellationToken can } } - public interface ITestInterface1 { } - public interface ITestInterface2 { } + public interface ITestInterface1 { } + public interface ITestInterface2 { } public interface ITestInterface3 { } public class TestClass1 : ITestInterface1 { } public class TestClass2 : ITestInterface2 { } public class TestClass3 : ITestInterface3 { } - public class MultipleGenericTypeParameterRequest : IRequest - where T1 : ITestInterface1 - where T2 : ITestInterface2 - where T3 : ITestInterface3 - { - public int Foo { get; set; } - } - + public class MultipleGenericTypeParameterRequest : IRequest + where T1 : ITestInterface1 + where T2 : ITestInterface2 + where T3 : ITestInterface3 + { + public int Foo { get; set; } + } + public class MultipleGenericTypeParameterRequestHandler : IRequestHandler, int> where T1 : ITestInterface1 where T2 : ITestInterface2 @@ -148,92 +153,141 @@ public class MultipleGenericTypeParameterRequestHandler : IRequestHa public MultipleGenericTypeParameterRequestHandler(Dependency dependency) => _dependency = dependency; - public Task Handle(MultipleGenericTypeParameterRequest request, CancellationToken cancellationToken) - { - _dependency.Called = true; - return Task.FromResult(1); - } - } - - [Fact] - public async Task Should_resolve_main_handler() - { - var response = await _mediator.Send(new Ping { Message = "Ping" }); - - response.Message.ShouldBe("Ping Pong"); - } - - [Fact] - public async Task Should_resolve_main_void_handler() - { - await _mediator.Send(new VoidPing()); - - _dependency.Called.ShouldBeTrue(); - } - - [Fact] - public async Task Should_resolve_main_handler_via_dynamic_dispatch() - { - object request = new Ping { Message = "Ping" }; - var response = await _mediator.Send(request); - - var pong = response.ShouldBeOfType(); - pong.Message.ShouldBe("Ping Pong"); - } - - [Fact] - public async Task Should_resolve_main_void_handler_via_dynamic_dispatch() - { - object request = new VoidPing(); - var response = await _mediator.Send(request); - - response.ShouldBeOfType(); - - _dependency.Called.ShouldBeTrue(); - } - - [Fact] - public async Task Should_resolve_main_handler_by_specific_interface() - { - var response = await _mediator.Send(new Ping { Message = "Ping" }); - - response.Message.ShouldBe("Ping Pong"); - } - - [Fact] - public async Task Should_resolve_main_handler_by_given_interface() - { - // wrap requests in an array, so this test won't break on a 'replace with var' refactoring - var requests = new IRequest[] { new VoidPing() }; - await _mediator.Send(requests[0]); - - _dependency.Called.ShouldBeTrue(); - } - - [Fact] - public Task Should_raise_execption_on_null_request() => Should.ThrowAsync(async () => await _mediator.Send(default!)); - - [Fact] - public async Task Should_resolve_generic_handler() + public Task Handle(MultipleGenericTypeParameterRequest request, CancellationToken cancellationToken) + { + _dependency.Called = true; + return Task.FromResult(1); + } + } + + public class TimeoutBehavior : IPipelineBehavior + where TRequest : notnull + { + public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + using (var cts = new CancellationTokenSource(500)) + { + return await next(cts.Token); + } + } + } + + public class TimeoutRequest : IRequest + { + } + + public class TimeoutRequest2 : IRequest + { + } + + public class TimeoutRequestHandler : IRequestHandler + { + private readonly Dependency _dependency; + + public TimeoutRequestHandler(Dependency dependency) => _dependency = dependency; + + public async Task Handle(TimeoutRequest request, CancellationToken cancellationToken) + { + await Task.Delay(2000, cancellationToken); + + _dependency.Called = true; + } + } + + public class TimeoutRequest2Handler : IRequestHandler + { + private readonly Dependency _dependency; + + public TimeoutRequest2Handler(Dependency dependency) => _dependency = dependency; + + public async Task Handle(TimeoutRequest2 request, CancellationToken cancellationToken) + { + await Task.Delay(2000, cancellationToken); + + _dependency.Called = true; + return 1; + } + } + + [Fact] + public async Task Should_resolve_main_handler() + { + var response = await _mediator.Send(new Ping { Message = "Ping" }); + + response.Message.ShouldBe("Ping Pong"); + } + + [Fact] + public async Task Should_resolve_main_void_handler() + { + await _mediator.Send(new VoidPing()); + + _dependency.Called.ShouldBeTrue(); + } + + [Fact] + public async Task Should_resolve_main_handler_via_dynamic_dispatch() + { + object request = new Ping { Message = "Ping" }; + var response = await _mediator.Send(request); + + var pong = response.ShouldBeOfType(); + pong.Message.ShouldBe("Ping Pong"); + } + + [Fact] + public async Task Should_resolve_main_void_handler_via_dynamic_dispatch() + { + object request = new VoidPing(); + var response = await _mediator.Send(request); + + response.ShouldBeOfType(); + + _dependency.Called.ShouldBeTrue(); + } + + [Fact] + public async Task Should_resolve_main_handler_by_specific_interface() + { + var response = await _mediator.Send(new Ping { Message = "Ping" }); + + response.Message.ShouldBe("Ping Pong"); + } + + [Fact] + public async Task Should_resolve_main_handler_by_given_interface() + { + // wrap requests in an array, so this test won't break on a 'replace with var' refactoring + var requests = new IRequest[] { new VoidPing() }; + await _mediator.Send(requests[0]); + + _dependency.Called.ShouldBeTrue(); + } + + [Fact] + public Task Should_raise_execption_on_null_request() => Should.ThrowAsync(async () => await _mediator.Send(default!)); + + [Fact] + public async Task Should_resolve_generic_handler() { var request = new GenericPing { Pong = new Pong { Message = "Ping" } }; - var result = await _mediator.Send(request); - - var pong = result.ShouldBeOfType(); - pong.Message.ShouldBe("Ping Pong"); - - _dependency.Called.ShouldBeTrue(); - } - - [Fact] - public async Task Should_resolve_generic_void_handler() - { - var request = new VoidGenericPing(); - await _mediator.Send(request); - - _dependency.Called.ShouldBeTrue(); - } - + var result = await _mediator.Send(request); + + var pong = result.ShouldBeOfType(); + pong.Message.ShouldBe("Ping Pong"); + + _dependency.Called.ShouldBeTrue(); + } + + [Fact] + public async Task Should_resolve_generic_void_handler() + { + var request = new VoidGenericPing(); + await _mediator.Send(request); + + _dependency.Called.ShouldBeTrue(); + } + [Fact] public async Task Should_resolve_multiple_type_parameter_generic_handler() { @@ -241,16 +295,16 @@ public async Task Should_resolve_multiple_type_parameter_generic_handler() await _mediator.Send(request); _dependency.Called.ShouldBeTrue(); - } - - [Fact] + } + + [Fact] public async Task Should_resolve_closed_handler_if_defined() { var dependency = new Dependency(); var services = new ServiceCollection(); - services.AddSingleton(dependency); + services.AddSingleton(dependency); services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(Assembly.GetExecutingAssembly())); - services.AddTransient>,TestClass1PingRequestHandler>(); + services.AddTransient>, TestClass1PingRequestHandler>(); var serviceProvider = services.BuildServiceProvider(); var mediator = serviceProvider.GetService()!; @@ -259,9 +313,9 @@ public async Task Should_resolve_closed_handler_if_defined() dependency.Called.ShouldBeFalse(); dependency.CalledSpecific.ShouldBeTrue(); - } - - [Fact] + } + + [Fact] public async Task Should_resolve_open_handler_if_not_defined() { var dependency = new Dependency(); @@ -277,5 +331,31 @@ public async Task Should_resolve_open_handler_if_not_defined() dependency.Called.ShouldBeTrue(); dependency.CalledSpecific.ShouldBeFalse(); - } + } + + [Fact] + public async Task TimeoutBehavior_Void_Should_Cancel_Long_Running_Task_And_Throw_Exception() + { + var request = new TimeoutRequest(); + + var exception = await Should.ThrowAsync(() => _mediator.Send(request)); + + exception.ShouldNotBeNull(); + exception.ShouldBeAssignableTo(); + _dependency.Called.ShouldBeFalse(); + } + + [Fact] + public async Task TimeoutBehavior_NonVoid_Should_Cancel_Long_Running_Task_And_Throw_Exception() + { + var request = new TimeoutRequest2(); + int result = 0; + + var exception = await Should.ThrowAsync(async () => { result = await _mediator.Send(request); }); + + exception.ShouldNotBeNull(); + exception.ShouldBeAssignableTo(); + _dependency.Called.ShouldBeFalse(); + result.ShouldBe(0); + } } \ No newline at end of file