Browse Source

Update the Entity Framework Core stores to dispose and commit transactions asynchronously

pull/2516/head
Kévin Chalet 2 weeks ago
parent
commit
aa4f2fd554
  1. 3
      src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs
  2. 34
      src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs
  3. 3
      src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreOptions.cs
  4. 67
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs
  5. 60
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs
  6. 42
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs

3
src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs

@ -60,9 +60,6 @@ public sealed class OpenIddictEntityFrameworkCoreBuilder
/// <summary>
/// Prevents the Entity Framework Core stores from using bulk operations.
/// </summary>
/// <remarks>
/// Note: bulk operations are only supported when targeting .NET 7.0 and higher.
/// </remarks>
/// <returns>The <see cref="OpenIddictEntityFrameworkCoreBuilder"/> instance.</returns>
public OpenIddictEntityFrameworkCoreBuilder DisableBulkOperations()
=> Configure(options => options.DisableBulkOperations = true);

34
src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs

@ -4,7 +4,6 @@
* the license and the contributors participating to this project.
*/
using System.Data;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
using OpenIddict.EntityFrameworkCore;
@ -214,37 +213,4 @@ public static class OpenIddictEntityFrameworkCoreHelpers
.ApplyConfiguration(new OpenIddictEntityFrameworkCoreScopeConfiguration<TScope, TKey>())
.ApplyConfiguration(new OpenIddictEntityFrameworkCoreTokenConfiguration<TToken, TApplication, TAuthorization, TKey>());
}
/// <summary>
/// Tries to create a new <see cref="IDbContextTransaction"/> with the specified <paramref name="level"/>.
/// </summary>
/// <param name="context">The Entity Framework Core context.</param>
/// <param name="level">The desired level of isolation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The <see cref="IDbContextTransaction"/> if it could be created, <see langword="null"/> otherwise.</returns>
internal static async ValueTask<IDbContextTransaction?> CreateTransactionAsync(
this DbContext context, IsolationLevel level, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
// Note: transactions that specify an explicit isolation level are only supported by
// relational providers and trying to use them with a different provider results in
// an invalid operation exception being thrown at runtime. To prevent that, a manual
// check is made to ensure the underlying transaction manager is relational.
var manager = context.Database.GetService<IDbContextTransactionManager>();
if (manager is IRelationalTransactionManager)
{
try
{
return await context.Database.BeginTransactionAsync(level, cancellationToken);
}
catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception))
{
return null;
}
}
return null;
}
}

3
src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreOptions.cs

@ -15,8 +15,5 @@ public sealed class OpenIddictEntityFrameworkCoreOptions
/// <summary>
/// Gets or sets a boolean indicating whether bulk operations should be disabled.
/// </summary>
/// <remarks>
/// Note: bulk operations are only supported when targeting .NET 7.0 and higher.
/// </remarks>
public bool DisableBulkOperations { get; set; }
}

67
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs

@ -134,7 +134,8 @@ public class OpenIddictEntityFrameworkCoreApplicationStore<
// To prevent an SQL exception from being thrown if a new associated entity is
// created after the existing entries have been listed, the following logic is
// executed in a serializable transaction, that will lock the affected tables.
using var transaction = await context.CreateTransactionAsync(IsolationLevel.Serializable, cancellationToken);
await using var transaction = await CreateTransactionAsync(context,
IsolationLevel.Serializable, cancellationToken);
// Remove all the tokens associated with the application.
await (from token in context.Set<TToken>()
@ -155,7 +156,11 @@ public class OpenIddictEntityFrameworkCoreApplicationStore<
try
{
await context.SaveChangesAsync(cancellationToken);
transaction?.Commit();
if (transaction is not null)
{
await transaction.CommitAsync(cancellationToken);
}
}
catch (DbUpdateConcurrencyException exception)
@ -176,13 +181,15 @@ public class OpenIddictEntityFrameworkCoreApplicationStore<
// To prevent an SQL exception from being thrown if a new associated entity is
// created after the existing entries have been listed, the following logic is
// executed in a serializable transaction, that will lock the affected tables.
using var transaction = await context.CreateTransactionAsync(IsolationLevel.Serializable, cancellationToken);
await using var transaction = await CreateTransactionAsync(context,
IsolationLevel.Serializable, cancellationToken);
// Remove all the authorizations associated with the application and
// the tokens attached to these implicit or explicit authorizations.
var authorizations = await (from authorization in context.Set<TAuthorization>().Include(authorization => authorization.Tokens).AsTracking()
where authorization.Application!.Id!.Equals(application.Id)
select authorization).ToListAsync(cancellationToken);
var authorizations = await (
from authorization in context.Set<TAuthorization>().Include(authorization => authorization.Tokens).AsTracking()
where authorization.Application!.Id!.Equals(application.Id)
select authorization).ToListAsync(cancellationToken);
foreach (var authorization in authorizations)
{
@ -195,10 +202,11 @@ public class OpenIddictEntityFrameworkCoreApplicationStore<
}
// Remove all the tokens associated with the application.
var tokens = await (from token in context.Set<TToken>().AsTracking()
where token.Authorization == null
where token.Application!.Id!.Equals(application.Id)
select token).ToListAsync(cancellationToken);
var tokens = await (
from token in context.Set<TToken>().AsTracking()
where token.Authorization == null
where token.Application!.Id!.Equals(application.Id)
select token).ToListAsync(cancellationToken);
foreach (var token in tokens)
{
@ -210,7 +218,11 @@ public class OpenIddictEntityFrameworkCoreApplicationStore<
try
{
await context.SaveChangesAsync(cancellationToken);
transaction?.Commit();
if (transaction is not null)
{
await transaction.CommitAsync(cancellationToken);
}
}
catch (DbUpdateConcurrencyException exception)
@ -735,4 +747,37 @@ public class OpenIddictEntityFrameworkCoreApplicationStore<
return converter.ConvertToInvariantString(identifier);
}
}
/// <summary>
/// Tries to create a new <see cref="IDbContextTransaction"/> with the specified <paramref name="level"/>.
/// </summary>
/// <param name="context">The Entity Framework Core context.</param>
/// <param name="level">The desired level of isolation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The <see cref="IDbContextTransaction"/> if it could be created, <see langword="null"/> otherwise.</returns>
protected virtual async ValueTask<IDbContextTransaction?> CreateTransactionAsync(
DbContext context, IsolationLevel level, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
// Note: transactions that specify an explicit isolation level are only supported by
// relational providers and trying to use them with a different provider results in
// an invalid operation exception being thrown at runtime. To prevent that, a manual
// check is made to ensure the underlying transaction manager is relational.
var manager = context.GetService<IDbContextTransactionManager>();
if (manager is IRelationalTransactionManager)
{
try
{
return await context.Database.BeginTransactionAsync(level, cancellationToken);
}
catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception))
{
return null;
}
}
return null;
}
}

60
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs

@ -133,7 +133,8 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore<
// To prevent an SQL exception from being thrown if a new associated entity is
// created after the existing entries have been listed, the following logic is
// executed in a serializable transaction, that will lock the affected tables.
using var transaction = await context.CreateTransactionAsync(IsolationLevel.Serializable, cancellationToken);
await using var transaction = await CreateTransactionAsync(context,
IsolationLevel.Serializable, cancellationToken);
// Remove all the tokens associated with the authorization.
await (from token in context.Set<TToken>().AsTracking()
@ -148,7 +149,11 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore<
try
{
await context.SaveChangesAsync(cancellationToken);
transaction?.Commit();
if (transaction is not null)
{
await transaction.CommitAsync(cancellationToken);
}
}
catch (DbUpdateConcurrencyException exception)
@ -169,7 +174,8 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore<
// To prevent an SQL exception from being thrown if a new associated entity is
// created after the existing entries have been listed, the following logic is
// executed in a serializable transaction, that will lock the affected tables.
using var transaction = await context.CreateTransactionAsync(IsolationLevel.Serializable, cancellationToken);
await using var transaction = await CreateTransactionAsync(context,
IsolationLevel.Serializable, cancellationToken);
// Remove all the tokens associated with the authorization.
var tokens = await (from token in context.Set<TToken>().AsTracking()
@ -186,7 +192,11 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore<
try
{
await context.SaveChangesAsync(cancellationToken);
transaction?.Commit();
if (transaction is not null)
{
await transaction.CommitAsync(cancellationToken);
}
}
catch (DbUpdateConcurrencyException exception)
@ -524,7 +534,8 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore<
// after it was retrieved from the database, the following logic is executed in
// a repeatable read transaction, that will put a lock on the retrieved entries
// and thus prevent them from being concurrently modified outside this block.
using var transaction = await context.CreateTransactionAsync(IsolationLevel.RepeatableRead, cancellationToken);
await using var transaction = await CreateTransactionAsync(context,
IsolationLevel.RepeatableRead, cancellationToken);
var authorizations = await
(from authorization in context.Set<TAuthorization>().Include(authorization => authorization.Tokens).AsTracking()
@ -545,7 +556,11 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore<
try
{
await context.SaveChangesAsync(cancellationToken);
transaction?.Commit();
if (transaction is not null)
{
await transaction.CommitAsync(cancellationToken);
}
}
catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception))
@ -943,4 +958,37 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore<
return converter.ConvertToInvariantString(identifier);
}
}
/// <summary>
/// Tries to create a new <see cref="IDbContextTransaction"/> with the specified <paramref name="level"/>.
/// </summary>
/// <param name="context">The Entity Framework Core context.</param>
/// <param name="level">The desired level of isolation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The <see cref="IDbContextTransaction"/> if it could be created, <see langword="null"/> otherwise.</returns>
protected virtual async ValueTask<IDbContextTransaction?> CreateTransactionAsync(
DbContext context, IsolationLevel level, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
// Note: transactions that specify an explicit isolation level are only supported by
// relational providers and trying to use them with a different provider results in
// an invalid operation exception being thrown at runtime. To prevent that, a manual
// check is made to ensure the underlying transaction manager is relational.
var manager = context.GetService<IDbContextTransactionManager>();
if (manager is IRelationalTransactionManager)
{
try
{
return await context.Database.BeginTransactionAsync(level, cancellationToken);
}
catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception))
{
return null;
}
}
return null;
}
}

42
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs

@ -559,7 +559,8 @@ public class OpenIddictEntityFrameworkCoreTokenStore<
// after it was retrieved from the database, the following logic is executed in
// a repeatable read transaction, that will put a lock on the retrieved entries
// and thus prevent them from being concurrently modified outside this block.
using var transaction = await context.CreateTransactionAsync(IsolationLevel.RepeatableRead, cancellationToken);
await using var transaction = await CreateTransactionAsync(context,
IsolationLevel.RepeatableRead, cancellationToken);
var tokens = await
(from token in context.Set<TToken>().AsTracking()
@ -577,7 +578,11 @@ public class OpenIddictEntityFrameworkCoreTokenStore<
try
{
await context.SaveChangesAsync(cancellationToken);
transaction?.Commit();
if (transaction is not null)
{
await transaction.CommitAsync(cancellationToken);
}
}
catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception))
@ -1100,4 +1105,37 @@ public class OpenIddictEntityFrameworkCoreTokenStore<
return converter.ConvertToInvariantString(identifier);
}
}
/// <summary>
/// Tries to create a new <see cref="IDbContextTransaction"/> with the specified <paramref name="level"/>.
/// </summary>
/// <param name="context">The Entity Framework Core context.</param>
/// <param name="level">The desired level of isolation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The <see cref="IDbContextTransaction"/> if it could be created, <see langword="null"/> otherwise.</returns>
protected virtual async ValueTask<IDbContextTransaction?> CreateTransactionAsync(
DbContext context, IsolationLevel level, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
// Note: transactions that specify an explicit isolation level are only supported by
// relational providers and trying to use them with a different provider results in
// an invalid operation exception being thrown at runtime. To prevent that, a manual
// check is made to ensure the underlying transaction manager is relational.
var manager = context.GetService<IDbContextTransactionManager>();
if (manager is IRelationalTransactionManager)
{
try
{
return await context.Database.BeginTransactionAsync(level, cancellationToken);
}
catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception))
{
return null;
}
}
return null;
}
}

Loading…
Cancel
Save