Browse Source

Backport the QueryTrackingBehavior.NoTracking support to OpenIddict 1.x

pull/670/head
Kévin Chalet 8 years ago
parent
commit
b12041654b
  1. 3
      src/OpenIddict.EntityFramework/OpenIddictExtensions.cs
  2. 50
      src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs
  3. 39
      src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs
  4. 6
      src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs
  5. 3
      src/OpenIddict.EntityFrameworkCore/OpenIddictExtensions.cs
  6. 74
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs
  7. 77
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs
  8. 4
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs
  9. 22
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.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");

50
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)
@ -168,30 +182,38 @@ namespace OpenIddict.EntityFramework
Task<List<TToken>> ListTokensAsync()
=> (from token in Tokens
where token.Authorization == null
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>

39
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 application.
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>
@ -353,6 +373,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));
@ -389,7 +413,8 @@ namespace OpenIddict.EntityFramework
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public override async Task SetApplicationIdAsync([NotNull] TAuthorization authorization, [CanBeNull] string identifier, CancellationToken cancellationToken)
public override async Task SetApplicationIdAsync([NotNull] TAuthorization authorization,
[CanBeNull] string identifier, CancellationToken cancellationToken)
{
if (authorization == null)
{

6
src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs

@ -419,7 +419,8 @@ namespace OpenIddict.EntityFramework
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public override async Task SetApplicationIdAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken)
public override async Task SetApplicationIdAsync([NotNull] TToken token,
[CanBeNull] string identifier, CancellationToken cancellationToken)
{
if (token == null)
{
@ -464,7 +465,8 @@ namespace OpenIddict.EntityFramework
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public override async Task SetAuthorizationIdAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken)
public override async Task SetAuthorizationIdAsync([NotNull] TToken token,
[CanBeNull] string identifier, CancellationToken cancellationToken)
{
if (token == null)
{

3
src/OpenIddict.EntityFrameworkCore/OpenIddictExtensions.cs

@ -268,7 +268,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");
});

74
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,14 +164,37 @@ 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.
// See https://github.com/openiddict/openiddict-core/issues/499 for more information.
Task<List<TAuthorization>> ListAuthorizationsAsync()
=> (from authorization in Authorizations.Include(authorization => authorization.Tokens)
join element in Applications on authorization.Application.Id equals element.Id
=> (from authorization in Authorizations.Include(authorization => authorization.Tokens).AsTracking()
join element in Applications.AsTracking() on authorization.Application.Id equals element.Id
where element.Id.Equals(application.Id)
select authorization).ToListAsync(cancellationToken);
@ -178,32 +204,40 @@ namespace OpenIddict.EntityFrameworkCore
// See https://github.com/openiddict/openiddict-core/issues/499 for more information.
Task<List<TToken>> ListTokensAsync()
=> (from token in Tokens
join element in Applications on token.Application.Id equals element.Id
=> (from token in Tokens.AsTracking()
where token.Authorization == null
join element in Applications.AsTracking() on token.Application.Id equals element.Id
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>
@ -227,7 +261,7 @@ namespace OpenIddict.EntityFrameworkCore
throw new ArgumentNullException(nameof(query));
}
return query(Applications, state).FirstOrDefaultAsync(cancellationToken);
return query(Applications.AsTracking(), state).FirstOrDefaultAsync(cancellationToken);
}
/// <summary>
@ -251,7 +285,7 @@ namespace OpenIddict.EntityFrameworkCore
throw new ArgumentNullException(nameof(query));
}
return ImmutableArray.CreateRange(await query(Applications, state).ToListAsync(cancellationToken));
return ImmutableArray.CreateRange(await query(Applications.AsTracking(), state).ToListAsync(cancellationToken));
}
/// <summary>

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

@ -164,26 +164,56 @@ 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.
// See https://github.com/openiddict/openiddict-core/issues/499 for more information.
Task<List<TToken>> ListTokensAsync()
=> (from token in Tokens
join element in Authorizations on token.Authorization.Id equals element.Id
=> (from token in Tokens.AsTracking()
join element in Authorizations.AsTracking() on token.Authorization.Id equals element.Id
where element.Id.Equals(authorization.Id)
select token).ToListAsync(cancellationToken);
// Remove all the tokens associated with the application.
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>
@ -217,9 +247,9 @@ namespace OpenIddict.EntityFrameworkCore
IQueryable<TAuthorization> Query(IQueryable<TAuthorization> authorizations,
IQueryable<TApplication> applications, TKey key, string principal)
=> from authorization in authorizations.Include(authorization => authorization.Application)
=> from authorization in authorizations.Include(authorization => authorization.Application).AsTracking()
where authorization.Subject == principal
join application in applications on authorization.Application.Id equals application.Id
join application in applications.AsTracking() on authorization.Application.Id equals application.Id
where application.Id.Equals(key)
select authorization;
@ -264,10 +294,9 @@ namespace OpenIddict.EntityFrameworkCore
IQueryable<TAuthorization> Query(IQueryable<TAuthorization> authorizations,
IQueryable<TApplication> applications, TKey key, string principal, string state)
=> from authorization in authorizations.Include(authorization => authorization.Application)
where authorization.Subject == principal &&
authorization.Status == state
join application in applications on authorization.Application.Id equals application.Id
=> from authorization in authorizations.Include(authorization => authorization.Application).AsTracking()
where authorization.Subject == principal && authorization.Status == state
join application in applications.AsTracking() on authorization.Application.Id equals application.Id
where application.Id.Equals(key)
select authorization;
@ -318,11 +347,11 @@ namespace OpenIddict.EntityFrameworkCore
IQueryable<TAuthorization> Query(IQueryable<TAuthorization> authorizations,
IQueryable<TApplication> applications, TKey key, string principal, string state, string kind)
=> from authorization in authorizations.Include(authorization => authorization.Application)
=> from authorization in authorizations.Include(authorization => authorization.Application).AsTracking()
where authorization.Subject == principal &&
authorization.Status == state &&
authorization.Type == kind
join application in applications on authorization.Application.Id equals application.Id
join application in applications.AsTracking() on authorization.Application.Id equals application.Id
where application.Id.Equals(key)
select authorization;
@ -380,7 +409,9 @@ namespace OpenIddict.EntityFrameworkCore
throw new ArgumentNullException(nameof(query));
}
return query(Authorizations.Include(authorization => authorization.Application), state).FirstOrDefaultAsync(cancellationToken);
return query(
Authorizations.Include(authorization => authorization.Application)
.AsTracking(), state).FirstOrDefaultAsync(cancellationToken);
}
/// <summary>
@ -405,7 +436,8 @@ namespace OpenIddict.EntityFrameworkCore
}
return ImmutableArray.CreateRange(await query(
Authorizations.Include(authorization => authorization.Application), state).ToListAsync(cancellationToken));
Authorizations.Include(authorization => authorization.Application)
.AsTracking(), state).ToListAsync(cancellationToken));
}
/// <summary>
@ -424,7 +456,7 @@ namespace OpenIddict.EntityFrameworkCore
IList<Exception> exceptions = null;
IQueryable<TAuthorization> Query(IQueryable<TAuthorization> authorizations, int offset)
=> (from authorization in authorizations.Include(authorization => authorization.Tokens)
=> (from authorization in authorizations.Include(authorization => authorization.Tokens).AsTracking()
where authorization.Status != OpenIddictConstants.Statuses.Valid ||
(authorization.Type == OpenIddictConstants.AuthorizationTypes.AdHoc &&
!authorization.Tokens.Any(token => token.Status == OpenIddictConstants.Statuses.Valid))
@ -473,6 +505,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));
@ -509,7 +545,8 @@ namespace OpenIddict.EntityFrameworkCore
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public override async Task SetApplicationIdAsync([NotNull] TAuthorization authorization, [CanBeNull] string identifier, CancellationToken cancellationToken)
public override async Task SetApplicationIdAsync([NotNull] TAuthorization authorization,
[CanBeNull] string identifier, CancellationToken cancellationToken)
{
if (authorization == null)
{

4
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs

@ -167,7 +167,7 @@ namespace OpenIddict.EntityFrameworkCore
throw new ArgumentNullException(nameof(query));
}
return query(Scopes, state).FirstOrDefaultAsync(cancellationToken);
return query(Scopes.AsTracking(), state).FirstOrDefaultAsync(cancellationToken);
}
/// <summary>
@ -191,7 +191,7 @@ namespace OpenIddict.EntityFrameworkCore
throw new ArgumentNullException(nameof(query));
}
return ImmutableArray.CreateRange(await query(Scopes, state).ToListAsync(cancellationToken));
return ImmutableArray.CreateRange(await query(Scopes.AsTracking(), state).ToListAsync(cancellationToken));
}
/// <summary>

22
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs

@ -189,8 +189,8 @@ namespace OpenIddict.EntityFrameworkCore
// See https://github.com/openiddict/openiddict-core/issues/499 for more information.
IQueryable<TToken> Query(IQueryable<TApplication> applications, IQueryable<TToken> tokens, TKey key)
=> from token in tokens.Include(token => token.Application).Include(token => token.Authorization)
join application in applications on token.Application.Id equals application.Id
=> from token in tokens.Include(token => token.Application).Include(token => token.Authorization).AsTracking()
join application in applications.AsTracking() on token.Application.Id equals application.Id
where application.Id.Equals(key)
select token;
@ -220,8 +220,8 @@ namespace OpenIddict.EntityFrameworkCore
// See https://github.com/openiddict/openiddict-core/issues/499 for more information.
IQueryable<TToken> Query(IQueryable<TAuthorization> authorizations, IQueryable<TToken> tokens, TKey key)
=> from token in tokens.Include(token => token.Application).Include(token => token.Authorization)
join authorization in authorizations on token.Authorization.Id equals authorization.Id
=> from token in tokens.Include(token => token.Application).Include(token => token.Authorization).AsTracking()
join authorization in authorizations.AsTracking() on token.Authorization.Id equals authorization.Id
where authorization.Id.Equals(key)
select token;
@ -281,7 +281,8 @@ namespace OpenIddict.EntityFrameworkCore
return query(
Tokens.Include(token => token.Application)
.Include(token => token.Authorization), state).FirstOrDefaultAsync(cancellationToken);
.Include(token => token.Authorization)
.AsTracking(), state).FirstOrDefaultAsync(cancellationToken);
}
/// <summary>
@ -307,7 +308,8 @@ namespace OpenIddict.EntityFrameworkCore
return ImmutableArray.CreateRange(await query(
Tokens.Include(token => token.Application)
.Include(token => token.Authorization), state).ToListAsync(cancellationToken));
.Include(token => token.Authorization)
.AsTracking(), state).ToListAsync(cancellationToken));
}
/// <summary>
@ -326,7 +328,7 @@ namespace OpenIddict.EntityFrameworkCore
IList<Exception> exceptions = null;
IQueryable<TToken> Query(IQueryable<TToken> tokens, int offset)
=> (from token in tokens
=> (from token in tokens.AsTracking()
where token.ExpirationDate < DateTimeOffset.UtcNow ||
token.Status != OpenIddictConstants.Statuses.Valid
orderby token.Id
@ -409,7 +411,8 @@ namespace OpenIddict.EntityFrameworkCore
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public override async Task SetApplicationIdAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken)
public override async Task SetApplicationIdAsync([NotNull] TToken token,
[CanBeNull] string identifier, CancellationToken cancellationToken)
{
if (token == null)
{
@ -452,7 +455,8 @@ namespace OpenIddict.EntityFrameworkCore
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public override async Task SetAuthorizationIdAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken)
public override async Task SetAuthorizationIdAsync([NotNull] TToken token,
[CanBeNull] string identifier, CancellationToken cancellationToken)
{
if (token == null)
{

Loading…
Cancel
Save