diff --git a/Code/Directory.Build.props b/Code/Directory.Build.props index c901845..51b1359 100644 --- a/Code/Directory.Build.props +++ b/Code/Directory.Build.props @@ -3,7 +3,7 @@ Synnotech AG Synnotech AG Copyright © Synnotech AG 2021 - 5.0.0 + 6.0.0 9.0 enable diff --git a/Code/Synnotech.Linq2Db.sln b/Code/Synnotech.Linq2Db.sln index 78e89c7..0c65120 100644 --- a/Code/Synnotech.Linq2Db.sln +++ b/Code/Synnotech.Linq2Db.sln @@ -13,6 +13,22 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Synnotech.Linq2Db.MsSqlServ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Synnotech.Linq2Db.MsSqlServer.Tests", "tests\Synnotech.Linq2Db.MsSqlServer.Tests\Synnotech.Linq2Db.MsSqlServer.Tests.csproj", "{9BAB4122-CA81-43DC-A3B7-0EBC6F47B9D7}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7FAA710E-14FF-40B1-9E9E-6B844FEE26B6}" + ProjectSection(SolutionItems) = preProject + CreateNuGetPackages.cmd = CreateNuGetPackages.cmd + Directory.Build.props = Directory.Build.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8FE25632-A132-4758-AE6C-EFBEB96ABCA2}" + ProjectSection(SolutionItems) = preProject + src\Directory.Build.props = src\Directory.Build.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0CD7D9FE-0FA5-42EC-B89D-42E5DAE4BC0C}" + ProjectSection(SolutionItems) = preProject + tests\Directory.Build.props = tests\Directory.Build.props + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -42,6 +58,8 @@ Global GlobalSection(NestedProjects) = preSolution {F5190667-0D64-4A81-A750-86A75360C34D} = {BD8C6FC1-44F8-4D80-8124-6CD2219ADF20} {9BAB4122-CA81-43DC-A3B7-0EBC6F47B9D7} = {BD8C6FC1-44F8-4D80-8124-6CD2219ADF20} + {8FE25632-A132-4758-AE6C-EFBEB96ABCA2} = {7FAA710E-14FF-40B1-9E9E-6B844FEE26B6} + {0CD7D9FE-0FA5-42EC-B89D-42E5DAE4BC0C} = {7FAA710E-14FF-40B1-9E9E-6B844FEE26B6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {38A61BC0-9686-4992-97D4-4ECA59CA8BD3} diff --git a/Code/src/Synnotech.Linq2Db.MsSqlServer/Synnotech.Linq2Db.MsSqlServer.csproj b/Code/src/Synnotech.Linq2Db.MsSqlServer/Synnotech.Linq2Db.MsSqlServer.csproj index d575871..73014b7 100644 --- a/Code/src/Synnotech.Linq2Db.MsSqlServer/Synnotech.Linq2Db.MsSqlServer.csproj +++ b/Code/src/Synnotech.Linq2Db.MsSqlServer/Synnotech.Linq2Db.MsSqlServer.csproj @@ -2,11 +2,11 @@ -Synntech.Linq2Db.MsSqlServer 5.0.0 +Synntech.Linq2Db.MsSqlServer 6.0.0 -------------------------------- -- added support for linq2db 3.4.3 and (breaking change) Synnotech.DatabaseAbstractions 3.0.0 -- read-only sessions and transactional sessions can now be created via ISessionFactory<T> +- added support for linq2db 3.4.5 +- the AsyncReadOnlySession can now have a transaction (breaking change) - see all docs at https://github.com/Synnotech-AG/Synnotech.Linq2Db diff --git a/Code/src/Synnotech.Linq2Db/AsyncReadOnlySession.cs b/Code/src/Synnotech.Linq2Db/AsyncReadOnlySession.cs index c81c0e0..a069444 100644 --- a/Code/src/Synnotech.Linq2Db/AsyncReadOnlySession.cs +++ b/Code/src/Synnotech.Linq2Db/AsyncReadOnlySession.cs @@ -1,4 +1,5 @@ using System; +using System.Data; using System.Threading.Tasks; using Light.GuardClauses; using LinqToDB.Data; @@ -18,22 +19,34 @@ namespace Synnotech.Linq2Db /// /// /// Your database context type that derives from . - public abstract class AsyncReadOnlySession : IAsyncReadOnlySession + public abstract class AsyncReadOnlySession : IAsyncReadOnlySession, IInitializeAsync where TDataConnection : DataConnection { /// /// Initializes a new instance of . /// /// The Linq2Db data connection used for database access. + /// + /// The isolation level for the transaction (optional). The default value is . + /// When this value is set to , no transaction will be started. + /// /// Thrown when is null. - protected AsyncReadOnlySession(TDataConnection dataConnection) => + protected AsyncReadOnlySession(TDataConnection dataConnection, IsolationLevel transactionLevel = IsolationLevel.Unspecified) + { DataConnection = dataConnection.MustNotBeNull(nameof(dataConnection)); + TransactionLevel = transactionLevel; + } /// /// Gets the Linq2Db data connection. /// protected TDataConnection DataConnection { get; } + /// + /// Gets the isolation level of the transaction. + /// + protected IsolationLevel TransactionLevel { get; } + /// /// Disposes the Linq2Db data connection. /// @@ -43,6 +56,14 @@ protected AsyncReadOnlySession(TDataConnection dataConnection) => /// Disposes the Linq2Db data connection. /// public ValueTask DisposeAsync() => DataConnection.DisposeAsync(); + + bool IInitializeAsync.IsInitialized => + TransactionLevel == IsolationLevel.Unspecified || DataConnection.Transaction != null; + + Task IInitializeAsync.InitializeAsync() => + TransactionLevel != IsolationLevel.Unspecified ? + DataConnection.BeginTransactionAsync(TransactionLevel) : + Task.CompletedTask; } /// @@ -59,7 +80,12 @@ public abstract class AsyncReadOnlySession : AsyncReadOnlySession directly. /// /// The Linq2Db data connection used for database access. + /// + /// The isolation level for the transaction (optional). The default value is . + /// When this value is set to , no transaction will be started. + /// /// Thrown when is null. - protected AsyncReadOnlySession(DataConnection dataConnection) : base(dataConnection) { } + protected AsyncReadOnlySession(DataConnection dataConnection, IsolationLevel transactionLevel = IsolationLevel.Unspecified) + : base(dataConnection, transactionLevel) { } } } \ No newline at end of file diff --git a/Code/src/Synnotech.Linq2Db/AsyncSession.cs b/Code/src/Synnotech.Linq2Db/AsyncSession.cs index 90f71fa..1c89067 100644 --- a/Code/src/Synnotech.Linq2Db/AsyncSession.cs +++ b/Code/src/Synnotech.Linq2Db/AsyncSession.cs @@ -32,33 +32,21 @@ public abstract class AsyncSession : AsyncReadOnlySession. /// /// The Linq2Db data connection used for database access. - /// The isolation level for the transaction. + /// + /// The isolation level for the transaction (optional). The default value is . + /// When this value is set to , no transaction will be started. + /// /// Thrown when is null. protected AsyncSession(TDataConnection dataConnection, IsolationLevel transactionLevel = IsolationLevel.Serializable) - : base(dataConnection) => - TransactionLevel = transactionLevel; - - /// - /// Gets the isolation level of the transaction. - /// - protected IsolationLevel TransactionLevel { get; } - - /// - /// Commits the internal transaction. - /// - public Task SaveChangesAsync(CancellationToken cancellationToken = default) => DataConnection.CommitTransactionAsync(cancellationToken); - - /// - /// Checks if a transaction is present on the underlying data connection. - /// - bool IInitializeAsync.IsInitialized => DataConnection.Transaction != null; + : base(dataConnection, transactionLevel) { } /// - /// Begins a transaction on the internal data connection asynchronously. This is an explicit interface implementation because clients should not - /// have to call this method. Instead, the session should be instantiated via which - /// in turn calls InitializeAsync. + /// Commits the internal transaction if possible. /// - Task IInitializeAsync.InitializeAsync() => DataConnection.BeginTransactionAsync(TransactionLevel); + public Task SaveChangesAsync(CancellationToken cancellationToken = default) => + TransactionLevel != IsolationLevel.Unspecified ? + DataConnection.CommitTransactionAsync(cancellationToken) : + Task.CompletedTask; } /// diff --git a/Code/src/Synnotech.Linq2Db/Synnotech.Linq2Db.csproj b/Code/src/Synnotech.Linq2Db/Synnotech.Linq2Db.csproj index cc0b73a..05282f1 100644 --- a/Code/src/Synnotech.Linq2Db/Synnotech.Linq2Db.csproj +++ b/Code/src/Synnotech.Linq2Db/Synnotech.Linq2Db.csproj @@ -2,17 +2,17 @@ -Synntech.Linq2Db 5.0.0 +Synntech.Linq2Db 6.0.0 -------------------------------- -- added support for linq2db 3.4.3 and (breaking change) Synnotech.DatabaseAbstractions 3.0.0 -- read-only sessions and transactional sessions can now be created via ISessionFactory<T> +- added support for linq2db 3.4.5 +- the AsyncReadOnlySession can now have a transaction (breaking change) - see all docs at https://github.com/Synnotech-AG/Synnotech.Linq2Db - + diff --git a/Code/tests/Directory.Build.props b/Code/tests/Directory.Build.props index 2869f05..c33ddc7 100644 --- a/Code/tests/Directory.Build.props +++ b/Code/tests/Directory.Build.props @@ -7,12 +7,12 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + \ No newline at end of file diff --git a/Code/tests/Synnotech.Linq2Db.MsSqlServer.Tests/AsyncReadOnlySessionTests.cs b/Code/tests/Synnotech.Linq2Db.MsSqlServer.Tests/AsyncReadOnlySessionTests.cs index fdc3be3..9a2f290 100644 --- a/Code/tests/Synnotech.Linq2Db.MsSqlServer.Tests/AsyncReadOnlySessionTests.cs +++ b/Code/tests/Synnotech.Linq2Db.MsSqlServer.Tests/AsyncReadOnlySessionTests.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Data; using System.Threading.Tasks; using FluentAssertions; using LinqToDB; @@ -42,6 +43,20 @@ public async Task LoadDataWithSessionFactory() CheckLoadedEmployees(employees); } + [SkippableFact] + public async Task LoadDataWithExplicitTransaction() + { + SkipTestIfNecessary(); + + var sessionFactory = PrepareContainer().AddSessionFactoryFor() + .BuildServiceProvider() + .GetRequiredService>(); + await using var session = await sessionFactory.OpenSessionAsync(); + var employees = await session.GetEmployeesAsync(); + + CheckLoadedEmployees(employees); + } + private static void CheckLoadedEmployees(List? employees) { var expectedEmployees = new[] @@ -64,5 +79,12 @@ public EmployeeSession(DataConnection dataConnection) : base(dataConnection) { } public Task> GetEmployeesAsync() => DataConnection.GetTable().ToListAsync(); } + + private sealed class SessionWithTransactions : AsyncReadOnlySession, IEmployeeSession + { + public SessionWithTransactions(DataConnection dataConnection) : base(dataConnection, IsolationLevel.ReadUncommitted) { } + + public Task> GetEmployeesAsync() => DataConnection.GetTable().ToListAsync(); + } } } \ No newline at end of file diff --git a/Code/tests/Synnotech.Linq2Db.Tests/AsyncReadOnlySessionTests.cs b/Code/tests/Synnotech.Linq2Db.Tests/AsyncReadOnlySessionTests.cs index 09e9dac..ced3d2e 100644 --- a/Code/tests/Synnotech.Linq2Db.Tests/AsyncReadOnlySessionTests.cs +++ b/Code/tests/Synnotech.Linq2Db.Tests/AsyncReadOnlySessionTests.cs @@ -18,5 +18,9 @@ public static void MustImplementIAsyncDisposable() => [Fact] public static void MustImplementIAsyncReadOnlySession() => typeof(AsyncReadOnlySession<>).Should().Implement(); + + [Fact] + public static void MustImplementIInitializeAsync() => + typeof(AsyncReadOnlySession<>).Should().Implement(); } } \ No newline at end of file diff --git a/Code/tests/Synnotech.Linq2Db.Tests/AsyncSessionTests.cs b/Code/tests/Synnotech.Linq2Db.Tests/AsyncSessionTests.cs index 22db6e0..36e4de9 100644 --- a/Code/tests/Synnotech.Linq2Db.Tests/AsyncSessionTests.cs +++ b/Code/tests/Synnotech.Linq2Db.Tests/AsyncSessionTests.cs @@ -9,5 +9,9 @@ public static class AsyncSessionTests [Fact] public static void MustImplementIAsyncSession() => typeof(AsyncSession<>).Should().Implement(); + + [Fact] + public static void MustDeriveFromAsyncReadOnlySession() => + typeof(AsyncSession<>).Should().BeDerivedFrom(typeof(AsyncReadOnlySession<>)); } } \ No newline at end of file diff --git a/Code/tests/Synnotech.Linq2Db.Tests/AsyncTransactionalSessionTests.cs b/Code/tests/Synnotech.Linq2Db.Tests/AsyncTransactionalSessionTests.cs index e470005..98d9f07 100644 --- a/Code/tests/Synnotech.Linq2Db.Tests/AsyncTransactionalSessionTests.cs +++ b/Code/tests/Synnotech.Linq2Db.Tests/AsyncTransactionalSessionTests.cs @@ -9,5 +9,9 @@ public static class AsyncTransactionalSessionTests [Fact] public static void MustImplementITransactionalSession() => typeof(AsyncTransactionalSession<>).Should().Implement(); + + [Fact] + public static void MustDeriveFromAsyncReadOnlySession() => + typeof(AsyncTransactionalSession<>).Should().BeDerivedFrom(typeof(AsyncReadOnlySession<>)); } } \ No newline at end of file diff --git a/readme.md b/readme.md index c95e0f3..6c6b251 100644 --- a/readme.md +++ b/readme.md @@ -6,7 +6,7 @@ [![License](https://img.shields.io/badge/License-MIT-green.svg?style=for-the-badge)](https://github.com/Synnotech-AG/Synnotech.Linq2Db/blob/main/LICENSE) -[![NuGet](https://img.shields.io/badge/NuGet-5.0.0-blue.svg?style=for-the-badge)](https://www.nuget.org/packages?q=Synnotech.Linq2Db/) +[![NuGet](https://img.shields.io/badge/NuGet-6.0.0-blue.svg?style=for-the-badge)](https://www.nuget.org/packages?q=Synnotech.Linq2Db/) # How to install @@ -296,6 +296,35 @@ When you register a session factory using `services.AddSessionFactoryFor`, you h - `factoryLifetime`: the life time of the `SessionFactory`. The default value is singleton. You could choose another lifetime if you want the GC to grab a session factory when it is not in use. - `registerCreateSessionDelegate`: the value indicating if a `Func` should also be registered with the DI container. This delegate is necessary for the session factory to resolve the session from the DI container. If you use a sophisticated DI container like [LightInject](https://www.lightinject.net/) that offers [Function Factories](https://www.lightinject.net/#function-factories), you can (and should) set this parameter to false. +## Using a transaction in AsyncReadOnlySession + +By default, `AsyncReadOnlySession` will not create a transaction explicitly. However, in some scenarios, you might want to create a transaction nonetheless even if you only read data. You can do this by deriving from `AsyncReadOnlySession` (or `AsyncReadOnlySession`) and supply an isolation value as the second parameter to the base constructor call: + +```csharp +public class MySession : AsyncReadOnlySession, IMySession +{ + public MySession(DataConnection dataConnection) : base(dataConnection, IsolationLevel.ReadUncommitted) { } + + // Other members omitted for brevity's sake +} +``` + +In the code sample above, the `AsyncReadOnlySession` will create a transaction when instatiated via `ISessionFactory`: + +```csharp +// In your composition root: +services.AddSessionFactoryFor(); + +// When instantiating your session: +await using var session = await SessionFactory.OpenSessionAsync(); // this call will start the transaction asynchronously +``` + +The transaction will always be rolled back when your session goes out of scope. + +A scenario where you might want to do this is the following: consider that you are having a long running transaction in MS SQL Server that also updates or inserts data. All other read-only calls to the database will be blocked as long as they touch one or more records that were also created / manipulated in the long running transaction. This will block every call, unless these read-only database calls are wrapped in read-uncommited transactions themselves. However, in MS SQL Server, this is not the default behavior: if you do not specify a dedicated transaction, each statement / command will be wrapped in a read-committed transaction. + +In general, we recommend to avoid this setting. Only use it if you have a special use case for it. + # General recommendations 1. All I/O should be abstracted. You should create abstractions that are specific for your use cases.