diff --git a/Cross.CQRS.EF/Behaviors/ScopeBehavior.cs b/Cross.CQRS.EF/Behaviors/ScopeBehavior.cs new file mode 100644 index 0000000..c4ca7f2 --- /dev/null +++ b/Cross.CQRS.EF/Behaviors/ScopeBehavior.cs @@ -0,0 +1,58 @@ +namespace Cross.CQRS.EF.Behaviors; + +internal sealed class ScopeBehavior : IPipelineBehavior + where TRequest : class, IRequest +{ + private readonly IDbContextProvider _dbContextProvider; + private readonly IHandlerLocator _handlerLocator; + + public ScopeBehavior(IHandlerLocator handlerLocator, IDbContextProvider dbContextProvider) + { + _handlerLocator = handlerLocator; + _dbContextProvider = dbContextProvider; + } + + /// + public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + if (request is not ICommand) + { + return await next(); + } + + var handler = _handlerLocator.FindHandlerTypeByRequest(typeof(TRequest)); + if (handler != null) + { + var isExplicitTransactionSet = handler + .GetCustomAttributes(typeof(ExplicitTransactionAttribute), inherit: false) + .Any(); + + if (isExplicitTransactionSet) + { + // Skip behavior if requested explicit transaction management. + return await next(); + } + } + + TResponse response = default; + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + response = await next(); + scope.Complete(); + } + + var dbContext = _dbContextProvider.Get(); + + // Clean-up tracked entries + var trackedEntries = dbContext.ChangeTracker.Entries() + .Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted) + .ToList(); + + foreach (var entry in trackedEntries) + { + entry.State = EntityState.Detached; + } + + return response; + } +} diff --git a/Cross.CQRS.EF/CqrsRegistrationSyntaxExtensions.cs b/Cross.CQRS.EF/CqrsRegistrationSyntaxExtensions.cs index 97d8c8c..f477bae 100644 --- a/Cross.CQRS.EF/CqrsRegistrationSyntaxExtensions.cs +++ b/Cross.CQRS.EF/CqrsRegistrationSyntaxExtensions.cs @@ -1,4 +1,6 @@ -namespace Cross.CQRS.EF; +using Cross.CQRS.EF.Enums; + +namespace Cross.CQRS.EF; public static class CqrsRegistrationSyntaxExtensions { @@ -7,13 +9,24 @@ public static class CqrsRegistrationSyntaxExtensions /// specified . /// /// + /// /// A reference to this instance after the operation has completed. - public static CqrsRegistrationSyntax AddEntityFrameworkIntegration(this CqrsRegistrationSyntax syntax) + public static CqrsRegistrationSyntax AddEntityFrameworkIntegration(this CqrsRegistrationSyntax syntax, TransactionBehaviorEnum transactionBehavior = TransactionBehaviorEnum.TransactionalBehavior) where TDbContext : DbContext { // Registration order is important, it works like ASP.NET Core middleware // Behaviors registered earlier will be executed earlier - syntax.Behaviors.AddBehavior(typeof(TransactionalBehavior<,>), order: 10); + switch (transactionBehavior) + { + case TransactionBehaviorEnum.TransactionalBehavior: + syntax.Behaviors.AddBehavior(typeof(TransactionalBehavior<,>), order: 10); + break; + case TransactionBehaviorEnum.ScopeBehavior: + syntax.Behaviors.AddBehavior(typeof(ScopeBehavior<,>), order: 10); + break; + default: + throw new ArgumentOutOfRangeException(nameof(transactionBehavior), transactionBehavior, null); + } // Filters syntax.Services.Scan(scan => diff --git a/Cross.CQRS.EF/Enums/TransactionBehaviorEnum.cs b/Cross.CQRS.EF/Enums/TransactionBehaviorEnum.cs new file mode 100644 index 0000000..52b4b89 --- /dev/null +++ b/Cross.CQRS.EF/Enums/TransactionBehaviorEnum.cs @@ -0,0 +1,7 @@ +namespace Cross.CQRS.EF.Enums; + +public enum TransactionBehaviorEnum +{ + TransactionalBehavior = 1, + ScopeBehavior = 2, +} diff --git a/Cross.CQRS.EF/GlobalUsings.cs b/Cross.CQRS.EF/GlobalUsings.cs index 08c6e8e..c786ef9 100644 --- a/Cross.CQRS.EF/GlobalUsings.cs +++ b/Cross.CQRS.EF/GlobalUsings.cs @@ -7,6 +7,7 @@ global using System.Linq.Expressions; global using System.Reflection; global using System.Threading; +global using System.Transactions; global using Cross.CQRS.Commands; global using Cross.CQRS.EF.Behaviors; global using Cross.CQRS.EF.Extensions; diff --git a/SampleWebApp/GlobalUsings.cs b/SampleWebApp/GlobalUsings.cs index e202341..2cdb1b0 100644 --- a/SampleWebApp/GlobalUsings.cs +++ b/SampleWebApp/GlobalUsings.cs @@ -4,13 +4,20 @@ global using System.Collections.Generic; global using System.Linq; global using System.Text; +global using System.Threading; global using System.Threading.Tasks; global using SampleWebApp.Infrastructure; global using Cross.CQRS; +global using Cross.CQRS.Commands; global using Cross.CQRS.EF; +global using Cross.CQRS.EF.Enums; +global using Cross.CQRS.Events; global using Cross.CQRS.Queries; global using FluentValidation; +global using MediatR; global using Microsoft.AspNetCore.Builder; +global using Microsoft.EntityFrameworkCore; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.DependencyInjection.Extensions; global using Microsoft.Extensions.Hosting; +global using SampleWebApp.Modules.Some.Handlers; diff --git a/SampleWebApp/Infrastructure/Context.cs b/SampleWebApp/Infrastructure/Context.cs index 6821423..3000982 100644 --- a/SampleWebApp/Infrastructure/Context.cs +++ b/SampleWebApp/Infrastructure/Context.cs @@ -1,5 +1,3 @@ -using Microsoft.EntityFrameworkCore; - namespace SampleWebApp.Infrastructure; public class Context : DbContext diff --git a/SampleWebApp/Modules/Some/Handlers/ExternalEvent.cs b/SampleWebApp/Modules/Some/Handlers/ExternalEvent.cs new file mode 100644 index 0000000..924b9e5 --- /dev/null +++ b/SampleWebApp/Modules/Some/Handlers/ExternalEvent.cs @@ -0,0 +1,3 @@ +namespace SampleWebApp.Modules.Some.Handlers; + +public record ExternalEvent(Guid CommandId, string Message) : IEvent; \ No newline at end of file diff --git a/SampleWebApp/Modules/Some/Handlers/ExternalEventHandler.cs b/SampleWebApp/Modules/Some/Handlers/ExternalEventHandler.cs new file mode 100644 index 0000000..cd98065 --- /dev/null +++ b/SampleWebApp/Modules/Some/Handlers/ExternalEventHandler.cs @@ -0,0 +1,11 @@ +namespace SampleWebApp.Modules.Some.Handlers; + +public class ExternalEventHandler : Cross.CQRS.Events.EventHandler +{ + protected override Task HandleAsync(ExternalEvent ev, CancellationToken cancellationToken) + { + Console.WriteLine($"{nameof(ExternalEventHandler)} with message '{ev.Message}'"); + + return Task.CompletedTask; + } +} diff --git a/SampleWebApp/Modules/Some/Handlers/InternalEvent.cs b/SampleWebApp/Modules/Some/Handlers/InternalEvent.cs new file mode 100644 index 0000000..b9c8978 --- /dev/null +++ b/SampleWebApp/Modules/Some/Handlers/InternalEvent.cs @@ -0,0 +1,3 @@ +namespace SampleWebApp.Modules.Some.Handlers; + +public record InternalEvent(Guid CommandId, string Message) : IEvent; \ No newline at end of file diff --git a/SampleWebApp/Modules/Some/Handlers/InternalEventHandler.cs b/SampleWebApp/Modules/Some/Handlers/InternalEventHandler.cs new file mode 100644 index 0000000..3ff46c6 --- /dev/null +++ b/SampleWebApp/Modules/Some/Handlers/InternalEventHandler.cs @@ -0,0 +1,11 @@ +namespace SampleWebApp.Modules.Some.Handlers; + +public class InternalEventHandler : Cross.CQRS.Events.EventHandler +{ + protected override Task HandleAsync(InternalEvent ev, CancellationToken cancellationToken) + { + Console.WriteLine($"{nameof(InternalEventHandler)} with message '{ev.Message}'"); + + return Task.CompletedTask; + } +} diff --git a/SampleWebApp/Modules/Some/Handlers/SomeScopeExternalCommand.cs b/SampleWebApp/Modules/Some/Handlers/SomeScopeExternalCommand.cs new file mode 100644 index 0000000..b878fb0 --- /dev/null +++ b/SampleWebApp/Modules/Some/Handlers/SomeScopeExternalCommand.cs @@ -0,0 +1,8 @@ +namespace SampleWebApp.Modules.Some.Handlers; + +public class SomeScopeExternalCommand : Command +{ + public SomeScopeExternalCommand() + { + } +} diff --git a/SampleWebApp/Modules/Some/Handlers/SomeScopeExternalCommandHandler.cs b/SampleWebApp/Modules/Some/Handlers/SomeScopeExternalCommandHandler.cs new file mode 100644 index 0000000..74eb329 --- /dev/null +++ b/SampleWebApp/Modules/Some/Handlers/SomeScopeExternalCommandHandler.cs @@ -0,0 +1,20 @@ +namespace SampleWebApp.Modules.Some.Handlers; + +public class SomeScopeExternalCommandHandler : CommandHandler +{ + + public SomeScopeExternalCommandHandler(IEventQueueWriter eventQueueWriter) + : base(eventQueueWriter) + { + } + + protected override Task HandleAsync(SomeScopeExternalCommand command, CancellationToken cancellationToken) + { + Events.Write(new ExternalEvent(command.CommandId, $"hello from {nameof(SomeScopeExternalCommandHandler)}")); + + Console.WriteLine($"{nameof(SomeScopeExternalCommandHandler)} do something"); + + // do nothing + return Task.CompletedTask; + } +} diff --git a/SampleWebApp/Modules/Some/Handlers/SomeScopeInternalCommand.cs b/SampleWebApp/Modules/Some/Handlers/SomeScopeInternalCommand.cs new file mode 100644 index 0000000..0b3ba78 --- /dev/null +++ b/SampleWebApp/Modules/Some/Handlers/SomeScopeInternalCommand.cs @@ -0,0 +1,10 @@ +namespace SampleWebApp.Modules.Some.Handlers; + + + +public class SomeScopeInternalCommand : Command +{ + public SomeScopeInternalCommand() + { + } +} diff --git a/SampleWebApp/Modules/Some/Handlers/SomeScopeInternalCommandHandler.cs b/SampleWebApp/Modules/Some/Handlers/SomeScopeInternalCommandHandler.cs new file mode 100644 index 0000000..e35e13e --- /dev/null +++ b/SampleWebApp/Modules/Some/Handlers/SomeScopeInternalCommandHandler.cs @@ -0,0 +1,20 @@ +namespace SampleWebApp.Modules.Some.Handlers; + +public class SomeScopeInternalCommandHandler : CommandHandler +{ + + public SomeScopeInternalCommandHandler(IEventQueueWriter eventQueueWriter) + : base(eventQueueWriter) + { + } + + protected override Task HandleAsync(SomeScopeExternalCommand command, CancellationToken cancellationToken) + { + Events.Write(new InternalEvent(command.CommandId, $"hello from {nameof(SomeScopeInternalCommandHandler)}")); + + Console.WriteLine($"{nameof(SomeScopeInternalCommandHandler)} do something"); + + // do nothing + return Task.CompletedTask; + } +} diff --git a/SampleWebApp/Program.cs b/SampleWebApp/Program.cs index aa8c4d3..f3eb53d 100644 --- a/SampleWebApp/Program.cs +++ b/SampleWebApp/Program.cs @@ -9,7 +9,11 @@ //MediatR builder.Services .AddCQRS(typeof(Program).Assembly) - .AddEntityFrameworkIntegration(); + .AddEntityFrameworkIntegration(TransactionBehaviorEnum.ScopeBehavior); + +// services.AddDbContext(options => options.UseSqlServer(...)); +// services.AddScoped, GenericRepository>(); +// services.AddScoped(); var app = builder.Build(); @@ -20,4 +24,11 @@ app.UseSwaggerUI(); } +app.MapGet("/somescope", (IMediator mediator ) => + { + var forecast = mediator.Send(new SomeScopeExternalCommand()); + return forecast; + }) + .WithName("RunSomeScope"); + app.Run();