diff --git a/Backi.sln b/Backi.sln index 5b52df7..cc289aa 100644 --- a/Backi.sln +++ b/Backi.sln @@ -21,6 +21,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dotnet", "dotnet", "{7F2FF8 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backi.Mediatr", "mediator\dotnet\lib\Backi.Mediatr.csproj", "{1AD779EA-2592-48D0-95BB-6E68FDB5276F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backi.Timediatr.Tests", "mediator\dotnet\Backi.Timediatr.Tests\Backi.Timediatr.Tests.csproj", "{C47EDCC3-FF07-4B1E-BDC4-06544C6264F5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backi.Mediatr.Tests", "mediator\dotnet\Backi.Mediatr.Tests\Backi.Mediatr.Tests.csproj", "{2243861D-99F1-427E-80A6-DDED312872D7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backi.Timediatr", "mediator\dotnet\Backi.Timediatr\Backi.Timediatr.csproj", "{28FE8A5E-60D5-43A9-8550-AF7A077B1B35}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -46,6 +52,18 @@ Global {1AD779EA-2592-48D0-95BB-6E68FDB5276F}.Debug|Any CPU.Build.0 = Debug|Any CPU {1AD779EA-2592-48D0-95BB-6E68FDB5276F}.Release|Any CPU.ActiveCfg = Release|Any CPU {1AD779EA-2592-48D0-95BB-6E68FDB5276F}.Release|Any CPU.Build.0 = Release|Any CPU + {C47EDCC3-FF07-4B1E-BDC4-06544C6264F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C47EDCC3-FF07-4B1E-BDC4-06544C6264F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C47EDCC3-FF07-4B1E-BDC4-06544C6264F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C47EDCC3-FF07-4B1E-BDC4-06544C6264F5}.Release|Any CPU.Build.0 = Release|Any CPU + {2243861D-99F1-427E-80A6-DDED312872D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2243861D-99F1-427E-80A6-DDED312872D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2243861D-99F1-427E-80A6-DDED312872D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2243861D-99F1-427E-80A6-DDED312872D7}.Release|Any CPU.Build.0 = Release|Any CPU + {28FE8A5E-60D5-43A9-8550-AF7A077B1B35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28FE8A5E-60D5-43A9-8550-AF7A077B1B35}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28FE8A5E-60D5-43A9-8550-AF7A077B1B35}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28FE8A5E-60D5-43A9-8550-AF7A077B1B35}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {ADCEE902-52AF-4E52-9CEA-77450049AA7C} = {BBB56B6A-667F-43C0-8AFE-79530301D381} @@ -55,5 +73,8 @@ Global {F9FA4AC6-6E34-4D32-8C9D-573B2321BDE7} = {ADCEE902-52AF-4E52-9CEA-77450049AA7C} {7F2FF828-62E7-497D-8D5C-F0F4A1540AD1} = {27BF9028-A9AC-4B9E-9A63-5260B70734D0} {1AD779EA-2592-48D0-95BB-6E68FDB5276F} = {7F2FF828-62E7-497D-8D5C-F0F4A1540AD1} + {C47EDCC3-FF07-4B1E-BDC4-06544C6264F5} = {7F2FF828-62E7-497D-8D5C-F0F4A1540AD1} + {2243861D-99F1-427E-80A6-DDED312872D7} = {7F2FF828-62E7-497D-8D5C-F0F4A1540AD1} + {28FE8A5E-60D5-43A9-8550-AF7A077B1B35} = {7F2FF828-62E7-497D-8D5C-F0F4A1540AD1} EndGlobalSection EndGlobal diff --git a/Backi.sln.DotSettings b/Backi.sln.DotSettings new file mode 100644 index 0000000..8dac555 --- /dev/null +++ b/Backi.sln.DotSettings @@ -0,0 +1,3 @@ + + True + True \ No newline at end of file diff --git a/Backi.sln.DotSettings.user b/Backi.sln.DotSettings.user new file mode 100644 index 0000000..ecd39c4 --- /dev/null +++ b/Backi.sln.DotSettings.user @@ -0,0 +1,7 @@ + + <SessionState ContinuousTestingMode="0" IsActive="True" Name="CreateHandlerEachTime" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <TestAncestor> + <TestId>MSTest::2243861D-99F1-427E-80A6-DDED312872D7::net8.0::Backi.Tests.SendMediatrRequestShould.CreateHandlerEachTime</TestId> + <TestId>MSTest::C47EDCC3-FF07-4B1E-BDC4-06544C6264F5::net8.0::Backi.Tests.TimediatrShould.SendCommandOneFewTimes</TestId> + </TestAncestor> +</SessionState> \ No newline at end of file diff --git a/mediator/dotnet/Backi.Mediatr.Tests/Backi.Mediatr.Tests.csproj b/mediator/dotnet/Backi.Mediatr.Tests/Backi.Mediatr.Tests.csproj new file mode 100644 index 0000000..563bb64 --- /dev/null +++ b/mediator/dotnet/Backi.Mediatr.Tests/Backi.Mediatr.Tests.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + diff --git a/mediator/dotnet/Backi.Mediatr.Tests/GlobalUsings.cs b/mediator/dotnet/Backi.Mediatr.Tests/GlobalUsings.cs new file mode 100644 index 0000000..ab67c7e --- /dev/null +++ b/mediator/dotnet/Backi.Mediatr.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git a/mediator/dotnet/Backi.Mediatr.Tests/Infrastructure.cs b/mediator/dotnet/Backi.Mediatr.Tests/Infrastructure.cs new file mode 100644 index 0000000..80c6bde --- /dev/null +++ b/mediator/dotnet/Backi.Mediatr.Tests/Infrastructure.cs @@ -0,0 +1,59 @@ +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Backi.Tests; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddMediatrTestingInfrastructure(this IServiceCollection services) + { + services.AddSingleton(); + services.AddMediatR(m => m.RegisterServicesFromAssembly(typeof(CounterCollection).Assembly)); + services.AddLogging(l => + { + l.AddSimpleConsole(c => c.SingleLine = true); + l.SetMinimumLevel(LogLevel.Debug); + }); + + return services; + } +} + +public class CounterCollection +{ + public readonly Dictionary items = new (); + + public void Increment(string key) + { + if (!items.TryAdd(key, 1)) + { + items[key] += 1; + } + } + + public int Get(string key) => items.GetValueOrDefault(key); +} + +public class CommandOne : IRequest +{ + public class Handler : IRequestHandler + { + readonly CounterCollection counters; + public const string ConstructorCounterKey = "CommandA.Handler.Constructor"; + public const string HandleCounterKey = "CommandA.Handler.Handle"; + + public Handler(CounterCollection counters) + { + this.counters = counters; + + counters.Increment(ConstructorCounterKey); + } + + public Task Handle(CommandOne request, CancellationToken cancellationToken) + { + counters.Increment(HandleCounterKey); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/mediator/dotnet/Backi.Mediatr.Tests/SendMediatrRequestShould.cs b/mediator/dotnet/Backi.Mediatr.Tests/SendMediatrRequestShould.cs new file mode 100644 index 0000000..abda12f --- /dev/null +++ b/mediator/dotnet/Backi.Mediatr.Tests/SendMediatrRequestShould.cs @@ -0,0 +1,32 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Backi.Tests; + +[TestClass] +public class SendMediatrRequestShould +{ + [TestMethod] + public async Task CreateHandlerEachTime() + { + var services = new ServiceCollection(); + + services.AddMediatrTestingInfrastructure(); + + var provider = services.BuildServiceProvider(); + + var scopeFactory = provider.GetRequiredService(); + var logger = provider.GetRequiredService>(); + + var commandInstance = new CommandOne(); + + await scopeFactory.SendMediatorRequest(commandInstance, logger); + await scopeFactory.SendMediatorRequest(commandInstance, logger); + + var counters = provider.GetRequiredService(); + + counters.Get(CommandOne.Handler.ConstructorCounterKey).Should().Be(2); + counters.Get(CommandOne.Handler.HandleCounterKey).Should().Be(2); + } +} \ No newline at end of file diff --git a/mediator/dotnet/Backi.Timediatr.Tests/Backi.Timediatr.Tests.csproj b/mediator/dotnet/Backi.Timediatr.Tests/Backi.Timediatr.Tests.csproj new file mode 100644 index 0000000..3bc88f3 --- /dev/null +++ b/mediator/dotnet/Backi.Timediatr.Tests/Backi.Timediatr.Tests.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + diff --git a/mediator/dotnet/Backi.Timediatr.Tests/GlobalUsings.cs b/mediator/dotnet/Backi.Timediatr.Tests/GlobalUsings.cs new file mode 100644 index 0000000..ab67c7e --- /dev/null +++ b/mediator/dotnet/Backi.Timediatr.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git a/mediator/dotnet/Backi.Timediatr.Tests/TimediatrShould.cs b/mediator/dotnet/Backi.Timediatr.Tests/TimediatrShould.cs new file mode 100644 index 0000000..38cd55e --- /dev/null +++ b/mediator/dotnet/Backi.Timediatr.Tests/TimediatrShould.cs @@ -0,0 +1,34 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Backi.Tests; + +[TestClass] +public class TimediatrShould +{ + [TestMethod] + public async Task SendCommandOneFewTimes() + { + var services = new ServiceCollection(); + + services.AddMediatrTestingInfrastructure(); + services.AddTimediatr(timediatr => timediatr.Configure(o => + { + o.Schedule.Add(new CommandOne(), TimeSpan.FromSeconds(3)); + })); + + var provider = services.BuildServiceProvider(); + + var backgroundService = (TimediatrBackgroundService)provider.GetRequiredService(); + + await backgroundService.StartAsync(CancellationToken.None); + await Task.Delay(TimeSpan.FromSeconds(10)); + await backgroundService.StopAsync(CancellationToken.None); + + var counter = provider.GetRequiredService(); + + counter.Get(CommandOne.Handler.HandleCounterKey).Should().Be(4); + counter.Get(CommandOne.Handler.ConstructorCounterKey).Should().Be(4); + } +} \ No newline at end of file diff --git a/mediator/dotnet/Backi.Timediatr/Backi.Timediatr.csproj b/mediator/dotnet/Backi.Timediatr/Backi.Timediatr.csproj new file mode 100644 index 0000000..9114ff2 --- /dev/null +++ b/mediator/dotnet/Backi.Timediatr/Backi.Timediatr.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/mediator/dotnet/Backi.Timediatr/Timediatr.cs b/mediator/dotnet/Backi.Timediatr/Timediatr.cs new file mode 100644 index 0000000..36fc8db --- /dev/null +++ b/mediator/dotnet/Backi.Timediatr/Timediatr.cs @@ -0,0 +1,65 @@ +using Backi.Timers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Backi; + +public class TimediatrBackgroundService( + IOptions options, + IServiceScopeFactory serviceScopeFactory, + ILogger logger) : IHostedService +{ + readonly List timers = []; + + public Task StartAsync(CancellationToken cancellationToken) + { + var schedule = options.Value.Schedule; + + foreach (var timer in schedule) + { + logger.LogDebug("Setting {requestType} to run with interval {interval}", timer.Key.GetType(), timer.Value); + + timers.Add(SafeTimer.RunNowAndPeriodically( + timer.Value, + () => serviceScopeFactory.SendMediatorRequest(timer.Key, logger, cancellationToken), + ex => logger.LogError(ex, "Error while processing {requestType}", timer.Key.GetType()) + )); + + logger.LogInformation("Set {requestType} to run with interval {interval}", timer.Key.GetType(), timer.Value); + } + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + logger.LogDebug("Stopping all timers"); + + foreach (var timer in timers) + { + timer.Stop(); + } + + logger.LogInformation("Stopped all timers"); + return Task.CompletedTask; + } +} + +public class TimediatrConfiguration +{ + public Dictionary Schedule { get; set; } = new(); +} + +public static class TimediatrServiceCollectionExtensions +{ + public static IServiceCollection AddTimediatr(this IServiceCollection services, Action> configuration) + { + services.AddHostedService(); + var optionsBuilder = services.AddOptions(); + configuration(optionsBuilder); + + return services; + } +} \ No newline at end of file diff --git a/mediator/dotnet/lib/ServiceScopeExtensions.cs b/mediator/dotnet/lib/ServiceScopeExtensions.cs index 05f2ca4..e45e2cf 100644 --- a/mediator/dotnet/lib/ServiceScopeExtensions.cs +++ b/mediator/dotnet/lib/ServiceScopeExtensions.cs @@ -16,4 +16,13 @@ public static async Task SendMediatorRequest(this IServiceScopeFactory await mediator.Send(request, cancellationToken ?? CancellationToken.None); logger?.LogInformation("{requestType} processed by mediator", typeof(TRequest)); } + + public static async Task SendMediatorRequest(this IServiceScopeFactory serviceScopeFactory, object request, ILogger? logger = null, CancellationToken? cancellationToken = null) + { + logger?.LogDebug("Sending {requestType} to mediator in a new scope", request.GetType()); + using var scope = serviceScopeFactory.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + await mediator.Send(request, cancellationToken ?? CancellationToken.None); + logger?.LogInformation("{requestType} processed by mediator", request.GetType()); + } }