From c8a93ce20de62ee175dba696b5423c6377ec87c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabr=C3=ADcio=20Godoy?= Date: Thu, 7 Sep 2023 17:09:33 -0300 Subject: [PATCH] feat(efcore): support DB transactions --- .../Sessions/EfDbSession.cs | 6 +++ .../Sessions/EfDbSessionTransaction.cs | 21 ++++++++ .../Sessions/MartenDbSession.cs | 6 +++ .../Sessions/IDbSession.cs | 13 ++++- .../Sessions/IDbSessionTransaction.cs | 9 ++++ .../Sessions/SessionFactoryTestBase.cs | 4 +- .../Sessions/EfSessionFactoryTest.cs | 48 ++++++++++++++++++- 7 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 src/Expressions.EntityFrameworkCore/Sessions/EfDbSessionTransaction.cs create mode 100644 src/Expressions.Writing/Sessions/IDbSessionTransaction.cs diff --git a/src/Expressions.EntityFrameworkCore/Sessions/EfDbSession.cs b/src/Expressions.EntityFrameworkCore/Sessions/EfDbSession.cs index 90707b2..8485d67 100644 --- a/src/Expressions.EntityFrameworkCore/Sessions/EfDbSession.cs +++ b/src/Expressions.EntityFrameworkCore/Sessions/EfDbSession.cs @@ -28,6 +28,12 @@ public EfDbSession( public TContext Context { get; } public ChangeTracking Tracking { get; } + public async ValueTask BeginTransactionAsync(CancellationToken cancellationToken = default) + { + var transaction = await Context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); + return new EfDbSessionTransaction(transaction); + } + public void Add(TEntity entity) where TEntity : class => Context.Add(entity); diff --git a/src/Expressions.EntityFrameworkCore/Sessions/EfDbSessionTransaction.cs b/src/Expressions.EntityFrameworkCore/Sessions/EfDbSessionTransaction.cs new file mode 100644 index 0000000..38468f5 --- /dev/null +++ b/src/Expressions.EntityFrameworkCore/Sessions/EfDbSessionTransaction.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore.Storage; +using Raiqub.Expressions.Sessions; + +namespace Raiqub.Expressions.EntityFrameworkCore.Sessions; + +public sealed class EfDbSessionTransaction : IDbSessionTransaction +{ + private readonly IDbContextTransaction _contextTransaction; + + public EfDbSessionTransaction(IDbContextTransaction contextTransaction) => + _contextTransaction = contextTransaction; + + public Task CommitAsync(CancellationToken cancellationToken = default) => + _contextTransaction.CommitAsync(cancellationToken); + + public ValueTask DisposeAsync() => + _contextTransaction.DisposeAsync(); + + public void Dispose() => + _contextTransaction.Dispose(); +} diff --git a/src/Expressions.Marten/Sessions/MartenDbSession.cs b/src/Expressions.Marten/Sessions/MartenDbSession.cs index d48a2a1..e23ef73 100644 --- a/src/Expressions.Marten/Sessions/MartenDbSession.cs +++ b/src/Expressions.Marten/Sessions/MartenDbSession.cs @@ -17,6 +17,12 @@ public MartenDbSession(ILogger logger, IDocumentSession session public ChangeTracking Tracking { get; } + public ValueTask BeginTransactionAsync(CancellationToken cancellationToken = default) + { + throw new NotSupportedException( + "Transactions are not currently supported by Marten implementation of IDbSession"); + } + public void Add(TEntity entity) where TEntity : class => AddRange(new[] { entity }); diff --git a/src/Expressions.Writing/Sessions/IDbSession.cs b/src/Expressions.Writing/Sessions/IDbSession.cs index 70d293c..23113c6 100644 --- a/src/Expressions.Writing/Sessions/IDbSession.cs +++ b/src/Expressions.Writing/Sessions/IDbSession.cs @@ -1,4 +1,6 @@ -namespace Raiqub.Expressions.Sessions; +using System.Diagnostics.Contracts; + +namespace Raiqub.Expressions.Sessions; /// Represents a session used to perform data access operations. public interface IDbSession : IDbQuerySession @@ -9,6 +11,15 @@ public interface IDbSession : IDbQuerySession /// ChangeTracking Tracking { get; } + /// Starts a new transaction. + /// A token to observe for cancellation requests. + /// + /// A task that represents the asynchronous transaction initialization. + /// The task result contains a that represents the started transaction. + /// + [Pure] + ValueTask BeginTransactionAsync(CancellationToken cancellationToken = default); + /// Tracks the specified entity as added. /// The type of entity to add. /// The entity to add. diff --git a/src/Expressions.Writing/Sessions/IDbSessionTransaction.cs b/src/Expressions.Writing/Sessions/IDbSessionTransaction.cs new file mode 100644 index 0000000..7dd03bf --- /dev/null +++ b/src/Expressions.Writing/Sessions/IDbSessionTransaction.cs @@ -0,0 +1,9 @@ +namespace Raiqub.Expressions.Sessions; + +public interface IDbSessionTransaction : IAsyncDisposable, IDisposable +{ + /// Commits all changes made to the database in the current transaction. + /// A token to observe for cancellation requests. + /// A that represents the asynchronous operation. + Task CommitAsync(CancellationToken cancellationToken = default); +} diff --git a/tests/Common.Tests/Sessions/SessionFactoryTestBase.cs b/tests/Common.Tests/Sessions/SessionFactoryTestBase.cs index eaeaa59..cfafd9e 100644 --- a/tests/Common.Tests/Sessions/SessionFactoryTestBase.cs +++ b/tests/Common.Tests/Sessions/SessionFactoryTestBase.cs @@ -165,9 +165,9 @@ public async Task RemoveAndSaveShouldCommitChanges() blogs.Should().NotContain(blog => blog.Name == "Second"); } - private IDbSessionFactory CreateSessionFactory() => ServiceProvider.GetRequiredService(); + protected IDbSessionFactory CreateSessionFactory() => ServiceProvider.GetRequiredService(); - private static IEnumerable GetBlogs() + protected static IEnumerable GetBlogs() { DateTimeOffset now = DateTimeOffset.UtcNow; diff --git a/tests/Expressions.EntityFrameworkCore.Tests/Sessions/EfSessionFactoryTest.cs b/tests/Expressions.EntityFrameworkCore.Tests/Sessions/EfSessionFactoryTest.cs index 5916788..d5d3b46 100644 --- a/tests/Expressions.EntityFrameworkCore.Tests/Sessions/EfSessionFactoryTest.cs +++ b/tests/Expressions.EntityFrameworkCore.Tests/Sessions/EfSessionFactoryTest.cs @@ -1,9 +1,12 @@ -using Microsoft.Extensions.DependencyInjection; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Raiqub.Common.Tests; +using Raiqub.Common.Tests.Examples; using Raiqub.Common.Tests.Sessions; using Raiqub.Expressions.EntityFrameworkCore.Tests.Examples; +using Raiqub.Expressions.Sessions; namespace Raiqub.Expressions.EntityFrameworkCore.Tests.Sessions; @@ -27,4 +30,47 @@ public EfSessionFactoryTest(PostgreSqlFixture fixture) public Task InitializeAsync() => _fixture.SnapshotDatabaseAsync(); public Task DisposeAsync() => _fixture.ResetDatabaseAsync(); + + [Fact] + public async Task AddAndSaveOnUncommittedTransactionShouldNotCommitChanges() + { + var sessionFactory = CreateSessionFactory(); + + await using (var session = sessionFactory.Create()) + await using (await session.BeginTransactionAsync()) + { + await session.AddAsync(GetBlogs().First()); + await session.SaveChangesAsync(); + } + + int count; + await using (var session = sessionFactory.Create()) + { + count = await session.Query().CountAsync(); + } + + count.Should().Be(0); + } + + [Fact] + public async Task AddAndSaveOnCommittedTransactionShouldCommitChanges() + { + var sessionFactory = CreateSessionFactory(); + + await using (var session = sessionFactory.Create()) + await using (var transaction = await session.BeginTransactionAsync()) + { + await session.AddAsync(GetBlogs().First()); + await session.SaveChangesAsync(); + await transaction.CommitAsync(); + } + + int count; + await using (var session = sessionFactory.Create()) + { + count = await session.Query().CountAsync(); + } + + count.Should().Be(1); + } }