Skip to content

Commit

Permalink
Merge pull request #3 from Synnotech-AG/transactions-for-readonly-ses…
Browse files Browse the repository at this point in the history
…sions

Transactions for readonly sessions
  • Loading branch information
feO2x authored Oct 8, 2021
2 parents 3ef8a9f + cacf8ce commit d868823
Show file tree
Hide file tree
Showing 12 changed files with 131 additions and 36 deletions.
2 changes: 1 addition & 1 deletion Code/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<Authors>Synnotech AG</Authors>
<Company>Synnotech AG</Company>
<Copyright>Copyright © Synnotech AG 2021</Copyright>
<Version>5.0.0</Version>
<Version>6.0.0</Version>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
18 changes: 18 additions & 0 deletions Code/Synnotech.Linq2Db.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

<PropertyGroup>
<PackageReleaseNotes>
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&lt;T&gt;
- 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
</PackageReleaseNotes>
</PropertyGroup>
Expand Down
32 changes: 29 additions & 3 deletions Code/src/Synnotech.Linq2Db/AsyncReadOnlySession.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Data;
using System.Threading.Tasks;
using Light.GuardClauses;
using LinqToDB.Data;
Expand All @@ -18,22 +19,34 @@ namespace Synnotech.Linq2Db
/// </para>
/// </summary>
/// <typeparam name="TDataConnection">Your database context type that derives from <see cref="DataConnection" />.</typeparam>
public abstract class AsyncReadOnlySession<TDataConnection> : IAsyncReadOnlySession
public abstract class AsyncReadOnlySession<TDataConnection> : IAsyncReadOnlySession, IInitializeAsync
where TDataConnection : DataConnection
{
/// <summary>
/// Initializes a new instance of <see cref="AsyncReadOnlySession{TDataConnection}" />.
/// </summary>
/// <param name="dataConnection">The Linq2Db data connection used for database access.</param>
/// <param name="transactionLevel">
/// The isolation level for the transaction (optional). The default value is <see cref="IsolationLevel.Unspecified" />.
/// When this value is set to <see cref="IsolationLevel.Unspecified" />, no transaction will be started.
/// </param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="dataConnection" /> is null.</exception>
protected AsyncReadOnlySession(TDataConnection dataConnection) =>
protected AsyncReadOnlySession(TDataConnection dataConnection, IsolationLevel transactionLevel = IsolationLevel.Unspecified)
{
DataConnection = dataConnection.MustNotBeNull(nameof(dataConnection));
TransactionLevel = transactionLevel;
}

/// <summary>
/// Gets the Linq2Db data connection.
/// </summary>
protected TDataConnection DataConnection { get; }

/// <summary>
/// Gets the isolation level of the transaction.
/// </summary>
protected IsolationLevel TransactionLevel { get; }

/// <summary>
/// Disposes the Linq2Db data connection.
/// </summary>
Expand All @@ -43,6 +56,14 @@ protected AsyncReadOnlySession(TDataConnection dataConnection) =>
/// Disposes the Linq2Db data connection.
/// </summary>
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;
}

/// <summary>
Expand All @@ -59,7 +80,12 @@ public abstract class AsyncReadOnlySession : AsyncReadOnlySession<DataConnection
/// if you want to pass in the <see cref="DataConnection" /> directly.
/// </summary>
/// <param name="dataConnection">The Linq2Db data connection used for database access.</param>
/// <param name="transactionLevel">
/// The isolation level for the transaction (optional). The default value is <see cref="IsolationLevel.Unspecified" />.
/// When this value is set to <see cref="IsolationLevel.Unspecified" />, no transaction will be started.
/// </param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="dataConnection" /> is null.</exception>
protected AsyncReadOnlySession(DataConnection dataConnection) : base(dataConnection) { }
protected AsyncReadOnlySession(DataConnection dataConnection, IsolationLevel transactionLevel = IsolationLevel.Unspecified)
: base(dataConnection, transactionLevel) { }
}
}
32 changes: 10 additions & 22 deletions Code/src/Synnotech.Linq2Db/AsyncSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,33 +32,21 @@ public abstract class AsyncSession<TDataConnection> : AsyncReadOnlySession<TData
/// Initializes a new instance of <see cref="AsyncSession{TDataConnection}" />.
/// </summary>
/// <param name="dataConnection">The Linq2Db data connection used for database access.</param>
/// <param name="transactionLevel">The isolation level for the transaction.</param>
/// <param name="transactionLevel">
/// The isolation level for the transaction (optional). The default value is <see cref="IsolationLevel.Serializable" />.
/// When this value is set to <see cref="IsolationLevel.Unspecified" />, no transaction will be started.
/// </param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="dataConnection" /> is null.</exception>
protected AsyncSession(TDataConnection dataConnection, IsolationLevel transactionLevel = IsolationLevel.Serializable)
: base(dataConnection) =>
TransactionLevel = transactionLevel;

/// <summary>
/// Gets the isolation level of the transaction.
/// </summary>
protected IsolationLevel TransactionLevel { get; }

/// <summary>
/// Commits the internal transaction.
/// </summary>
public Task SaveChangesAsync(CancellationToken cancellationToken = default) => DataConnection.CommitTransactionAsync(cancellationToken);

/// <summary>
/// Checks if a transaction is present on the underlying data connection.
/// </summary>
bool IInitializeAsync.IsInitialized => DataConnection.Transaction != null;
: base(dataConnection, transactionLevel) { }

/// <summary>
/// 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 <see cref="SessionFactory{T}" /> which
/// in turn calls InitializeAsync.
/// Commits the internal transaction if possible.
/// </summary>
Task IInitializeAsync.InitializeAsync() => DataConnection.BeginTransactionAsync(TransactionLevel);
public Task SaveChangesAsync(CancellationToken cancellationToken = default) =>
TransactionLevel != IsolationLevel.Unspecified ?
DataConnection.CommitTransactionAsync(cancellationToken) :
Task.CompletedTask;
}

/// <summary>
Expand Down
8 changes: 4 additions & 4 deletions Code/src/Synnotech.Linq2Db/Synnotech.Linq2Db.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@

<PropertyGroup>
<PackageReleaseNotes>
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&lt;T&gt;
- 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
</PackageReleaseNotes>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="linq2db" Version="3.4.3" />
<PackageReference Include="linq2db" Version="3.4.5" />
<PackageReference Include="Light.GuardClauses" Version="9.0.0" />
<PackageReference Include="Synnotech.DatabaseAbstractions" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
Expand Down
4 changes: 2 additions & 2 deletions Code/tests/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="FluentAssertions" Version="6.1.0" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;
using FluentAssertions;
using LinqToDB;
Expand Down Expand Up @@ -42,6 +43,20 @@ public async Task LoadDataWithSessionFactory()
CheckLoadedEmployees(employees);
}

[SkippableFact]
public async Task LoadDataWithExplicitTransaction()
{
SkipTestIfNecessary();

var sessionFactory = PrepareContainer().AddSessionFactoryFor<IEmployeeSession, SessionWithTransactions>()
.BuildServiceProvider()
.GetRequiredService<ISessionFactory<IEmployeeSession>>();
await using var session = await sessionFactory.OpenSessionAsync();
var employees = await session.GetEmployeesAsync();

CheckLoadedEmployees(employees);
}

private static void CheckLoadedEmployees(List<Employee>? employees)
{
var expectedEmployees = new[]
Expand All @@ -64,5 +79,12 @@ public EmployeeSession(DataConnection dataConnection) : base(dataConnection) { }

public Task<List<Employee>> GetEmployeesAsync() => DataConnection.GetTable<Employee>().ToListAsync();
}

private sealed class SessionWithTransactions : AsyncReadOnlySession, IEmployeeSession
{
public SessionWithTransactions(DataConnection dataConnection) : base(dataConnection, IsolationLevel.ReadUncommitted) { }

public Task<List<Employee>> GetEmployeesAsync() => DataConnection.GetTable<Employee>().ToListAsync();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,9 @@ public static void MustImplementIAsyncDisposable() =>
[Fact]
public static void MustImplementIAsyncReadOnlySession() =>
typeof(AsyncReadOnlySession<>).Should().Implement<IAsyncReadOnlySession>();

[Fact]
public static void MustImplementIInitializeAsync() =>
typeof(AsyncReadOnlySession<>).Should().Implement<IInitializeAsync>();
}
}
4 changes: 4 additions & 0 deletions Code/tests/Synnotech.Linq2Db.Tests/AsyncSessionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,9 @@ public static class AsyncSessionTests
[Fact]
public static void MustImplementIAsyncSession() =>
typeof(AsyncSession<>).Should().Implement<IAsyncSession>();

[Fact]
public static void MustDeriveFromAsyncReadOnlySession() =>
typeof(AsyncSession<>).Should().BeDerivedFrom(typeof(AsyncReadOnlySession<>));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,9 @@ public static class AsyncTransactionalSessionTests
[Fact]
public static void MustImplementITransactionalSession() =>
typeof(AsyncTransactionalSession<>).Should().Implement<IAsyncTransactionalSession>();

[Fact]
public static void MustDeriveFromAsyncReadOnlySession() =>
typeof(AsyncTransactionalSession<>).Should().BeDerivedFrom(typeof(AsyncReadOnlySession<>));
}
}
31 changes: 30 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -296,6 +296,35 @@ When you register a session factory using `services.AddSessionFactoryFor`, you h
- `factoryLifetime`: the life time of the `SessionFactory<T>`. 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<TSessionAbstraction>` 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<T>` 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<T>` (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<IMySession>`:

```csharp
// In your composition root:
services.AddSessionFactoryFor<IMySession, MySession>();

// 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.
Expand Down

0 comments on commit d868823

Please sign in to comment.