From aa4f2fd55441ac071fb58f3e3edc6f6f35d645c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Sun, 26 Jul 2026 17:08:05 +0200 Subject: [PATCH] Update the Entity Framework Core stores to dispose and commit transactions asynchronously --- .../OpenIddictEntityFrameworkCoreBuilder.cs | 3 - .../OpenIddictEntityFrameworkCoreHelpers.cs | 34 ---------- .../OpenIddictEntityFrameworkCoreOptions.cs | 3 - ...dictEntityFrameworkCoreApplicationStore.cs | 67 ++++++++++++++++--- ...ctEntityFrameworkCoreAuthorizationStore.cs | 60 +++++++++++++++-- ...OpenIddictEntityFrameworkCoreTokenStore.cs | 42 +++++++++++- 6 files changed, 150 insertions(+), 59 deletions(-) diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs index a24b3799..1ff80c58 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs @@ -60,9 +60,6 @@ public sealed class OpenIddictEntityFrameworkCoreBuilder /// /// Prevents the Entity Framework Core stores from using bulk operations. /// - /// - /// Note: bulk operations are only supported when targeting .NET 7.0 and higher. - /// /// The instance. public OpenIddictEntityFrameworkCoreBuilder DisableBulkOperations() => Configure(options => options.DisableBulkOperations = true); diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs index 49f349bb..afa60cca 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs +++ b/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()) .ApplyConfiguration(new OpenIddictEntityFrameworkCoreTokenConfiguration()); } - - /// - /// Tries to create a new with the specified . - /// - /// The Entity Framework Core context. - /// The desired level of isolation. - /// The that can be used to abort the operation. - /// The if it could be created, otherwise. - internal static async ValueTask 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(); - if (manager is IRelationalTransactionManager) - { - try - { - return await context.Database.BeginTransactionAsync(level, cancellationToken); - } - - catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) - { - return null; - } - } - - return null; - } } diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreOptions.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreOptions.cs index 42cc0731..ef0ac88f 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreOptions.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreOptions.cs @@ -15,8 +15,5 @@ public sealed class OpenIddictEntityFrameworkCoreOptions /// /// Gets or sets a boolean indicating whether bulk operations should be disabled. /// - /// - /// Note: bulk operations are only supported when targeting .NET 7.0 and higher. - /// public bool DisableBulkOperations { get; set; } } diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs index 45074311..c43a71b4 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs +++ b/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() @@ -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().Include(authorization => authorization.Tokens).AsTracking() - where authorization.Application!.Id!.Equals(application.Id) - select authorization).ToListAsync(cancellationToken); + var authorizations = await ( + from authorization in context.Set().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().AsTracking() - where token.Authorization == null - where token.Application!.Id!.Equals(application.Id) - select token).ToListAsync(cancellationToken); + var tokens = await ( + from token in context.Set().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); } } + + /// + /// Tries to create a new with the specified . + /// + /// The Entity Framework Core context. + /// The desired level of isolation. + /// The that can be used to abort the operation. + /// The if it could be created, otherwise. + protected virtual async ValueTask 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(); + if (manager is IRelationalTransactionManager) + { + try + { + return await context.Database.BeginTransactionAsync(level, cancellationToken); + } + + catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) + { + return null; + } + } + + return null; + } } diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs index 9a2e1ef8..60ccc566 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs +++ b/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().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().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().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); } } + + /// + /// Tries to create a new with the specified . + /// + /// The Entity Framework Core context. + /// The desired level of isolation. + /// The that can be used to abort the operation. + /// The if it could be created, otherwise. + protected virtual async ValueTask 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(); + if (manager is IRelationalTransactionManager) + { + try + { + return await context.Database.BeginTransactionAsync(level, cancellationToken); + } + + catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) + { + return null; + } + } + + return null; + } } \ No newline at end of file diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs index 24d64199..f98a9848 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs +++ b/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().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); } } + + /// + /// Tries to create a new with the specified . + /// + /// The Entity Framework Core context. + /// The desired level of isolation. + /// The that can be used to abort the operation. + /// The if it could be created, otherwise. + protected virtual async ValueTask 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(); + if (manager is IRelationalTransactionManager) + { + try + { + return await context.Database.BeginTransactionAsync(level, cancellationToken); + } + + catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) + { + return null; + } + } + + return null; + } }