Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transactions for readonly sessions #3

Merged
merged 3 commits into from
Oct 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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