Skip to content

Commit

Permalink
add new transaction behavior (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
denis-peshkov authored Aug 10, 2024
1 parent c94238e commit 4145ac2
Show file tree
Hide file tree
Showing 15 changed files with 187 additions and 6 deletions.
58 changes: 58 additions & 0 deletions Cross.CQRS.EF/Behaviors/ScopeBehavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
namespace Cross.CQRS.EF.Behaviors;

internal sealed class ScopeBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : class, IRequest<TResponse>
{
private readonly IDbContextProvider _dbContextProvider;
private readonly IHandlerLocator _handlerLocator;

public ScopeBehavior(IHandlerLocator handlerLocator, IDbContextProvider dbContextProvider)
{
_handlerLocator = handlerLocator;
_dbContextProvider = dbContextProvider;
}

/// <inheritdoc />
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
if (request is not ICommand<TResponse>)
{
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;
}
}
19 changes: 16 additions & 3 deletions Cross.CQRS.EF/CqrsRegistrationSyntaxExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Cross.CQRS.EF;
using Cross.CQRS.EF.Enums;

namespace Cross.CQRS.EF;

public static class CqrsRegistrationSyntaxExtensions
{
Expand All @@ -7,13 +9,24 @@ public static class CqrsRegistrationSyntaxExtensions
/// specified <see cref="IServiceCollection"/>.
/// </summary>
/// <param name="syntax"></param>
/// <param name="transactionBehavior"></param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static CqrsRegistrationSyntax AddEntityFrameworkIntegration<TDbContext>(this CqrsRegistrationSyntax syntax)
public static CqrsRegistrationSyntax AddEntityFrameworkIntegration<TDbContext>(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 =>
Expand Down
7 changes: 7 additions & 0 deletions Cross.CQRS.EF/Enums/TransactionBehaviorEnum.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Cross.CQRS.EF.Enums;

public enum TransactionBehaviorEnum
{
TransactionalBehavior = 1,
ScopeBehavior = 2,
}
1 change: 1 addition & 0 deletions Cross.CQRS.EF/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions SampleWebApp/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
2 changes: 0 additions & 2 deletions SampleWebApp/Infrastructure/Context.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using Microsoft.EntityFrameworkCore;

namespace SampleWebApp.Infrastructure;

public class Context : DbContext
Expand Down
3 changes: 3 additions & 0 deletions SampleWebApp/Modules/Some/Handlers/ExternalEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace SampleWebApp.Modules.Some.Handlers;

public record ExternalEvent(Guid CommandId, string Message) : IEvent;
11 changes: 11 additions & 0 deletions SampleWebApp/Modules/Some/Handlers/ExternalEventHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace SampleWebApp.Modules.Some.Handlers;

public class ExternalEventHandler : Cross.CQRS.Events.EventHandler<ExternalEvent>
{
protected override Task HandleAsync(ExternalEvent ev, CancellationToken cancellationToken)
{
Console.WriteLine($"{nameof(ExternalEventHandler)} with message '{ev.Message}'");

return Task.CompletedTask;
}
}
3 changes: 3 additions & 0 deletions SampleWebApp/Modules/Some/Handlers/InternalEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace SampleWebApp.Modules.Some.Handlers;

public record InternalEvent(Guid CommandId, string Message) : IEvent;
11 changes: 11 additions & 0 deletions SampleWebApp/Modules/Some/Handlers/InternalEventHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace SampleWebApp.Modules.Some.Handlers;

public class InternalEventHandler : Cross.CQRS.Events.EventHandler<InternalEvent>
{
protected override Task HandleAsync(InternalEvent ev, CancellationToken cancellationToken)
{
Console.WriteLine($"{nameof(InternalEventHandler)} with message '{ev.Message}'");

return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace SampleWebApp.Modules.Some.Handlers;

public class SomeScopeExternalCommand : Command
{
public SomeScopeExternalCommand()
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace SampleWebApp.Modules.Some.Handlers;

public class SomeScopeExternalCommandHandler : CommandHandler<SomeScopeExternalCommand>
{

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;
}
}
10 changes: 10 additions & 0 deletions SampleWebApp/Modules/Some/Handlers/SomeScopeInternalCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace SampleWebApp.Modules.Some.Handlers;



public class SomeScopeInternalCommand : Command
{
public SomeScopeInternalCommand()
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace SampleWebApp.Modules.Some.Handlers;

public class SomeScopeInternalCommandHandler : CommandHandler<SomeScopeExternalCommand>
{

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;
}
}
13 changes: 12 additions & 1 deletion SampleWebApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
//MediatR
builder.Services
.AddCQRS(typeof(Program).Assembly)
.AddEntityFrameworkIntegration<Context>();
.AddEntityFrameworkIntegration<Context>(TransactionBehaviorEnum.ScopeBehavior);

// services.AddDbContext<MyDbContext>(options => options.UseSqlServer(...));
// services.AddScoped<IGenericRepository<MyModel>, GenericRepository<MyModel>>();
// services.AddScoped<IMyDbContext, MyDbContext>();

var app = builder.Build();

Expand All @@ -20,4 +24,11 @@
app.UseSwaggerUI();
}

app.MapGet("/somescope", (IMediator mediator ) =>
{
var forecast = mediator.Send(new SomeScopeExternalCommand());
return forecast;
})
.WithName("RunSomeScope");

app.Run();

0 comments on commit 4145ac2

Please sign in to comment.