Skip to content

Commit e216454

Browse files
Merge pull request #182 from AntonioFalcao/feature/graphql-4
Evolving UnitOfWork with ExecutionStrategy and TransactionScope
2 parents 9cdf433 + 2f1b78c commit e216454

15 files changed

Lines changed: 253 additions & 47 deletions

File tree

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,36 @@ ___
237237

238238
## Highlights
239239

240+
### UnitOfWork + Execution Strategy + Transaction Scope
241+
242+
The implementation of the `UnitOfWork` gives support to the `ExecutionStrategy` from **EF Core** with `TransactionScope`.
243+
244+
> **operationAsync**: Encapsulates all desired transactions;
245+
> **condition**: External control for commitment;
246+
> **cancellationToken**: The cancellation token to be used within operation.
247+
248+
```c#
249+
public Task<Review> AddReviewAsync(ReviewModel reviewModel, CancellationToken cancellationToken)
250+
{
251+
return UnitOfWork.ExecuteInTransactionAsync(
252+
operationAsync: async ct => // Func<CancellationToken, Task<TResult>>
253+
{
254+
var product = await Repository.GetByIdAsync(
255+
id: reviewModel.ProductId,
256+
include: products => products.Include(x => x.Reviews),
257+
asTracking: true,
258+
cancellationToken: ct);
259+
260+
var review = Mapper.Map<Review>(reviewModel);
261+
product?.AddReview(review);
262+
await OnEditAsync(product, ct);
263+
return review;
264+
},
265+
condition: _ => NotificationContext.AllValidAsync, // Func<CancellationToken, Task<bool>>
266+
cancellationToken: cancellationToken);
267+
}
268+
```
269+
240270
### Notifications (pattern/context)
241271

242272
To avoid handle exceptions, was implemented a [`NotificationContext`](./src/Dotnet6.GraphQL4.CrossCutting/Notifications/NotificationContext.cs) that's allow all layers add business notifications through the request, with support to receive **Domain** notifications, that by other side, implementing validators from **Fluent Validation** and return a `ValidationResult`.

src/Dotnet6.GraphQL4.CrossCutting/Notifications/INotificationContext.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Threading.Tasks;
34
using FluentValidation.Results;
45
using GraphQL;
56

@@ -9,7 +10,10 @@ public interface INotificationContext
910
{
1011
ExecutionErrors ExecutionErrors { get; }
1112
IReadOnlyList<Notification> Notifications { get; }
13+
bool AllValid { get; }
14+
Task<bool> AllValidAsync { get; }
1215
bool HasNotifications { get; }
16+
Task<bool> HasNotificationsAsync { get; }
1317

1418
void AddNotification(string message, string key = default);
1519
void AddNotification(Notification notification);

src/Dotnet6.GraphQL4.CrossCutting/Notifications/NotificationContext.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4+
using System.Threading.Tasks;
45
using FluentValidation.Results;
56
using GraphQL;
67

@@ -26,8 +27,14 @@ public ExecutionErrors ExecutionErrors
2627

2728
public IReadOnlyList<Notification> Notifications
2829
=> _notifications;
29-
public bool HasNotifications
30+
public bool AllValid
31+
=> HasNotifications is false;
32+
public Task<bool> AllValidAsync
33+
=> Task.FromResult(HasNotifications is false);
34+
public bool HasNotifications
3035
=> _notifications.Any();
36+
public Task<bool> HasNotificationsAsync
37+
=> Task.FromResult(HasNotifications);
3138

3239
public void AddNotification(string message, string key = default)
3340
=> _notifications.Add(new(key, message));
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using System.Transactions;
2+
3+
namespace Dotnet6.GraphQL4.Repositories.Abstractions.DependencyInjection
4+
{
5+
public class ApplicationTransactionOptions
6+
{
7+
public IsolationLevel IsolationLevel { get; set; }
8+
}
9+
}

src/Dotnet6.GraphQL4.Repositories.Abstractions/Extensions/DependencyInjection/ServiceCollectionExtensions.cs renamed to src/Dotnet6.GraphQL4.Repositories.Abstractions/DependencyInjection/Extensions/ServiceCollectionExtensions.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
using Dotnet6.GraphQL4.CrossCutting;
1+
using System.Transactions;
2+
using Dotnet6.GraphQL4.CrossCutting;
23
using Dotnet6.GraphQL4.Repositories.Abstractions.UnitsOfWork;
4+
using Microsoft.Extensions.Configuration;
35
using Microsoft.Extensions.DependencyInjection;
6+
using Microsoft.Extensions.Options;
47
using Scrutor;
58

6-
namespace Dotnet6.GraphQL4.Repositories.Abstractions.Extensions.DependencyInjection
9+
namespace Dotnet6.GraphQL4.Repositories.Abstractions.DependencyInjection.Extensions
710
{
811
public static class ServiceCollectionExtensions
912
{
@@ -17,5 +20,11 @@ public static IServiceCollection AddRepositories(this IServiceCollection service
1720

1821
public static IServiceCollection AddUnitOfWork(this IServiceCollection services)
1922
=> services.AddScoped<IUnitOfWork, UnitOfWork>();
23+
24+
public static OptionsBuilder<ApplicationTransactionOptions> ConfigureTransactionOptions(this IServiceCollection services, IConfigurationSection section)
25+
=> services
26+
.AddOptions<ApplicationTransactionOptions>()
27+
.Bind(section)
28+
.Validate(options => options.IsolationLevel is not IsolationLevel.Unspecified);
2029
}
2130
}

src/Dotnet6.GraphQL4.Repositories.Abstractions/Dotnet6.GraphQL4.Repositories.Abstractions.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<ItemGroup>
33
<PackageReference Include="AutoMapper" Version="$(AutoMapper_Version)" />
44
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="$(Microsoft_EntityFrameworkCore_Version)" />
5+
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="$(Microsoft_Extensions_Version)" />
56
<PackageReference Include="Scrutor" Version="$(Scrutor_Version)" />
67
</ItemGroup>
78
<ItemGroup>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
5+
namespace Dotnet6.GraphQL4.Repositories.Abstractions.Transactions.Extensions
6+
{
7+
public static class TransactionScopeExecuterExtensions
8+
{
9+
public static TransactionScopeExecutor<T> Transaction<T>(this Func<T> operation)
10+
=> new(operation);
11+
12+
public static TransactionScopeExecutor<T> TransactionAsync<T>(this Func<CancellationToken, Task<T>> operationAsync)
13+
=> new(operationAsync);
14+
}
15+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using System.Transactions;
5+
6+
namespace Dotnet6.GraphQL4.Repositories.Abstractions.Transactions
7+
{
8+
public class TransactionScopeExecutor<T>
9+
{
10+
private TransactionScopeOption _scopeOption;
11+
private TransactionScopeAsyncFlowOption _asyncFlowOption;
12+
13+
private Func<bool> _condition;
14+
private Func<CancellationToken, Task<bool>> _conditionAsync;
15+
16+
private readonly TransactionOptions _transactionOptions;
17+
private readonly Func<T> _operation;
18+
private readonly Func<CancellationToken, Task<T>> _operationAsync;
19+
20+
public TransactionScopeExecutor() { }
21+
22+
public TransactionScopeExecutor(Func<T> operation)
23+
{
24+
_operation = operation;
25+
}
26+
27+
public TransactionScopeExecutor(Func<CancellationToken, Task<T>> operationAsync)
28+
{
29+
_operationAsync = operationAsync;
30+
}
31+
32+
public TransactionScopeExecutor<T> WithScopeOption(TransactionScopeOption scopeOption = TransactionScopeOption.Required)
33+
{
34+
_scopeOption = scopeOption;
35+
return this;
36+
}
37+
38+
public TransactionScopeExecutor<T> WithOptions(Action<TransactionOptions> actionTransactionOptions)
39+
{
40+
actionTransactionOptions(_transactionOptions);
41+
return this;
42+
}
43+
44+
public TransactionScopeExecutor<T> WithScopeAsyncFlowOption(TransactionScopeAsyncFlowOption asyncFlowOption = TransactionScopeAsyncFlowOption.Enabled)
45+
{
46+
_asyncFlowOption = asyncFlowOption;
47+
return this;
48+
}
49+
50+
public TransactionScopeExecutor<T> WithCondition(Func<bool> condition)
51+
{
52+
_condition = condition;
53+
return this;
54+
}
55+
56+
public TransactionScopeExecutor<T> WithConditionAsync(Func<CancellationToken, Task<bool>> conditionAsync)
57+
{
58+
_conditionAsync = conditionAsync;
59+
return this;
60+
}
61+
62+
public T Execute(Func<T> operation)
63+
{
64+
using var scope = CreateScope();
65+
var result = operation();
66+
if (_condition()) scope.Complete();
67+
return result;
68+
}
69+
70+
public T Execute()
71+
{
72+
using var scope = CreateScope();
73+
var result = _operation();
74+
75+
if (_condition())
76+
scope.Complete();
77+
78+
return result;
79+
}
80+
81+
public async Task<T> ExecuteAsync(CancellationToken cancellationToken)
82+
{
83+
using var scope = CreateScope();
84+
var result = await _operationAsync(cancellationToken);
85+
86+
if (await _conditionAsync(cancellationToken))
87+
scope.Complete();
88+
89+
return result;
90+
}
91+
92+
public async Task<T> ExecuteAsync(Func<CancellationToken, Task<T>> operationAsync, CancellationToken cancellationToken)
93+
{
94+
using var scope = CreateScope();
95+
var result = await operationAsync(cancellationToken);
96+
97+
if (await _conditionAsync(cancellationToken))
98+
scope.Complete();
99+
100+
return result;
101+
}
102+
103+
private TransactionScope CreateScope()
104+
=> new(
105+
scopeOption: _scopeOption,
106+
transactionOptions: _transactionOptions,
107+
asyncFlowOption: _asyncFlowOption);
108+
}
109+
}

src/Dotnet6.GraphQL4.Repositories.Abstractions/UnitsOfWork/IUnitOfWork.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
using System;
22
using System.Threading;
33
using System.Threading.Tasks;
4-
using Microsoft.EntityFrameworkCore.Storage;
54

65
namespace Dotnet6.GraphQL4.Repositories.Abstractions.UnitsOfWork
76
{
8-
public interface IUnitOfWork : IDisposable
7+
public interface IUnitOfWork
98
{
10-
IDbContextTransaction BeginTransaction();
11-
Task<IDbContextTransaction> BeginTransactionAsync(CancellationToken cancellationToken);
12-
void CommitTransaction();
13-
Task CommitTransactionAsync(CancellationToken cancellationToken);
14-
void RollbackTransaction();
15-
Task RollbackTransactionAsync(CancellationToken cancellationToken);
9+
TResult ExecuteInTransaction<TResult>(Func<TResult> operation, Func<bool> condition);
10+
Task<TResult> ExecuteInTransactionAsync<TResult>(Func<CancellationToken, Task<TResult>> operationAsync, Func<CancellationToken, Task<bool>> condition, CancellationToken cancellationToken);
11+
1612
bool SaveChanges();
1713
Task<bool> SaveChangesAsync(CancellationToken cancellationToken);
1814
}
Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,63 @@
11
using System;
22
using System.Threading;
33
using System.Threading.Tasks;
4+
using System.Transactions;
5+
using Dotnet6.GraphQL4.CrossCutting.Notifications;
6+
using Dotnet6.GraphQL4.Repositories.Abstractions.DependencyInjection;
7+
using Dotnet6.GraphQL4.Repositories.Abstractions.Transactions.Extensions;
48
using Microsoft.EntityFrameworkCore;
59
using Microsoft.EntityFrameworkCore.Infrastructure;
610
using Microsoft.EntityFrameworkCore.Storage;
11+
using Microsoft.Extensions.Options;
712

813
namespace Dotnet6.GraphQL4.Repositories.Abstractions.UnitsOfWork
914
{
1015
public class UnitOfWork : IUnitOfWork
1116
{
1217
private readonly DatabaseFacade _database;
1318
private readonly DbContext _dbContext;
19+
private readonly IOptionsMonitor<ApplicationTransactionOptions> _options;
20+
private readonly INotificationContext _notificationContext;
1421

15-
public UnitOfWork(DbContext dbContext)
22+
public UnitOfWork(DbContext dbContext, IOptionsMonitor<ApplicationTransactionOptions> options, INotificationContext notificationContext)
1623
{
1724
_dbContext = dbContext;
25+
_options = options;
1826
_database = _dbContext.Database;
27+
_notificationContext = notificationContext;
1928
}
2029

21-
public IDbContextTransaction BeginTransaction()
22-
=> _database.BeginTransaction();
23-
24-
public Task<IDbContextTransaction> BeginTransactionAsync(CancellationToken cancellationToken)
25-
=> _database.BeginTransactionAsync(cancellationToken);
30+
public TResult ExecuteInTransaction<TResult>(Func<TResult> operation, Func<bool> condition)
31+
=> CreateExecutionStrategy().Execute(() => ExecuteWithScope(operation, condition));
32+
33+
public Task<TResult> ExecuteInTransactionAsync<TResult>(Func<CancellationToken, Task<TResult>> operationAsync, Func<CancellationToken, Task<bool>> condition, CancellationToken cancellationToken)
34+
=> CreateExecutionStrategy().ExecuteAsync(ct => ExecuteWithScopeAsync(operationAsync, condition, ct), cancellationToken);
35+
36+
private Task<TResult> ExecuteWithScopeAsync<TResult>(Func<CancellationToken, Task<TResult>> operationAsync, Func<CancellationToken, Task<bool>> condition, CancellationToken cancellationToken)
37+
=> operationAsync
38+
.TransactionAsync()
39+
.WithScopeOption(TransactionScopeOption.Required)
40+
.WithOptions(options => options.IsolationLevel = _options.CurrentValue.IsolationLevel)
41+
.WithScopeAsyncFlowOption(TransactionScopeAsyncFlowOption.Enabled)
42+
.WithConditionAsync(condition ?? (_ => _notificationContext.AllValidAsync))
43+
.ExecuteAsync(cancellationToken);
44+
45+
private TResult ExecuteWithScope<TResult>(Func<TResult> operation, Func<bool> condition)
46+
=> operation
47+
.Transaction()
48+
.WithScopeOption(TransactionScopeOption.Required)
49+
.WithOptions(options => options.IsolationLevel = _options.CurrentValue.IsolationLevel)
50+
.WithScopeAsyncFlowOption(TransactionScopeAsyncFlowOption.Enabled)
51+
.WithCondition(condition ?? (() => _notificationContext.AllValid))
52+
.Execute();
53+
54+
private IExecutionStrategy CreateExecutionStrategy()
55+
=> _database.CreateExecutionStrategy();
2656

2757
public bool SaveChanges()
2858
=> _dbContext.SaveChanges(true) > default(int);
2959

3060
public async Task<bool> SaveChangesAsync(CancellationToken cancellationToken)
3161
=> await _dbContext.SaveChangesAsync(true, cancellationToken) > default(int);
32-
33-
public void CommitTransaction()
34-
=> _database.CommitTransaction();
35-
36-
public Task CommitTransactionAsync(CancellationToken cancellationToken)
37-
=> _database.CommitTransactionAsync(cancellationToken);
38-
39-
public void RollbackTransaction()
40-
=> _database.RollbackTransaction();
41-
42-
public Task RollbackTransactionAsync(CancellationToken cancellationToken)
43-
=> _database.RollbackTransactionAsync(cancellationToken);
44-
45-
public void Dispose()
46-
{
47-
_dbContext?.Dispose();
48-
GC.SuppressFinalize(this);
49-
}
5062
}
5163
}

0 commit comments

Comments
 (0)