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 @@
[](https://github.com/Synnotech-AG/Synnotech.Linq2Db/blob/main/LICENSE)
-[](https://www.nuget.org/packages?q=Synnotech.Linq2Db/)
+[](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.