diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
index b12b6916..c362a269 100644
--- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
+++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
@@ -699,23 +699,6 @@ namespace OpenIddict.Core
return Store.ListAsync(query, state, cancellationToken);
}
- ///
- /// Lists the ad-hoc authorizations that are marked as invalid or have no
- /// valid token attached and that can be safely removed from the database.
- ///
- /// The number of results to return.
- /// The number of results to skip.
- /// The that can be used to abort the operation.
- ///
- /// A that can be used to monitor the asynchronous operation,
- /// whose result returns all the elements returned when executing the specified query.
- ///
- public virtual Task> ListInvalidAsync(
- [CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken = default)
- {
- return Store.ListInvalidAsync(count, offset, cancellationToken);
- }
-
///
/// Removes the ad-hoc authorizations that are marked as invalid or have no valid token attached.
///
@@ -723,51 +706,8 @@ namespace OpenIddict.Core
///
/// A that can be used to monitor the asynchronous operation.
///
- public virtual async Task PruneInvalidAsync(CancellationToken cancellationToken = default)
- {
- IList exceptions = null;
- var authorizations = new List();
-
- // First, start retrieving the invalid authorizations from the database.
- for (var offset = 0; offset < 10_000; offset = offset + 100)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- var results = await ListInvalidAsync(100, offset, cancellationToken);
- if (results.IsEmpty)
- {
- break;
- }
-
- authorizations.AddRange(results);
- }
-
- // Then, remove the invalid authorizations one by one.
- foreach (var authorization in authorizations)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- try
- {
- await DeleteAsync(authorization, cancellationToken);
- }
-
- catch (Exception exception)
- {
- if (exceptions == null)
- {
- exceptions = new List(capacity: 1);
- }
-
- exceptions.Add(exception);
- }
- }
-
- if (exceptions != null)
- {
- throw new AggregateException("An error occurred while pruning authorizations.", exceptions);
- }
- }
+ public virtual Task PruneAsync(CancellationToken cancellationToken = default)
+ => Store.PruneAsync(cancellationToken);
///
/// Revokes an authorization.
diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
index f06c48d6..6dc9cad0 100644
--- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
+++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
@@ -589,23 +589,6 @@ namespace OpenIddict.Core
return Store.ListAsync(query, state, cancellationToken);
}
- ///
- /// Lists the tokens that are marked as expired or invalid
- /// and that can be safely removed from the database.
- ///
- /// The number of results to return.
- /// The number of results to skip.
- /// The that can be used to abort the operation.
- ///
- /// A that can be used to monitor the asynchronous operation,
- /// whose result returns all the elements returned when executing the specified query.
- ///
- public virtual Task> ListInvalidAsync(
- [CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken = default)
- {
- return Store.ListInvalidAsync(count, offset, cancellationToken);
- }
-
///
/// Obfuscates the specified reference identifier so it can be safely stored in a database.
/// By default, this method returns a simple hashed representation computed using SHA256.
@@ -637,51 +620,8 @@ namespace OpenIddict.Core
///
/// A that can be used to monitor the asynchronous operation.
///
- public virtual async Task PruneInvalidAsync(CancellationToken cancellationToken = default)
- {
- IList exceptions = null;
- var tokens = new List();
-
- // First, start retrieving the invalid tokens from the database.
- for (var offset = 0; offset < 10_000; offset = offset + 100)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- var results = await ListInvalidAsync(100, offset, cancellationToken);
- if (results.IsEmpty)
- {
- break;
- }
-
- tokens.AddRange(results);
- }
-
- // Then, remove the invalid tokens one by one.
- foreach (var token in tokens)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- try
- {
- await DeleteAsync(token, cancellationToken);
- }
-
- catch (Exception exception)
- {
- if (exceptions == null)
- {
- exceptions = new List(capacity: 1);
- }
-
- exceptions.Add(exception);
- }
- }
-
- if (exceptions != null)
- {
- throw new AggregateException("An error occurred while pruning tokens.", exceptions);
- }
- }
+ public virtual Task PruneAsync(CancellationToken cancellationToken = default)
+ => Store.PruneAsync(cancellationToken);
///
/// Redeems a token.
diff --git a/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs b/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs
index 7c15d92c..38073448 100644
--- a/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs
+++ b/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs
@@ -261,17 +261,13 @@ namespace OpenIddict.Core
[CanBeNull] TState state, CancellationToken cancellationToken);
///
- /// Lists the ad-hoc authorizations that are marked as invalid or have no
- /// valid token attached and that can be safely removed from the database.
+ /// Removes the ad-hoc authorizations that are marked as invalid or have no valid token attached.
///
- /// The number of results to return.
- /// The number of results to skip.
/// The that can be used to abort the operation.
///
- /// A that can be used to monitor the asynchronous operation,
- /// whose result returns all the elements returned when executing the specified query.
+ /// A that can be used to monitor the asynchronous operation.
///
- Task> ListInvalidAsync([CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken);
+ Task PruneAsync(CancellationToken cancellationToken);
///
/// Sets the application identifier associated with an authorization.
diff --git a/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs b/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs
index 11ca3528..57daeb73 100644
--- a/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs
+++ b/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs
@@ -294,17 +294,13 @@ namespace OpenIddict.Core
[CanBeNull] TState state, CancellationToken cancellationToken);
///
- /// Lists the tokens that are marked as expired or invalid
- /// and that can be safely removed from the database.
+ /// Removes the tokens that are marked as expired or invalid.
///
- /// The number of results to return.
- /// The number of results to skip.
/// The that can be used to abort the operation.
///
- /// A that can be used to monitor the asynchronous operation,
- /// whose result returns all the elements returned when executing the specified query.
+ /// A that can be used to monitor the asynchronous operation.
///
- Task> ListInvalidAsync([CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken);
+ Task PruneAsync(CancellationToken cancellationToken);
///
/// Sets the application identifier associated with a token.
diff --git a/src/OpenIddict.Core/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.Core/Stores/OpenIddictAuthorizationStore.cs
index c30556bc..2d0689cf 100644
--- a/src/OpenIddict.Core/Stores/OpenIddictAuthorizationStore.cs
+++ b/src/OpenIddict.Core/Stores/OpenIddictAuthorizationStore.cs
@@ -499,44 +499,13 @@ namespace OpenIddict.Core
[CanBeNull] TState state, CancellationToken cancellationToken);
///
- /// Lists the ad-hoc authorizations that are marked as invalid or have no
- /// valid token attached and that can be safely removed from the database.
+ /// Removes the ad-hoc authorizations that are marked as invalid or have no valid token attached.
///
- /// The number of results to return.
- /// The number of results to skip.
/// The that can be used to abort the operation.
///
- /// A that can be used to monitor the asynchronous operation,
- /// whose result returns all the elements returned when executing the specified query.
+ /// A that can be used to monitor the asynchronous operation.
///
- public virtual Task> ListInvalidAsync([CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken)
- {
- IQueryable Query(IQueryable authorizations, int? skip, int? take)
- {
- var query = (from authorization in authorizations
- where authorization.Status != OpenIddictConstants.Statuses.Valid ||
- (authorization.Type == OpenIddictConstants.AuthorizationTypes.AdHoc &&
- !authorization.Tokens.Any(token => token.Status == OpenIddictConstants.Statuses.Valid))
- orderby authorization.Id
- select authorization).AsQueryable();
-
- if (skip.HasValue)
- {
- query = query.Skip(skip.Value);
- }
-
- if (take.HasValue)
- {
- query = query.Take(take.Value);
- }
-
- return query;
- }
-
- return ListAsync(
- (authorizations, state) => Query(authorizations, state.Key, state.Value),
- new KeyValuePair(offset, count), cancellationToken);
- }
+ public abstract Task PruneAsync(CancellationToken cancellationToken);
///
/// Sets the application identifier associated with an authorization.
diff --git a/src/OpenIddict.Core/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.Core/Stores/OpenIddictTokenStore.cs
index 440493fd..d969fdaa 100644
--- a/src/OpenIddict.Core/Stores/OpenIddictTokenStore.cs
+++ b/src/OpenIddict.Core/Stores/OpenIddictTokenStore.cs
@@ -529,43 +529,13 @@ namespace OpenIddict.Core
[CanBeNull] TState state, CancellationToken cancellationToken);
///
- /// Lists the tokens that are marked as expired or invalid
- /// and that can be safely removed from the database.
+ /// Removes the tokens that are marked as expired or invalid.
///
- /// The number of results to return.
- /// The number of results to skip.
/// The that can be used to abort the operation.
///
- /// A that can be used to monitor the asynchronous operation,
- /// whose result returns all the elements returned when executing the specified query.
+ /// A that can be used to monitor the asynchronous operation.
///
- public virtual Task> ListInvalidAsync([CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken)
- {
- IQueryable Query(IQueryable tokens, int? skip, int? take)
- {
- var query = (from token in tokens
- where token.ExpirationDate < DateTimeOffset.UtcNow ||
- token.Status != OpenIddictConstants.Statuses.Valid
- orderby token.Id
- select token).AsQueryable();
-
- if (skip.HasValue)
- {
- query = query.Skip(skip.Value);
- }
-
- if (take.HasValue)
- {
- query = query.Take(take.Value);
- }
-
- return query;
- }
-
- return ListAsync(
- (tokens, state) => Query(tokens, state.Key, state.Value),
- new KeyValuePair(offset, count), cancellationToken);
- }
+ public abstract Task PruneAsync(CancellationToken cancellationToken);
///
/// Sets the authorization identifier associated with a token.
diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs
index df50b5c5..dc436919 100644
--- a/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs
+++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs
@@ -7,6 +7,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
+using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Threading;
@@ -291,6 +292,88 @@ namespace OpenIddict.EntityFramework
Authorizations.Include(authorization => authorization.Application), state).ToListAsync(cancellationToken));
}
+ ///
+ /// Removes the ad-hoc authorizations that are marked as invalid or have no valid token attached.
+ ///
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ public override async Task PruneAsync(CancellationToken cancellationToken = default)
+ {
+ // Note: Entity Framework 6.x doesn't support set-based deletes, which prevents removing
+ // entities in a single command without having to retrieve and materialize them first.
+ // To work around this limitation, entities are manually listed and deleted using a batch logic.
+
+ IList exceptions = null;
+
+ IQueryable Query(IQueryable authorizations, int offset)
+ => (from authorization in authorizations.Include(authorization => authorization.Tokens)
+ where authorization.Status != OpenIddictConstants.Statuses.Valid ||
+ (authorization.Type == OpenIddictConstants.AuthorizationTypes.AdHoc &&
+ !authorization.Tokens.Any(token => token.Status == OpenIddictConstants.Statuses.Valid))
+ orderby authorization.Id
+ select authorization).Skip(offset).Take(1_000);
+
+ DbContextTransaction CreateTransaction()
+ {
+ // Note: relational providers like Sqlite are known to lack proper support
+ // for repeatable read transactions. To ensure this method can be safely used
+ // with such providers, the database transaction is created in a try/catch block.
+ try
+ {
+ return Context.Database.BeginTransaction(IsolationLevel.RepeatableRead);
+ }
+
+ catch
+ {
+ return null;
+ }
+ }
+
+ for (var offset = 0; offset < 100_000; offset = offset + 1_000)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // To prevent concurrency exceptions from being thrown if an entry is modified
+ // 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 = CreateTransaction())
+ {
+ var authorizations = await ListAsync((source, state) => Query(source, state), offset, cancellationToken);
+ if (authorizations.IsEmpty)
+ {
+ break;
+ }
+
+ Authorizations.RemoveRange(authorizations);
+ Tokens.RemoveRange(authorizations.SelectMany(authorization => authorization.Tokens));
+
+ try
+ {
+ await Context.SaveChangesAsync(cancellationToken);
+ transaction?.Commit();
+ }
+
+ catch (Exception exception)
+ {
+ if (exceptions == null)
+ {
+ exceptions = new List(capacity: 1);
+ }
+
+ exceptions.Add(exception);
+ }
+ }
+ }
+
+ if (exceptions != null)
+ {
+ throw new AggregateException("An error occurred while pruning authorizations.", exceptions);
+ }
+ }
+
///
/// Sets the application identifier associated with an authorization.
///
diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs
index 14873f05..bc34ecc9 100644
--- a/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs
+++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs
@@ -5,7 +5,9 @@
*/
using System;
+using System.Collections.Generic;
using System.Collections.Immutable;
+using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Threading;
@@ -316,6 +318,86 @@ namespace OpenIddict.EntityFramework
.Include(token => token.Authorization), state).ToListAsync(cancellationToken));
}
+ ///
+ /// Removes the tokens that are marked as expired or invalid.
+ ///
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ public override async Task PruneAsync(CancellationToken cancellationToken = default)
+ {
+ // Note: Entity Framework 6.x doesn't support set-based deletes, which prevents removing
+ // entities in a single command without having to retrieve and materialize them first.
+ // To work around this limitation, entities are manually listed and deleted using a batch logic.
+
+ IList exceptions = null;
+
+ IQueryable Query(IQueryable tokens, int offset)
+ => (from token in tokens
+ where token.ExpirationDate < DateTimeOffset.UtcNow ||
+ token.Status != OpenIddictConstants.Statuses.Valid
+ orderby token.Id
+ select token).Skip(offset).Take(1_000);
+
+ DbContextTransaction CreateTransaction()
+ {
+ // Note: relational providers like Sqlite are known to lack proper support
+ // for repeatable read transactions. To ensure this method can be safely used
+ // with such providers, the database transaction is created in a try/catch block.
+ try
+ {
+ return Context.Database.BeginTransaction(IsolationLevel.RepeatableRead);
+ }
+
+ catch
+ {
+ return null;
+ }
+ }
+
+ for (var offset = 0; offset < 100_000; offset = offset + 1_000)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // To prevent concurrency exceptions from being thrown if an entry is modified
+ // 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 = CreateTransaction())
+ {
+ var tokens = await ListAsync((source, state) => Query(source, state), offset, cancellationToken);
+ if (tokens.IsEmpty)
+ {
+ break;
+ }
+
+ Tokens.RemoveRange(tokens);
+
+ try
+ {
+ await Context.SaveChangesAsync(cancellationToken);
+ transaction?.Commit();
+ }
+
+ catch (Exception exception)
+ {
+ if (exceptions == null)
+ {
+ exceptions = new List(capacity: 1);
+ }
+
+ exceptions.Add(exception);
+ }
+ }
+ }
+
+ if (exceptions != null)
+ {
+ throw new AggregateException("An error occurred while pruning tokens.", exceptions);
+ }
+ }
+
///
/// Sets the application identifier associated with a token.
///
diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs
index ea2b2b20..51b9caf9 100644
--- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs
+++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs
@@ -7,11 +7,14 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
+using System.Data;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Caching.Memory;
using OpenIddict.Core;
using OpenIddict.Models;
@@ -405,6 +408,98 @@ namespace OpenIddict.EntityFrameworkCore
Authorizations.Include(authorization => authorization.Application), state).ToListAsync(cancellationToken));
}
+ ///
+ /// Removes the ad-hoc authorizations that are marked as invalid or have no valid token attached.
+ ///
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ public override async Task PruneAsync(CancellationToken cancellationToken = default)
+ {
+ // Note: Entity Framework Core doesn't support set-based deletes, which prevents removing
+ // entities in a single command without having to retrieve and materialize them first.
+ // To work around this limitation, entities are manually listed and deleted using a batch logic.
+
+ IList exceptions = null;
+
+ IQueryable Query(IQueryable authorizations, int offset)
+ => (from authorization in authorizations.Include(authorization => authorization.Tokens)
+ where authorization.Status != OpenIddictConstants.Statuses.Valid ||
+ (authorization.Type == OpenIddictConstants.AuthorizationTypes.AdHoc &&
+ !authorization.Tokens.Any(token => token.Status == OpenIddictConstants.Statuses.Valid))
+ orderby authorization.Id
+ select authorization).Skip(offset).Take(1_000);
+
+ async Task CreateTransactionAsync()
+ {
+ // 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)
+ {
+ // Note: relational providers like Sqlite are known to lack proper support
+ // for repeatable read transactions. To ensure this method can be safely used
+ // with such providers, the database transaction is created in a try/catch block.
+ try
+ {
+ return await Context.Database.BeginTransactionAsync(IsolationLevel.RepeatableRead, cancellationToken);
+ }
+
+ catch
+ {
+ return null;
+ }
+ }
+
+ return null;
+ }
+
+ for (var offset = 0; offset < 100_000; offset = offset + 1_000)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // To prevent concurrency exceptions from being thrown if an entry is modified
+ // 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 CreateTransactionAsync())
+ {
+ var authorizations = await ListAsync((source, state) => Query(source, state), offset, cancellationToken);
+ if (authorizations.IsEmpty)
+ {
+ break;
+ }
+
+ Context.RemoveRange(authorizations);
+ Context.RemoveRange(authorizations.SelectMany(authorization => authorization.Tokens));
+
+ try
+ {
+ await Context.SaveChangesAsync(cancellationToken);
+ transaction?.Commit();
+ }
+
+ catch (Exception exception)
+ {
+ if (exceptions == null)
+ {
+ exceptions = new List(capacity: 1);
+ }
+
+ exceptions.Add(exception);
+ }
+ }
+ }
+
+ if (exceptions != null)
+ {
+ throw new AggregateException("An error occurred while pruning authorizations.", exceptions);
+ }
+ }
+
///
/// Sets the application identifier associated with an authorization.
///
diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs
index c30975ad..ffdc45f0 100644
--- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs
+++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs
@@ -5,12 +5,16 @@
*/
using System;
+using System.Collections.Generic;
using System.Collections.Immutable;
+using System.Data;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Caching.Memory;
using OpenIddict.Core;
using OpenIddict.Models;
@@ -306,6 +310,96 @@ namespace OpenIddict.EntityFrameworkCore
.Include(token => token.Authorization), state).ToListAsync(cancellationToken));
}
+ ///
+ /// Removes the tokens that are marked as expired or invalid.
+ ///
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ public override async Task PruneAsync(CancellationToken cancellationToken = default)
+ {
+ // Note: Entity Framework Core doesn't support set-based deletes, which prevents removing
+ // entities in a single command without having to retrieve and materialize them first.
+ // To work around this limitation, entities are manually listed and deleted using a batch logic.
+
+ IList exceptions = null;
+
+ IQueryable Query(IQueryable tokens, int offset)
+ => (from token in tokens
+ where token.ExpirationDate < DateTimeOffset.UtcNow ||
+ token.Status != OpenIddictConstants.Statuses.Valid
+ orderby token.Id
+ select token).Skip(offset).Take(1_000);
+
+ async Task CreateTransactionAsync()
+ {
+ // 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)
+ {
+ // Note: relational providers like Sqlite are known to lack proper support
+ // for repeatable read transactions. To ensure this method can be safely used
+ // with such providers, the database transaction is created in a try/catch block.
+ try
+ {
+ return await Context.Database.BeginTransactionAsync(IsolationLevel.RepeatableRead, cancellationToken);
+ }
+
+ catch
+ {
+ return null;
+ }
+ }
+
+ return null;
+ }
+
+ for (var offset = 0; offset < 100_000; offset = offset + 1_000)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // To prevent concurrency exceptions from being thrown if an entry is modified
+ // 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 CreateTransactionAsync())
+ {
+ var tokens = await ListAsync((source, state) => Query(source, state), offset, cancellationToken);
+ if (tokens.IsEmpty)
+ {
+ break;
+ }
+
+ Context.RemoveRange(tokens);
+
+ try
+ {
+ await Context.SaveChangesAsync(cancellationToken);
+ transaction?.Commit();
+ }
+
+ catch (Exception exception)
+ {
+ if (exceptions == null)
+ {
+ exceptions = new List(capacity: 1);
+ }
+
+ exceptions.Add(exception);
+ }
+ }
+ }
+
+ if (exceptions != null)
+ {
+ throw new AggregateException("An error occurred while pruning tokens.", exceptions);
+ }
+ }
+
///
/// Sets the application identifier associated with a token.
///