From 7dd319e8a55f31347c95eb06b435b7a956967c60 Mon Sep 17 00:00:00 2001 From: mkyrylo Date: Mon, 9 Feb 2026 12:25:06 +0200 Subject: [PATCH 1/2] Add Mediation Support - Add IMediator, IRequest, IRequestHandler, IPipelineBehavior interfaces and respective implementations - Add DI method --- README.md | 35 +++++++++++++++++++ ...Bss.Platform.Mediation.Abstractions.csproj | 9 +++++ .../IMediator.cs | 7 ++++ .../IPipelineBehavior.cs | 9 +++++ .../IRequest.cs | 3 ++ .../IRequestHandler.cs | 7 ++++ .../Bss.Platform.Mediation.csproj | 18 ++++++++++ .../DependencyInjection.cs | 28 +++++++++++++++ src/Bss.Platform.Mediation/Mediator.cs | 28 +++++++++++++++ src/Bss.Platform.sln | 16 +++++++++ src/Directory.Packages.props | 2 ++ src/__SolutionItems/CommonAssemblyInfo.cs | 8 ++--- 12 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 src/Bss.Platform.Mediation.Abstractions/Bss.Platform.Mediation.Abstractions.csproj create mode 100644 src/Bss.Platform.Mediation.Abstractions/IMediator.cs create mode 100644 src/Bss.Platform.Mediation.Abstractions/IPipelineBehavior.cs create mode 100644 src/Bss.Platform.Mediation.Abstractions/IRequest.cs create mode 100644 src/Bss.Platform.Mediation.Abstractions/IRequestHandler.cs create mode 100644 src/Bss.Platform.Mediation/Bss.Platform.Mediation.csproj create mode 100644 src/Bss.Platform.Mediation/DependencyInjection.cs create mode 100644 src/Bss.Platform.Mediation/Mediator.cs diff --git a/README.md b/README.md index e7b9402..b105c0d 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ This repository offers a wide collection of .NET packages for use in microservic - [NHibernate](#NHibernate) - [Notifications](#Notifications) - [Notifications Audit](#Notifications-Audit) +- [Mediation](#Custom-Mediation) ## RabbitMQ @@ -531,3 +532,37 @@ services Thats all - db schema and tables will be generated on application start (you can customize schema and table names on DI step). + +# Custom Mediation + +## Purpose +A small in-process mediator. Replaces MediatR-style usage with a minimal API surface. + +Exposed types: +- `IMediator` +- `IRequest` +- `IRequestHandler` +- `IPipelineBehavior` +- One DI entry point: `AddMediation(...)` + +Goal: presentation layer only does `await mediator.Send(request)` and never touches application logic. +## DI registration + +### Add mediation once (composition root) +Call `AddMediation` . Pass assemblies that contain handlers and pipeline behaviors. + +```C# +builder.Services.AddMediation(typeof(DependencyInjection).Assembly); +``` +### Pipeline behaviors + +Register pipeline behaviors as open generics. Order matters. + +```C# +// Outer → inner +builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ExceptionMappingBehavior<,>)); +builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); +builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); +builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(TransactionBehavior<,>)); +``` +~~~~ \ No newline at end of file diff --git a/src/Bss.Platform.Mediation.Abstractions/Bss.Platform.Mediation.Abstractions.csproj b/src/Bss.Platform.Mediation.Abstractions/Bss.Platform.Mediation.Abstractions.csproj new file mode 100644 index 0000000..17b910f --- /dev/null +++ b/src/Bss.Platform.Mediation.Abstractions/Bss.Platform.Mediation.Abstractions.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/src/Bss.Platform.Mediation.Abstractions/IMediator.cs b/src/Bss.Platform.Mediation.Abstractions/IMediator.cs new file mode 100644 index 0000000..0f669c2 --- /dev/null +++ b/src/Bss.Platform.Mediation.Abstractions/IMediator.cs @@ -0,0 +1,7 @@ +namespace Bss.Platform.Mediation.Abstractions; + +public interface IMediator +{ + Task Send(TRequest request, CancellationToken cancellationToken = default) + where TRequest : IRequest; +} diff --git a/src/Bss.Platform.Mediation.Abstractions/IPipelineBehavior.cs b/src/Bss.Platform.Mediation.Abstractions/IPipelineBehavior.cs new file mode 100644 index 0000000..73e7b47 --- /dev/null +++ b/src/Bss.Platform.Mediation.Abstractions/IPipelineBehavior.cs @@ -0,0 +1,9 @@ +namespace Bss.Platform.Mediation.Abstractions; + +public interface IPipelineBehavior +{ + Task Handle( + TRequest request, + CancellationToken ct, + Func> next); +} diff --git a/src/Bss.Platform.Mediation.Abstractions/IRequest.cs b/src/Bss.Platform.Mediation.Abstractions/IRequest.cs new file mode 100644 index 0000000..156473a --- /dev/null +++ b/src/Bss.Platform.Mediation.Abstractions/IRequest.cs @@ -0,0 +1,3 @@ +namespace Bss.Platform.Mediation.Abstractions; + +public interface IRequest; diff --git a/src/Bss.Platform.Mediation.Abstractions/IRequestHandler.cs b/src/Bss.Platform.Mediation.Abstractions/IRequestHandler.cs new file mode 100644 index 0000000..a3ef973 --- /dev/null +++ b/src/Bss.Platform.Mediation.Abstractions/IRequestHandler.cs @@ -0,0 +1,7 @@ +namespace Bss.Platform.Mediation.Abstractions; + +public interface IRequestHandler + where TRequest : IRequest +{ + Task Handle(TRequest request, CancellationToken cancellationToken); +} diff --git a/src/Bss.Platform.Mediation/Bss.Platform.Mediation.csproj b/src/Bss.Platform.Mediation/Bss.Platform.Mediation.csproj new file mode 100644 index 0000000..53c0bbe --- /dev/null +++ b/src/Bss.Platform.Mediation/Bss.Platform.Mediation.csproj @@ -0,0 +1,18 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + diff --git a/src/Bss.Platform.Mediation/DependencyInjection.cs b/src/Bss.Platform.Mediation/DependencyInjection.cs new file mode 100644 index 0000000..730baec --- /dev/null +++ b/src/Bss.Platform.Mediation/DependencyInjection.cs @@ -0,0 +1,28 @@ +using System.Reflection; + +using Bss.Platform.Mediation.Abstractions; + +using Microsoft.Extensions.DependencyInjection; + +namespace Bss.Platform.Mediation; + +public static class DependencyInjection +{ + public static IServiceCollection AddMediation( + this IServiceCollection services, + params Assembly[] assemblies) + { + services.AddScoped(); + + services.Scan(s => s + .FromAssemblies(assemblies) + .AddClasses(c => c.AssignableTo(typeof(IRequestHandler<,>))) + .AsImplementedInterfaces() + .WithScopedLifetime() + .AddClasses(c => c.AssignableTo(typeof(IPipelineBehavior<,>))) + .AsImplementedInterfaces() + .WithScopedLifetime()); + + return services; + } +} diff --git a/src/Bss.Platform.Mediation/Mediator.cs b/src/Bss.Platform.Mediation/Mediator.cs new file mode 100644 index 0000000..255f16d --- /dev/null +++ b/src/Bss.Platform.Mediation/Mediator.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.DependencyInjection; + +using Bss.Platform.Mediation.Abstractions; + +namespace Bss.Platform.Mediation; + +public record Mediator(IServiceProvider ServiceProvider) : IMediator +{ + public Task Send(TRequest request, CancellationToken cancellationToken = default) + where TRequest : IRequest + { + var handler = this.ServiceProvider.GetRequiredService>(); + var behaviors = this.ServiceProvider + .GetServices>() + .Reverse() + .ToArray(); + + Func> next = (r, ct) + => handler.Handle(r, ct); + foreach (var behavior in behaviors) + { + var prev = next; + next = (r, ct) => behavior.Handle(r, ct, prev); + } + + return next(request, cancellationToken); + } +} diff --git a/src/Bss.Platform.sln b/src/Bss.Platform.sln index 1d04266..a69001f 100644 --- a/src/Bss.Platform.sln +++ b/src/Bss.Platform.sln @@ -42,6 +42,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bss.Platform.Notifications. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bss.Platform.RabbitMq.JsonSchemaGenerator", "Bss.Platform.RabbitMq.JsonSchemaGenerator\Bss.Platform.RabbitMq.JsonSchemaGenerator.csproj", "{0FC70E24-62A9-44BF-A433-804C98514A09}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mediation", "Mediation", "{40F06493-58CE-4E9D-BBB4-771BD26A9658}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bss.Platform.Mediation", "Bss.Platform.Mediation\Bss.Platform.Mediation.csproj", "{393C3A3D-E55F-4006-81A7-ED1CB3FAC1FF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bss.Platform.Mediation.Abstractions", "Bss.Platform.Mediation.Abstractions\Bss.Platform.Mediation.Abstractions.csproj", "{9EE0DF52-2C67-4DD1-AA9D-64ABC5BD00D7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -61,6 +67,8 @@ Global {A97421A9-58B7-4948-8231-2028B5D11F36} = {94640F4D-8BC6-407F-835E-73B454972CAD} {03CD2FDC-2BF1-497D-BF18-F15170C0AD85} = {94640F4D-8BC6-407F-835E-73B454972CAD} {0FC70E24-62A9-44BF-A433-804C98514A09} = {0F54786D-AB46-46AC-88DF-BEB789A62C1F} + {393C3A3D-E55F-4006-81A7-ED1CB3FAC1FF} = {40F06493-58CE-4E9D-BBB4-771BD26A9658} + {9EE0DF52-2C67-4DD1-AA9D-64ABC5BD00D7} = {40F06493-58CE-4E9D-BBB4-771BD26A9658} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {860BFBD8-26EA-44F9-980E-21B828FC8F72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -115,5 +123,13 @@ Global {0FC70E24-62A9-44BF-A433-804C98514A09}.Debug|Any CPU.Build.0 = Debug|Any CPU {0FC70E24-62A9-44BF-A433-804C98514A09}.Release|Any CPU.ActiveCfg = Release|Any CPU {0FC70E24-62A9-44BF-A433-804C98514A09}.Release|Any CPU.Build.0 = Release|Any CPU + {393C3A3D-E55F-4006-81A7-ED1CB3FAC1FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {393C3A3D-E55F-4006-81A7-ED1CB3FAC1FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {393C3A3D-E55F-4006-81A7-ED1CB3FAC1FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {393C3A3D-E55F-4006-81A7-ED1CB3FAC1FF}.Release|Any CPU.Build.0 = Release|Any CPU + {9EE0DF52-2C67-4DD1-AA9D-64ABC5BD00D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9EE0DF52-2C67-4DD1-AA9D-64ABC5BD00D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9EE0DF52-2C67-4DD1-AA9D-64ABC5BD00D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9EE0DF52-2C67-4DD1-AA9D-64ABC5BD00D7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index e8edfdb..d7e8a96 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -8,6 +8,7 @@ + @@ -24,6 +25,7 @@ + diff --git a/src/__SolutionItems/CommonAssemblyInfo.cs b/src/__SolutionItems/CommonAssemblyInfo.cs index d0adb52..97eff57 100644 --- a/src/__SolutionItems/CommonAssemblyInfo.cs +++ b/src/__SolutionItems/CommonAssemblyInfo.cs @@ -2,11 +2,11 @@ [assembly: AssemblyProduct("BSS Platform Services")] [assembly: AssemblyCompany("Luxoft")] -[assembly: AssemblyCopyright("Copyright © Luxoft 2025")] +[assembly: AssemblyCopyright("Copyright © Luxoft 2026")] -[assembly: AssemblyVersion("1.6.1.0")] -[assembly: AssemblyFileVersion("1.6.1.0")] -[assembly: AssemblyInformationalVersion("1.6.1.0")] +[assembly: AssemblyVersion("1.6.2.0")] +[assembly: AssemblyFileVersion("1.6.2.0")] +[assembly: AssemblyInformationalVersion("1.6.2.0")] #if DEBUG [assembly: AssemblyConfiguration("Debug")] From 7481cf861db2d7eb67158dbdd834a6b76e362011 Mon Sep 17 00:00:00 2001 From: mkyrylo Date: Thu, 19 Feb 2026 10:01:01 +0200 Subject: [PATCH 2/2] Add empty IRequest support --- .../IMediator.cs | 3 ++ .../IPipelineBehavior.cs | 8 +++++ .../IRequest.cs | 1 + .../IRequestHandler.cs | 6 ++++ src/Bss.Platform.Mediation/Mediator.cs | 31 +++++++++++++++---- 5 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/Bss.Platform.Mediation.Abstractions/IMediator.cs b/src/Bss.Platform.Mediation.Abstractions/IMediator.cs index 0f669c2..e7029b1 100644 --- a/src/Bss.Platform.Mediation.Abstractions/IMediator.cs +++ b/src/Bss.Platform.Mediation.Abstractions/IMediator.cs @@ -4,4 +4,7 @@ public interface IMediator { Task Send(TRequest request, CancellationToken cancellationToken = default) where TRequest : IRequest; + + Task Send(TRequest request, CancellationToken cancellationToken = default) + where TRequest : IRequest; } diff --git a/src/Bss.Platform.Mediation.Abstractions/IPipelineBehavior.cs b/src/Bss.Platform.Mediation.Abstractions/IPipelineBehavior.cs index 73e7b47..f8b3510 100644 --- a/src/Bss.Platform.Mediation.Abstractions/IPipelineBehavior.cs +++ b/src/Bss.Platform.Mediation.Abstractions/IPipelineBehavior.cs @@ -7,3 +7,11 @@ Task Handle( CancellationToken ct, Func> next); } + +public interface IPipelineBehavior +{ + Task Handle( + TRequest request, + CancellationToken ct, + Func next); +} diff --git a/src/Bss.Platform.Mediation.Abstractions/IRequest.cs b/src/Bss.Platform.Mediation.Abstractions/IRequest.cs index 156473a..e79a62d 100644 --- a/src/Bss.Platform.Mediation.Abstractions/IRequest.cs +++ b/src/Bss.Platform.Mediation.Abstractions/IRequest.cs @@ -1,3 +1,4 @@ namespace Bss.Platform.Mediation.Abstractions; public interface IRequest; +public interface IRequest; diff --git a/src/Bss.Platform.Mediation.Abstractions/IRequestHandler.cs b/src/Bss.Platform.Mediation.Abstractions/IRequestHandler.cs index a3ef973..5a46a00 100644 --- a/src/Bss.Platform.Mediation.Abstractions/IRequestHandler.cs +++ b/src/Bss.Platform.Mediation.Abstractions/IRequestHandler.cs @@ -5,3 +5,9 @@ public interface IRequestHandler { Task Handle(TRequest request, CancellationToken cancellationToken); } + +public interface IRequestHandler + where TRequest : IRequest +{ + Task Handle(TRequest request, CancellationToken cancellationToken); +} diff --git a/src/Bss.Platform.Mediation/Mediator.cs b/src/Bss.Platform.Mediation/Mediator.cs index 255f16d..3af73e7 100644 --- a/src/Bss.Platform.Mediation/Mediator.cs +++ b/src/Bss.Platform.Mediation/Mediator.cs @@ -10,13 +10,10 @@ public Task Send(TRequest request, CancellationToken where TRequest : IRequest { var handler = this.ServiceProvider.GetRequiredService>(); - var behaviors = this.ServiceProvider - .GetServices>() - .Reverse() - .ToArray(); + var behaviors = this.GetBehaviors>(); - Func> next = (r, ct) - => handler.Handle(r, ct); + Func> next = + (r, ct) => handler.Handle(r, ct); foreach (var behavior in behaviors) { var prev = next; @@ -25,4 +22,26 @@ public Task Send(TRequest request, CancellationToken return next(request, cancellationToken); } + + public Task Send(TRequest request, CancellationToken cancellationToken = default) + where TRequest : IRequest + { + var handler = this.ServiceProvider.GetRequiredService>(); + var behaviors = this.GetBehaviors>(); + + Func next = (r, ct) => handler.Handle(r, ct); + foreach (var behavior in behaviors) + { + var prev = next; + next = (r, ct) => behavior.Handle(r, ct, prev); + } + + return next(request, cancellationToken); + } + + private TInterface[] GetBehaviors() => + this.ServiceProvider + .GetServices() + .Reverse() + .ToArray(); }