diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index f0b64676b..44c2f8805 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -56,6 +56,7 @@ "PulsarTests", "RabbitmqTests", "Restore", + "ShimsTests", "SqliteTests", "Test", "TestExtensions", diff --git a/build/build.cs b/build/build.cs index 4ad173797..1862f8b31 100644 --- a/build/build.cs +++ b/build/build.cs @@ -89,6 +89,19 @@ class Build : NukeBuild .SetFramework(Framework)); }); + Target ShimsTests => _ => _ + .DependsOn(Compile) + .ProceedAfterFailure() + .Executes(() => + { + DotNetTest(c => c + .SetProjectFile(Solution.Testing.ShimsTests) + .SetConfiguration(Configuration) + .EnableNoBuild() + .EnableNoRestore() + .SetFramework(Framework)); + }); + Target TestExtensions => _ => _ .DependsOn(FluentValidationTests, DataAnnotationsValidationTests, MemoryPackTests, MessagePackTests); @@ -566,4 +579,4 @@ private void removeProject(string repository, string projectName) -} \ No newline at end of file +} diff --git a/src/Testing/ShimsTests/MediatR/MediatRHandlers.cs b/src/Testing/ShimsTests/MediatR/MediatRHandlers.cs new file mode 100644 index 000000000..2f4a9720c --- /dev/null +++ b/src/Testing/ShimsTests/MediatR/MediatRHandlers.cs @@ -0,0 +1,39 @@ +using Wolverine.Shims.MediatR; +using Wolverine.Attributes; + +namespace Wolverine.Shims.Tests.MediatR; + +public class RequestWithResponseHandler : IRequestHandler +{ + public Task Handle(RequestWithResponse request, CancellationToken cancellationToken) + { + return Task.FromResult(new Response($"passed: {request.Data}", "MediatR")); + } +} + +public class RequestWithoutResponseHandler : IRequestHandler +{ + public Task Handle(RequestWithoutResponse request, CancellationToken cancellationToken) + { + // Just process and return + return Task.CompletedTask; + } +} + + +public class RequestCascadeHandler : IRequestHandler +{ + public async Task Handle(RequestCascade request, CancellationToken cancellationToken) + { + return new CascadingMessage(request.Data); + } +} + +public class RequestAdditionHandler(IAdditionService service) : IRequestHandler +{ + public async Task Handle(RequestAdditionFromService request, CancellationToken cancellationToken) + { + return service.Process(request.number); + } + +} diff --git a/src/Testing/ShimsTests/MediatR/Messages.cs b/src/Testing/ShimsTests/MediatR/Messages.cs new file mode 100644 index 000000000..1c4eccf9c --- /dev/null +++ b/src/Testing/ShimsTests/MediatR/Messages.cs @@ -0,0 +1,29 @@ +using Wolverine.Shims.MediatR; + +namespace Wolverine.Shims.Tests.MediatR; + +public record RequestWithResponse(string Data) : IRequest; +public record Response(string Data, string ProcessedBy); + +public record RequestWithoutResponse(string Data) : IRequest; + + +public record RequestCascade(string Data) : IRequest; +public record CascadingMessage(string Data); + +public record RequestAdditionFromService(int number) : IRequest; + + +public interface IAdditionService +{ + int Process(int number); +} + +public class AdditionService : IAdditionService +{ + public int Process(int number) + { + return number + 1; + } + +} diff --git a/src/Testing/ShimsTests/MediatR/WolverineHandlers.cs b/src/Testing/ShimsTests/MediatR/WolverineHandlers.cs new file mode 100644 index 000000000..2797ccea8 --- /dev/null +++ b/src/Testing/ShimsTests/MediatR/WolverineHandlers.cs @@ -0,0 +1,14 @@ +using Wolverine.Attributes; + +namespace Wolverine.Shims.Tests.MediatR; + +public static class CascadingMessageHandler +{ + public static string? ReceivedData { get; set; } + + [WolverineHandler] + public static void Handle(CascadingMessage message) + { + ReceivedData = message.Data; + } +} diff --git a/src/Testing/ShimsTests/MediatR/mediatr_shim_cascading_message_tests.cs b/src/Testing/ShimsTests/MediatR/mediatr_shim_cascading_message_tests.cs new file mode 100644 index 000000000..32cd23ef8 --- /dev/null +++ b/src/Testing/ShimsTests/MediatR/mediatr_shim_cascading_message_tests.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.Hosting; +using Shouldly; +using Xunit; +using Alba; + +namespace Wolverine.Shims.Tests.MediatR; + +public class mediatr_shim_cascading_message_tests +{ + private IHostBuilder _builder; + public mediatr_shim_cascading_message_tests() + { + _builder = Host.CreateDefaultBuilder(); + _builder.UseWolverine(opts => + { + opts.Discovery.IncludeAssembly(typeof(mediatr_shim_cascading_message_tests).Assembly); + opts.UseMediatRHandlers(); + }); + } + + [Fact] + public async Task mediatr_handler_can_return_cascading_message() + { + await using var host = await AlbaHost.For(_builder); + + // Reset static state + CascadingMessageHandler.ReceivedData = null; + + // Invoke the message and wait for cascading messages to be processed + await host.InvokeAsync(new CascadingMessage("cascade-test")); + + // Verify the cascading message was handled + CascadingMessageHandler.ReceivedData.ShouldBe("cascade-test"); + } +} diff --git a/src/Testing/ShimsTests/MediatR/mediatr_shim_handler_integration_tests.cs b/src/Testing/ShimsTests/MediatR/mediatr_shim_handler_integration_tests.cs new file mode 100644 index 000000000..9e6131a1a --- /dev/null +++ b/src/Testing/ShimsTests/MediatR/mediatr_shim_handler_integration_tests.cs @@ -0,0 +1,55 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Shouldly; +using Xunit; +using Alba; + +namespace Wolverine.Shims.Tests.MediatR; + +public class mediatr_shim_handler_integration_tests +{ + private IHostBuilder _builder; + public mediatr_shim_handler_integration_tests() + { + _builder = Host.CreateDefaultBuilder(); + _builder.UseWolverine(opts => + { + opts.Discovery.IncludeAssembly(typeof(mediatr_shim_cascading_message_tests).Assembly); + opts.Services.AddScoped(); + opts.UseMediatRHandlers(); + }); + } + + [Fact] + public async Task invoke_mediatr_handler_with_response() + { + await using var host = await AlbaHost.For(_builder); + + var response = await host.MessageBus().InvokeAsync( + new RequestWithResponse("test")); + + response.ShouldNotBeNull(); + response.Data.ShouldBe("passed: test"); + response.ProcessedBy.ShouldBe("MediatR"); + } + + [Fact] + public async Task invoke_mediatr_handler_without_response() + { + await using var host = await AlbaHost.For(_builder); + + // Should not throw + await host.InvokeAsync(new RequestWithoutResponse("test")); + } + + [Fact] + public async Task mediatr_handler_receives_dependencies_from_di() + { + await using var host = await AlbaHost.For(_builder); + + var response = await host.MessageBus().InvokeAsync( + new RequestAdditionFromService(1)); + + response.ShouldBe(2); + } +} diff --git a/src/Testing/ShimsTests/MediatR/mediatr_shim_response_tests.cs b/src/Testing/ShimsTests/MediatR/mediatr_shim_response_tests.cs new file mode 100644 index 000000000..501675d16 --- /dev/null +++ b/src/Testing/ShimsTests/MediatR/mediatr_shim_response_tests.cs @@ -0,0 +1,43 @@ +using Microsoft.Extensions.Hosting; +using Shouldly; +using Xunit; +using Alba; + +namespace Wolverine.Shims.Tests.MediatR; + +public class mediatr_shim_response_tests +{ + private IHostBuilder _builder; + public mediatr_shim_response_tests() + { + _builder = Host.CreateDefaultBuilder(); + _builder.UseWolverine(opts => + { + opts.Discovery.IncludeAssembly(typeof(mediatr_shim_cascading_message_tests).Assembly); + opts.UseMediatRHandlers(); + }); + } + + [Fact] + public async Task response_is_returned_from_invoke_async() + { + await using var host = await AlbaHost.For(_builder); + + var response = await host.MessageBus().InvokeAsync( + new RequestWithResponse("response-test")); + + response.ShouldNotBeNull(); + response.Data.ShouldBe("passed: response-test"); + } + + [Fact] + public async Task response_type_is_correct() + { + await using var host = await AlbaHost.For(_builder); + + var response = await host.MessageBus().InvokeAsync( + new RequestWithResponse("type-test")); + + response.ShouldBeOfType(); + } +} diff --git a/src/Testing/ShimsTests/ShimsTests.csproj b/src/Testing/ShimsTests/ShimsTests.csproj new file mode 100644 index 000000000..131baaf95 --- /dev/null +++ b/src/Testing/ShimsTests/ShimsTests.csproj @@ -0,0 +1,25 @@ + + + + false + net9.0 + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + diff --git a/src/Wolverine/Shims/MediatR/IRequest.cs b/src/Wolverine/Shims/MediatR/IRequest.cs new file mode 100644 index 000000000..07f58c977 --- /dev/null +++ b/src/Wolverine/Shims/MediatR/IRequest.cs @@ -0,0 +1,21 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Wolverine.Shims.MediatR; + +/// +/// Marker interface for requests that return a response. +/// This is a shim interface compatible with MediatR's IRequest<T>. +/// +/// The response type +public interface IRequest +{ +} + +/// +/// Marker interface for requests that do not return a response. +/// This is a shim interface compatible with MediatR's IRequest. +/// +public interface IRequest +{ +} diff --git a/src/Wolverine/Shims/MediatR/IRequestHandler.cs b/src/Wolverine/Shims/MediatR/IRequestHandler.cs new file mode 100644 index 000000000..1a1f759b1 --- /dev/null +++ b/src/Wolverine/Shims/MediatR/IRequestHandler.cs @@ -0,0 +1,39 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Wolverine.Shims.MediatR; + +/// +/// Handler for requests that return a response. +/// This is a shim interface compatible with MediatR's IRequestHandler<TRequest, TResponse>. +/// Handlers implementing this interface will be automatically discovered by Wolverine. +/// +/// The request type +/// The response type +public interface IRequestHandler where TRequest : IRequest +{ + /// + /// Handles a request and returns a response + /// + /// The request + /// Cancellation token + /// Response from the request + Task Handle(TRequest request, CancellationToken cancellationToken); +} + +/// +/// Handler for requests that do not return a response. +/// This is a shim interface compatible with MediatR's IRequestHandler<TRequest>. +/// Handlers implementing this interface will be automatically discovered by Wolverine. +/// +/// The request type +public interface IRequestHandler where TRequest : IRequest +{ + /// + /// Handles a request without returning a response + /// + /// The request + /// Cancellation token + /// A task representing the completion of the request + Task Handle(TRequest request, CancellationToken cancellationToken); +} diff --git a/src/Wolverine/Shims/MediatR/WolverineOptionsExtensions.cs b/src/Wolverine/Shims/MediatR/WolverineOptionsExtensions.cs new file mode 100644 index 000000000..eec295577 --- /dev/null +++ b/src/Wolverine/Shims/MediatR/WolverineOptionsExtensions.cs @@ -0,0 +1,25 @@ +using Wolverine.Shims.MediatR; + +namespace Wolverine.Shims; + +/// +/// Extension methods for using MediatR-style handlers in Wolverine +/// +public static class WolverineOptionsExtensions +{ + /// + /// Enables discovery of handlers implementing Wolverine's MediatR shim interfaces + /// (IRequestHandler<TRequest, TResponse> and IRequestHandler<TRequest>). + /// This allows you to use MediatR-style handler patterns without depending on the MediatR library. + /// + public static WolverineOptions UseMediatRHandlers(this WolverineOptions options) + { + options.Discovery.CustomizeHandlerDiscovery(query => + { + query.Includes.Implements(typeof(IRequestHandler<>)); + query.Includes.Implements(typeof(IRequestHandler<,>)); + }); + + return options; + } +} diff --git a/wolverine.sln b/wolverine.sln index 2a427d4b6..2d96d04fc 100644 --- a/wolverine.sln +++ b/wolverine.sln @@ -359,6 +359,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CosmosDbTests", "src\Persis EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EfCoreTests.MultiTenancy", "src\Persistence\EfCoreTests.MultiTenancy\EfCoreTests.MultiTenancy.csproj", "{ACB9EEA0-A545-4D02-A040-B1AE3CEF83ED}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShimsTests", "src\Testing\ShimsTests\ShimsTests.csproj", "{077DF0C6-C8C0-4C93-A595-1E2015639E3F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1989,6 +1991,18 @@ Global {E0D51CAE-97CF-48A8-879E-149A4E69BEE2}.Release|x64.Build.0 = Release|Any CPU {E0D51CAE-97CF-48A8-879E-149A4E69BEE2}.Release|x86.ActiveCfg = Release|Any CPU {E0D51CAE-97CF-48A8-879E-149A4E69BEE2}.Release|x86.Build.0 = Release|Any CPU + {077DF0C6-C8C0-4C93-A595-1E2015639E3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {077DF0C6-C8C0-4C93-A595-1E2015639E3F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {077DF0C6-C8C0-4C93-A595-1E2015639E3F}.Debug|x64.ActiveCfg = Debug|Any CPU + {077DF0C6-C8C0-4C93-A595-1E2015639E3F}.Debug|x64.Build.0 = Debug|Any CPU + {077DF0C6-C8C0-4C93-A595-1E2015639E3F}.Debug|x86.ActiveCfg = Debug|Any CPU + {077DF0C6-C8C0-4C93-A595-1E2015639E3F}.Debug|x86.Build.0 = Debug|Any CPU + {077DF0C6-C8C0-4C93-A595-1E2015639E3F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {077DF0C6-C8C0-4C93-A595-1E2015639E3F}.Release|Any CPU.Build.0 = Release|Any CPU + {077DF0C6-C8C0-4C93-A595-1E2015639E3F}.Release|x64.ActiveCfg = Release|Any CPU + {077DF0C6-C8C0-4C93-A595-1E2015639E3F}.Release|x64.Build.0 = Release|Any CPU + {077DF0C6-C8C0-4C93-A595-1E2015639E3F}.Release|x86.ActiveCfg = Release|Any CPU + {077DF0C6-C8C0-4C93-A595-1E2015639E3F}.Release|x86.Build.0 = Release|Any CPU {ACB9EEA0-A545-4D02-A040-B1AE3CEF83ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {ACB9EEA0-A545-4D02-A040-B1AE3CEF83ED}.Debug|Any CPU.Build.0 = Debug|Any CPU {ACB9EEA0-A545-4D02-A040-B1AE3CEF83ED}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -2166,6 +2180,7 @@ Global {9DBA7EBE-C6E6-4F26-87E8-D87A6CDDE737} = {68B94BE1-185D-D133-8A8C-EFE0C95F2BC7} {E0D51CAE-97CF-48A8-879E-149A4E69BEE2} = {68B94BE1-185D-D133-8A8C-EFE0C95F2BC7} {ACB9EEA0-A545-4D02-A040-B1AE3CEF83ED} = {7A9E0EAE-9ABF-40F6-9DB9-8FB1243F4210} + {077DF0C6-C8C0-4C93-A595-1E2015639E3F} = {96119B5E-B5F0-400A-9580-B342EBE26212} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {30422362-0D90-4DBE-8C97-DD2B5B962768}