Browse Source

Update OpenIddictApplicationStore/OpenIddictAuthorizationStore.DeleteAsync() to use serializable transactions

pull/575/head
Kévin Chalet 8 years ago
parent
commit
cfcba5f79f
  1. 3
      src/OpenIddict.EntityFramework/OpenIddictExtensions.cs
  2. 49
      src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs
  3. 36
      src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs
  4. 3
      src/OpenIddict.EntityFrameworkCore/OpenIddictExtensions.cs
  5. 61
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs
  6. 46
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs

3
src/OpenIddict.EntityFramework/OpenIddictExtensions.cs

@ -236,7 +236,8 @@ namespace Microsoft.Extensions.DependencyInjection
builder.Entity<TAuthorization>()
.HasMany(application => application.Tokens)
.WithOptional(token => token.Authorization)
.Map(association => association.MapKey("AuthorizationId"));
.Map(association => association.MapKey("AuthorizationId"))
.WillCascadeOnDelete();
builder.Entity<TAuthorization>()
.ToTable("OpenIddictAuthorizations");

49
src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.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;
@ -161,6 +162,19 @@ namespace OpenIddict.EntityFramework
throw new ArgumentNullException(nameof(application));
}
DbContextTransaction CreateTransaction()
{
try
{
return Context.Database.BeginTransaction(IsolationLevel.Serializable);
}
catch
{
return null;
}
}
Task<List<TAuthorization>> ListAuthorizationsAsync()
=> (from authorization in Authorizations.Include(authorization => authorization.Tokens)
where authorization.Application.Id.Equals(application.Id)
@ -172,27 +186,34 @@ namespace OpenIddict.EntityFramework
where token.Application.Id.Equals(application.Id)
select token).ToListAsync(cancellationToken);
// Remove all the authorizations associated with the application and
// the tokens attached to these implicit or explicit authorizations.
foreach (var authorization in await ListAuthorizationsAsync())
// 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 = CreateTransaction())
{
foreach (var token in authorization.Tokens)
// Remove all the authorizations associated with the application and
// the tokens attached to these implicit or explicit authorizations.
foreach (var authorization in await ListAuthorizationsAsync())
{
foreach (var token in authorization.Tokens)
{
Tokens.Remove(token);
}
Authorizations.Remove(authorization);
}
// Remove all the tokens associated with the application.
foreach (var token in await ListTokensAsync())
{
Tokens.Remove(token);
}
Authorizations.Remove(authorization);
}
Applications.Remove(application);
// Remove all the tokens associated with the application.
foreach (var token in await ListTokensAsync())
{
Tokens.Remove(token);
await Context.SaveChangesAsync(cancellationToken);
transaction?.Commit();
}
Applications.Remove(application);
await Context.SaveChangesAsync(cancellationToken);
}
/// <summary>

36
src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs

@ -162,20 +162,40 @@ namespace OpenIddict.EntityFramework
throw new ArgumentNullException(nameof(authorization));
}
DbContextTransaction CreateTransaction()
{
try
{
return Context.Database.BeginTransaction(IsolationLevel.Serializable);
}
catch
{
return null;
}
}
Task<List<TToken>> ListTokensAsync()
=> (from token in Tokens
where token.Authorization.Id.Equals(authorization.Id)
select token).ToListAsync(cancellationToken);
// Remove all the tokens associated with the authorization.
foreach (var token in await ListTokensAsync())
// 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 = CreateTransaction())
{
Tokens.Remove(token);
}
// Remove all the tokens associated with the authorization.
foreach (var token in await ListTokensAsync())
{
Tokens.Remove(token);
}
Authorizations.Remove(authorization);
Authorizations.Remove(authorization);
await Context.SaveChangesAsync(cancellationToken);
await Context.SaveChangesAsync(cancellationToken);
transaction?.Commit();
}
}
/// <summary>
@ -347,6 +367,10 @@ namespace OpenIddict.EntityFramework
break;
}
// Note: new tokens may be attached after the authorizations were retrieved
// from the database since the transaction level is deliberately limited to
// repeatable read instead of serializable for performance reasons). In this
// case, the operation will fail, which is considered an acceptable risk.
Authorizations.RemoveRange(authorizations);
Tokens.RemoveRange(authorizations.SelectMany(authorization => authorization.Tokens));

3
src/OpenIddict.EntityFrameworkCore/OpenIddictExtensions.cs

@ -264,7 +264,8 @@ namespace Microsoft.Extensions.DependencyInjection
entity.HasMany(authorization => authorization.Tokens)
.WithOne(token => token.Authorization)
.HasForeignKey("AuthorizationId")
.IsRequired(required: false);
.IsRequired(required: false)
.OnDelete(DeleteBehavior.Cascade);
entity.ToTable("OpenIddictAuthorizations");
});

61
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.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;
@ -161,6 +164,29 @@ namespace OpenIddict.EntityFrameworkCore
throw new ArgumentNullException(nameof(application));
}
async Task<IDbContextTransaction> 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<IDbContextTransactionManager>();
if (manager is IRelationalTransactionManager)
{
try
{
return await Context.Database.BeginTransactionAsync(IsolationLevel.Serializable, cancellationToken);
}
catch
{
return null;
}
}
return null;
}
// Note: due to a bug in Entity Framework Core's query visitor, the authorizations can't be
// filtered using authorization.Application.Id.Equals(key). To work around this issue,
// this local method uses an explicit join before applying the equality check.
@ -184,27 +210,34 @@ namespace OpenIddict.EntityFrameworkCore
where element.Id.Equals(application.Id)
select token).ToListAsync(cancellationToken);
// Remove all the authorizations associated with the application and
// the tokens attached to these implicit or explicit authorizations.
foreach (var authorization in await ListAuthorizationsAsync())
// 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 CreateTransactionAsync())
{
foreach (var token in authorization.Tokens)
// Remove all the authorizations associated with the application and
// the tokens attached to these implicit or explicit authorizations.
foreach (var authorization in await ListAuthorizationsAsync())
{
foreach (var token in authorization.Tokens)
{
Context.Remove(token);
}
Context.Remove(authorization);
}
// Remove all the tokens associated with the application.
foreach (var token in await ListTokensAsync())
{
Context.Remove(token);
}
Context.Remove(authorization);
}
Context.Remove(application);
// Remove all the tokens associated with the application.
foreach (var token in await ListTokensAsync())
{
Context.Remove(token);
await Context.SaveChangesAsync(cancellationToken);
transaction?.Commit();
}
Context.Remove(application);
await Context.SaveChangesAsync(cancellationToken);
}
/// <summary>

46
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs

@ -164,6 +164,29 @@ namespace OpenIddict.EntityFrameworkCore
throw new ArgumentNullException(nameof(authorization));
}
async Task<IDbContextTransaction> 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<IDbContextTransactionManager>();
if (manager is IRelationalTransactionManager)
{
try
{
return await Context.Database.BeginTransactionAsync(IsolationLevel.Serializable, cancellationToken);
}
catch
{
return null;
}
}
return null;
}
// Note: due to a bug in Entity Framework Core's query visitor, the tokens can't be
// filtered using token.Application.Id.Equals(key). To work around this issue,
// this local method uses an explicit join before applying the equality check.
@ -175,15 +198,22 @@ namespace OpenIddict.EntityFrameworkCore
where element.Id.Equals(authorization.Id)
select token).ToListAsync(cancellationToken);
// Remove all the tokens associated with the authorization.
foreach (var token in await ListTokensAsync())
// 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 CreateTransactionAsync())
{
Context.Remove(token);
}
// Remove all the tokens associated with the authorization.
foreach (var token in await ListTokensAsync())
{
Context.Remove(token);
}
Context.Remove(authorization);
Context.Remove(authorization);
await Context.SaveChangesAsync(cancellationToken);
await Context.SaveChangesAsync(cancellationToken);
transaction?.Commit();
}
}
/// <summary>
@ -511,6 +541,10 @@ namespace OpenIddict.EntityFrameworkCore
break;
}
// Note: new tokens may be attached after the authorizations were retrieved
// from the database since the transaction level is deliberately limited to
// repeatable read instead of serializable for performance reasons). In this
// case, the operation will fail, which is considered an acceptable risk.
Context.RemoveRange(authorizations);
Context.RemoveRange(authorizations.SelectMany(authorization => authorization.Tokens));

Loading…
Cancel
Save