Browse Source

Update the Entity Framework Core 2.0 stores to use compiled queries

pull/576/head
Kévin Chalet 8 years ago
parent
commit
2554bd55e1
  1. 42
      src/OpenIddict.Core/Stores/OpenIddictApplicationStore.cs
  2. 14
      src/OpenIddict.Core/Stores/OpenIddictScopeStore.cs
  3. 31
      src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs
  4. 41
      src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs
  5. 27
      src/OpenIddict.EntityFramework/Stores/OpenIddictScopeStore.cs
  6. 41
      src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs
  7. 165
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs
  8. 158
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs
  9. 91
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs
  10. 156
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs

42
src/OpenIddict.Core/Stores/OpenIddictApplicationStore.cs

@ -389,15 +389,17 @@ namespace OpenIddict.Core
// To mitigate that, the resulting array is stored in the memory cache.
var key = string.Concat(nameof(GetPermissionsAsync), "\x1e", application.Permissions);
var permissions = Cache.Get(key) as ImmutableArray<string>?;
if (permissions == null)
var permissions = Cache.GetOrCreate(key, entry =>
{
permissions = Cache.Set(key, JArray.Parse(application.Permissions)
entry.SetPriority(CacheItemPriority.High)
.SetSlidingExpiration(TimeSpan.FromMinutes(1));
return JArray.Parse(application.Permissions)
.Select(element => (string) element)
.ToImmutableArray());
}
.ToImmutableArray();
});
return new ValueTask<ImmutableArray<string>>(permissions.GetValueOrDefault());
return new ValueTask<ImmutableArray<string>>(permissions);
}
/// <summary>
@ -425,15 +427,17 @@ namespace OpenIddict.Core
// To mitigate that, the resulting array is stored in the memory cache.
var key = string.Concat(nameof(GetPostLogoutRedirectUrisAsync), "\x1e", application.PostLogoutRedirectUris);
var addresses = Cache.Get(key) as ImmutableArray<string>?;
if (addresses == null)
var addresses = Cache.GetOrCreate(key, entry =>
{
addresses = Cache.Set(key, JArray.Parse(application.PostLogoutRedirectUris)
entry.SetPriority(CacheItemPriority.High)
.SetSlidingExpiration(TimeSpan.FromMinutes(1));
return JArray.Parse(application.PostLogoutRedirectUris)
.Select(element => (string) element)
.ToImmutableArray());
}
.ToImmutableArray();
});
return new ValueTask<ImmutableArray<string>>(addresses.GetValueOrDefault());
return new ValueTask<ImmutableArray<string>>(addresses);
}
/// <summary>
@ -485,15 +489,17 @@ namespace OpenIddict.Core
// To mitigate that, the resulting array is stored in the memory cache.
var key = string.Concat(nameof(GetRedirectUrisAsync), "\x1e", application.RedirectUris);
var addresses = Cache.Get(key) as ImmutableArray<string>?;
if (addresses == null)
var addresses = Cache.GetOrCreate(key, entry =>
{
addresses = Cache.Set(key, JArray.Parse(application.RedirectUris)
entry.SetPriority(CacheItemPriority.High)
.SetSlidingExpiration(TimeSpan.FromMinutes(1));
return JArray.Parse(application.RedirectUris)
.Select(element => (string) element)
.ToImmutableArray());
}
.ToImmutableArray();
});
return new ValueTask<ImmutableArray<string>>(addresses.GetValueOrDefault());
return new ValueTask<ImmutableArray<string>>(addresses);
}
/// <summary>

14
src/OpenIddict.Core/Stores/OpenIddictScopeStore.cs

@ -302,15 +302,17 @@ namespace OpenIddict.Core
// To mitigate that, the resulting array is stored in the memory cache.
var key = string.Concat(nameof(GetResourcesAsync), "\x1e", scope.Resources);
var resources = Cache.Get(key) as ImmutableArray<string>?;
if (resources == null)
var resources = Cache.GetOrCreate(key, entry =>
{
resources = Cache.Set(key, JArray.Parse(scope.Resources)
entry.SetPriority(CacheItemPriority.High)
.SetSlidingExpiration(TimeSpan.FromMinutes(1));
return JArray.Parse(scope.Resources)
.Select(element => (string) element)
.ToImmutableArray());
}
.ToImmutableArray();
});
return new ValueTask<ImmutableArray<string>>(resources.GetValueOrDefault());
return new ValueTask<ImmutableArray<string>>(resources);
}
/// <summary>

31
src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs

@ -29,9 +29,7 @@ namespace OpenIddict.EntityFramework
OpenIddictToken, TContext, string>
where TContext : DbContext
{
public OpenIddictApplicationStore(
[NotNull] TContext context,
[NotNull] IMemoryCache cache)
public OpenIddictApplicationStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(context, cache)
{
}
@ -49,9 +47,7 @@ namespace OpenIddict.EntityFramework
where TContext : DbContext
where TKey : IEquatable<TKey>
{
public OpenIddictApplicationStore(
[NotNull] TContext context,
[NotNull] IMemoryCache cache)
public OpenIddictApplicationStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(context, cache)
{
}
@ -74,9 +70,7 @@ namespace OpenIddict.EntityFramework
where TContext : DbContext
where TKey : IEquatable<TKey>
{
public OpenIddictApplicationStore(
[NotNull] TContext context,
[NotNull] IMemoryCache cache)
public OpenIddictApplicationStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(cache)
{
if (context == null)
@ -216,25 +210,6 @@ namespace OpenIddict.EntityFramework
}
}
/// <summary>
/// Retrieves an application using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
public override Task<TApplication> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
return Applications.FindAsync(cancellationToken, ConvertIdentifierFromString(identifier));
}
/// <summary>
/// Executes the specified query and returns the first element.
/// </summary>

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

@ -29,9 +29,7 @@ namespace OpenIddict.EntityFramework
OpenIddictToken, TContext, string>
where TContext : DbContext
{
public OpenIddictAuthorizationStore(
[NotNull] TContext context,
[NotNull] IMemoryCache cache)
public OpenIddictAuthorizationStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(context, cache)
{
}
@ -49,9 +47,7 @@ namespace OpenIddict.EntityFramework
where TContext : DbContext
where TKey : IEquatable<TKey>
{
public OpenIddictAuthorizationStore(
[NotNull] TContext context,
[NotNull] IMemoryCache cache)
public OpenIddictAuthorizationStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(context, cache)
{
}
@ -74,9 +70,7 @@ namespace OpenIddict.EntityFramework
where TContext : DbContext
where TKey : IEquatable<TKey>
{
public OpenIddictAuthorizationStore(
[NotNull] TContext context,
[NotNull] IMemoryCache cache)
public OpenIddictAuthorizationStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(cache)
{
if (context == null)
@ -198,35 +192,6 @@ namespace OpenIddict.EntityFramework
}
}
/// <summary>
/// Retrieves an authorization using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorization corresponding to the identifier.
/// </returns>
public override Task<TAuthorization> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
var authorization = (from entry in Context.ChangeTracker.Entries<TAuthorization>()
where entry.Entity != null
where entry.Entity.Id.Equals(ConvertIdentifierFromString(identifier))
select entry.Entity).FirstOrDefault();
if (authorization != null)
{
return Task.FromResult(authorization);
}
return base.FindByIdAsync(identifier, cancellationToken);
}
/// <summary>
/// Retrieves the optional application identifier associated with an authorization.
/// </summary>

27
src/OpenIddict.EntityFramework/Stores/OpenIddictScopeStore.cs

@ -43,9 +43,7 @@ namespace OpenIddict.EntityFramework
where TContext : DbContext
where TKey : IEquatable<TKey>
{
public OpenIddictScopeStore(
[NotNull] TContext context,
[NotNull] IMemoryCache cache)
public OpenIddictScopeStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(context, cache)
{
}
@ -63,9 +61,7 @@ namespace OpenIddict.EntityFramework
where TContext : DbContext
where TKey : IEquatable<TKey>
{
public OpenIddictScopeStore(
[NotNull] TContext context,
[NotNull] IMemoryCache cache)
public OpenIddictScopeStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(cache)
{
if (context == null)
@ -146,25 +142,6 @@ namespace OpenIddict.EntityFramework
return Context.SaveChangesAsync(cancellationToken);
}
/// <summary>
/// Retrieves a scope using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the identifier.
/// </returns>
public override Task<TScope> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
return Scopes.FindAsync(cancellationToken, ConvertIdentifierFromString(identifier));
}
/// <summary>
/// Executes the specified query and returns the first element.
/// </summary>

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

@ -29,9 +29,7 @@ namespace OpenIddict.EntityFramework
OpenIddictAuthorization, TContext, string>
where TContext : DbContext
{
public OpenIddictTokenStore(
[NotNull] TContext context,
[NotNull] IMemoryCache cache)
public OpenIddictTokenStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(context, cache)
{
}
@ -49,9 +47,7 @@ namespace OpenIddict.EntityFramework
where TContext : DbContext
where TKey : IEquatable<TKey>
{
public OpenIddictTokenStore(
[NotNull] TContext context,
[NotNull] IMemoryCache cache)
public OpenIddictTokenStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(context, cache)
{
}
@ -74,9 +70,7 @@ namespace OpenIddict.EntityFramework
where TContext : DbContext
where TKey : IEquatable<TKey>
{
public OpenIddictTokenStore(
[NotNull] TContext context,
[NotNull] IMemoryCache cache)
public OpenIddictTokenStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(cache)
{
if (context == null)
@ -165,35 +159,6 @@ namespace OpenIddict.EntityFramework
return Context.SaveChangesAsync(cancellationToken);
}
/// <summary>
/// Retrieves a token using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token corresponding to the unique identifier.
/// </returns>
public override Task<TToken> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
var token = (from entry in Context.ChangeTracker.Entries<TToken>()
where entry.Entity != null
where entry.Entity.Id.Equals(ConvertIdentifierFromString(identifier))
select entry.Entity).FirstOrDefault();
if (token != null)
{
return Task.FromResult(token);
}
return base.FindByIdAsync(identifier, cancellationToken);
}
/// <summary>
/// Retrieves the optional application identifier associated with a token.
/// </summary>

165
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs

@ -9,11 +9,13 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Data;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Caching.Memory;
using OpenIddict.Core;
@ -31,9 +33,7 @@ namespace OpenIddict.EntityFrameworkCore
OpenIddictToken, TContext, string>
where TContext : DbContext
{
public OpenIddictApplicationStore(
[NotNull] TContext context,
[NotNull] IMemoryCache cache)
public OpenIddictApplicationStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(context, cache)
{
}
@ -51,9 +51,7 @@ namespace OpenIddict.EntityFrameworkCore
where TContext : DbContext
where TKey : IEquatable<TKey>
{
public OpenIddictApplicationStore(
[NotNull] TContext context,
[NotNull] IMemoryCache cache)
public OpenIddictApplicationStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(context, cache)
{
}
@ -76,9 +74,7 @@ namespace OpenIddict.EntityFrameworkCore
where TContext : DbContext
where TKey : IEquatable<TKey>
{
public OpenIddictApplicationStore(
[NotNull] TContext context,
[NotNull] IMemoryCache cache)
public OpenIddictApplicationStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(cache)
{
if (context == null)
@ -240,6 +236,37 @@ namespace OpenIddict.EntityFrameworkCore
}
}
/// <summary>
/// Retrieves an application using its client identifier.
/// </summary>
/// <param name="identifier">The client identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
public override Task<TApplication> FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
const string key = nameof(FindByClientIdAsync) + "\x1e" + nameof(identifier);
var query = Cache.GetOrCreate(key, entry =>
{
entry.SetPriority(CacheItemPriority.NeverRemove);
return EF.CompileAsyncQuery((TContext context, string id) =>
(from application in context.Set<TApplication>().AsTracking()
where application.ClientId == id
select application).FirstOrDefault());
});
return query(Context, identifier);
}
/// <summary>
/// Retrieves an application using its unique identifier.
/// </summary>
@ -256,7 +283,125 @@ namespace OpenIddict.EntityFrameworkCore
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
return Applications.FindAsync(new object[] { ConvertIdentifierFromString(identifier) }, cancellationToken);
const string key = nameof(FindByIdAsync) + "\x1e" + nameof(identifier);
var query = Cache.GetOrCreate(key, entry =>
{
entry.SetPriority(CacheItemPriority.NeverRemove);
return EF.CompileAsyncQuery((TContext context, TKey id) =>
(from application in context.Set<TApplication>().AsTracking()
where application.Id.Equals(id)
select application).FirstOrDefault());
});
return query(Context, ConvertIdentifierFromString(identifier));
}
/// <summary>
/// Retrieves all the applications associated with the specified post_logout_redirect_uri.
/// </summary>
/// <param name="address">The post_logout_redirect_uri associated with the applications.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified post_logout_redirect_uri.
/// </returns>
public override async Task<ImmutableArray<TApplication>> FindByPostLogoutRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(address))
{
throw new ArgumentException("The address cannot be null or empty.", nameof(address));
}
const string key = nameof(FindByPostLogoutRedirectUriAsync) + "\x1e" + nameof(address);
// To optimize the efficiency of the query a bit, only applications whose stringified
// PostLogoutRedirectUris contains the specified URL are returned. Once the applications
// are retrieved, a second pass is made to ensure only valid elements are returned.
// Implementers that use this method in a hot path may want to override this method
// to use SQL Server 2016 functions like JSON_VALUE to make the query more efficient.
var query = Cache.GetOrCreate(key, entry =>
{
entry.SetPriority(CacheItemPriority.NeverRemove);
return EF.CompileAsyncQuery((TContext context, string uri) =>
from application in context.Set<TApplication>().AsTracking()
where application.PostLogoutRedirectUris.Contains(uri)
select application);
});
var builder = ImmutableArray.CreateBuilder<TApplication>();
foreach (var application in await query(Context, address).ToListAsync(cancellationToken))
{
foreach (var uri in await GetPostLogoutRedirectUrisAsync(application, cancellationToken))
{
// Note: the post_logout_redirect_uri must be compared
// using case-sensitive "Simple String Comparison".
if (string.Equals(uri, address, StringComparison.Ordinal))
{
builder.Add(application);
break;
}
}
}
return builder.ToImmutable();
}
/// <summary>
/// Retrieves all the applications associated with the specified redirect_uri.
/// </summary>
/// <param name="address">The redirect_uri associated with the applications.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified redirect_uri.
/// </returns>
public override async Task<ImmutableArray<TApplication>> FindByRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(address))
{
throw new ArgumentException("The address cannot be null or empty.", nameof(address));
}
const string key = nameof(FindByRedirectUriAsync) + "\x1e" + nameof(address);
// To optimize the efficiency of the query a bit, only applications whose stringified
// RedirectUris property contains the specified URL are returned. Once the applications
// are retrieved, a second pass is made to ensure only valid elements are returned.
// Implementers that use this method in a hot path may want to override this method
// to use SQL Server 2016 functions like JSON_VALUE to make the query more efficient.
var query = Cache.GetOrCreate(key, entry =>
{
entry.SetPriority(CacheItemPriority.NeverRemove);
return EF.CompileAsyncQuery((TContext context, string uri) =>
from application in context.Set<TApplication>().AsTracking()
where application.RedirectUris.Contains(uri)
select application);
});
var builder = ImmutableArray.CreateBuilder<TApplication>();
foreach (var application in await query(Context, address).ToListAsync(cancellationToken))
{
foreach (var uri in await GetRedirectUrisAsync(application, cancellationToken))
{
// Note: the redirect_uri must be compared using case-sensitive "Simple String Comparison".
// See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest for more information.
if (string.Equals(uri, address, StringComparison.Ordinal))
{
builder.Add(application);
break;
}
}
}
return builder.ToImmutable();
}
/// <summary>

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

@ -9,11 +9,13 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Data;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Caching.Memory;
using OpenIddict.Core;
@ -31,9 +33,7 @@ namespace OpenIddict.EntityFrameworkCore
OpenIddictToken, TContext, string>
where TContext : DbContext
{
public OpenIddictAuthorizationStore(
[NotNull] TContext context,
[NotNull] IMemoryCache cache)
public OpenIddictAuthorizationStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(context, cache)
{
}
@ -51,9 +51,7 @@ namespace OpenIddict.EntityFrameworkCore
where TContext : DbContext
where TKey : IEquatable<TKey>
{
public OpenIddictAuthorizationStore(
[NotNull] TContext context,
[NotNull] IMemoryCache cache)
public OpenIddictAuthorizationStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(context, cache)
{
}
@ -76,9 +74,7 @@ namespace OpenIddict.EntityFrameworkCore
where TContext : DbContext
where TKey : IEquatable<TKey>
{
public OpenIddictAuthorizationStore(
[NotNull] TContext context,
[NotNull] IMemoryCache cache)
public OpenIddictAuthorizationStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(cache)
{
if (context == null)
@ -240,21 +236,28 @@ namespace OpenIddict.EntityFrameworkCore
throw new ArgumentException("The client cannot be null or empty.", nameof(client));
}
const string key = nameof(FindAsync) + "\x1e" + nameof(subject) + "\x1e" + nameof(client);
// 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 method is overriden to use an explicit join before applying the equality check.
// See https://github.com/openiddict/openiddict-core/issues/499 for more information.
IQueryable<TAuthorization> Query(IQueryable<TAuthorization> authorizations,
IQueryable<TApplication> applications, TKey key, string principal)
=> from authorization in authorizations.Include(authorization => authorization.Application).AsTracking()
where authorization.Subject == principal
join application in applications.AsTracking() on authorization.Application.Id equals application.Id
where application.Id.Equals(key)
select authorization;
return ImmutableArray.CreateRange(await Query(
Authorizations, Applications, ConvertIdentifierFromString(client), subject).ToListAsync(cancellationToken));
var query = Cache.GetOrCreate(key, entry =>
{
entry.SetPriority(CacheItemPriority.NeverRemove);
return EF.CompileAsyncQuery((TContext context, TKey id, string principal) =>
from authorization in context.Set<TAuthorization>()
.Include(authorization => authorization.Application)
.AsTracking()
where authorization.Subject == principal
join application in context.Set<TApplication>().AsTracking() on authorization.Application.Id equals application.Id
where application.Id.Equals(id)
select authorization);
});
return ImmutableArray.CreateRange(await query(Context,
ConvertIdentifierFromString(client), subject).ToListAsync(cancellationToken));
}
/// <summary>
@ -287,21 +290,28 @@ namespace OpenIddict.EntityFrameworkCore
throw new ArgumentException("The status cannot be null or empty.", nameof(status));
}
const string key = nameof(FindAsync) + "\x1e" + nameof(subject) + "\x1e" + nameof(client) + "\x1e" + nameof(status);
// 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 method is overriden to use an explicit join before applying the equality check.
// See https://github.com/openiddict/openiddict-core/issues/499 for more information.
IQueryable<TAuthorization> Query(IQueryable<TAuthorization> authorizations,
IQueryable<TApplication> applications, TKey key, string principal, string state)
=> 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;
return ImmutableArray.CreateRange(await Query(
Authorizations, Applications, ConvertIdentifierFromString(client), subject, status).ToListAsync(cancellationToken));
var query = Cache.GetOrCreate(key, entry =>
{
entry.SetPriority(CacheItemPriority.NeverRemove);
return EF.CompileAsyncQuery((TContext context, TKey id, string principal, string state) =>
from authorization in context.Set<TAuthorization>()
.Include(authorization => authorization.Application)
.AsTracking()
where authorization.Subject == principal && authorization.Status == state
join application in context.Set<TApplication>().AsTracking() on authorization.Application.Id equals application.Id
where application.Id.Equals(id)
select authorization);
});
return ImmutableArray.CreateRange(await query(Context,
ConvertIdentifierFromString(client), subject, status).ToListAsync(cancellationToken));
}
/// <summary>
@ -340,23 +350,31 @@ namespace OpenIddict.EntityFrameworkCore
throw new ArgumentException("The type cannot be null or empty.", nameof(type));
}
const string key = nameof(FindAsync) + "\x1e" + nameof(subject) + "\x1e" +
nameof(client) + "\x1e" + nameof(status) + "\x1e" + nameof(type);
// 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 method is overriden to use an explicit join before applying the equality check.
// See https://github.com/openiddict/openiddict-core/issues/499 for more information.
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).AsTracking()
where authorization.Subject == principal &&
authorization.Status == state &&
authorization.Type == kind
join application in applications.AsTracking() on authorization.Application.Id equals application.Id
where application.Id.Equals(key)
select authorization;
return ImmutableArray.CreateRange(await Query(
Authorizations, Applications, ConvertIdentifierFromString(client), subject, status, type).ToListAsync(cancellationToken));
var query = Cache.GetOrCreate(key, entry =>
{
entry.SetPriority(CacheItemPriority.NeverRemove);
return EF.CompileAsyncQuery((TContext context, TKey id, string principal, string state, string kind) =>
from authorization in context.Set<TAuthorization>()
.Include(authorization => authorization.Application)
.AsTracking()
where authorization.Subject == principal &&
authorization.Status == state &&
authorization.Type == kind
join application in context.Set<TApplication>().AsTracking() on authorization.Application.Id equals application.Id
where application.Id.Equals(id)
select authorization);
});
return ImmutableArray.CreateRange(await query(Context,
ConvertIdentifierFromString(client), subject, status, type).ToListAsync(cancellationToken));
}
/// <summary>
@ -375,17 +393,55 @@ namespace OpenIddict.EntityFrameworkCore
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
var authorization = (from entry in Context.ChangeTracker.Entries<TAuthorization>()
where entry.Entity != null &&
entry.Entity.Id.Equals(ConvertIdentifierFromString(identifier))
select entry.Entity).FirstOrDefault();
const string key = nameof(FindByIdAsync) + "\x1e" + nameof(identifier);
var query = Cache.GetOrCreate(key, entry =>
{
entry.SetPriority(CacheItemPriority.NeverRemove);
return EF.CompileAsyncQuery((TContext context, TKey id) =>
(from authorization in context.Set<TAuthorization>()
.Include(authorization => authorization.Application)
.AsTracking()
where authorization.Id.Equals(id)
select authorization).FirstOrDefault());
});
return query(Context, ConvertIdentifierFromString(identifier));
}
if (authorization != null)
/// <summary>
/// Retrieves all the authorizations corresponding to the specified subject.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the specified subject.
/// </returns>
public override async Task<ImmutableArray<TAuthorization>> FindBySubjectAsync(
[NotNull] string subject, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(subject))
{
return Task.FromResult(authorization);
throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
}
return base.FindByIdAsync(identifier, cancellationToken);
const string key = nameof(FindBySubjectAsync) + "\x1e" + nameof(subject);
var query = Cache.GetOrCreate(key, entry =>
{
entry.SetPriority(CacheItemPriority.NeverRemove);
return EF.CompileAsyncQuery((TContext context, string principal) =>
from authorization in context.Set<TAuthorization>()
.Include(authorization => authorization.Application)
.AsTracking()
where authorization.Subject == principal
select authorization);
});
return ImmutableArray.CreateRange(await query(Context, subject).ToListAsync(cancellationToken));
}
/// <summary>

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

@ -11,8 +11,8 @@ using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.Extensions.Caching.Memory;
using OpenIddict.Core;
using OpenIddict.Models;
namespace OpenIddict.EntityFrameworkCore
@ -25,9 +25,7 @@ namespace OpenIddict.EntityFrameworkCore
public class OpenIddictScopeStore<TContext> : OpenIddictScopeStore<OpenIddictScope, TContext, string>
where TContext : DbContext
{
public OpenIddictScopeStore(
[NotNull] TContext context,
[NotNull] IMemoryCache cache)
public OpenIddictScopeStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(context, cache)
{
}
@ -43,9 +41,7 @@ namespace OpenIddict.EntityFrameworkCore
where TContext : DbContext
where TKey : IEquatable<TKey>
{
public OpenIddictScopeStore(
[NotNull] TContext context,
[NotNull] IMemoryCache cache)
public OpenIddictScopeStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(context, cache)
{
}
@ -63,9 +59,7 @@ namespace OpenIddict.EntityFrameworkCore
where TContext : DbContext
where TKey : IEquatable<TKey>
{
public OpenIddictScopeStore(
[NotNull] TContext context,
[NotNull] IMemoryCache cache)
public OpenIddictScopeStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(cache)
{
if (context == null)
@ -162,7 +156,82 @@ namespace OpenIddict.EntityFrameworkCore
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
return Scopes.FindAsync(new object[] { ConvertIdentifierFromString(identifier) }, cancellationToken);
const string key = nameof(FindByIdAsync) + "\x1e" + nameof(identifier);
var query = Cache.GetOrCreate(key, entry =>
{
entry.SetPriority(CacheItemPriority.NeverRemove);
return EF.CompileAsyncQuery((TContext context, TKey id) =>
(from scope in context.Set<TScope>().AsTracking()
where scope.Id.Equals(id)
select scope).FirstOrDefault());
});
return query(Context, ConvertIdentifierFromString(identifier));
}
/// <summary>
/// Retrieves a scope using its name.
/// </summary>
/// <param name="name">The name associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the specified name.
/// </returns>
public override Task<TScope> FindByNameAsync([NotNull] string name, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("The scope name cannot be null or empty.", nameof(name));
}
const string key = nameof(FindByNameAsync) + "\x1e" + nameof(name);
var query = Cache.GetOrCreate(key, entry =>
{
entry.SetPriority(CacheItemPriority.NeverRemove);
return EF.CompileAsyncQuery((TContext context, string id) =>
(from scope in context.Set<TScope>().AsTracking()
where scope.Name == id
select scope).FirstOrDefault());
});
return query(Context, name);
}
/// <summary>
/// Retrieves a list of scopes using their name.
/// </summary>
/// <param name="names">The names associated with the scopes.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scopes corresponding to the specified names.
/// </returns>
public override async Task<ImmutableArray<TScope>> FindByNamesAsync(
ImmutableArray<string> names, CancellationToken cancellationToken)
{
if (names.Any(name => string.IsNullOrEmpty(name)))
{
throw new ArgumentException("Scope names cannot be null or empty.", nameof(names));
}
const string key = nameof(FindByNamesAsync) + "\x1e" + nameof(names);
var query = Cache.GetOrCreate(key, entry =>
{
entry.SetPriority(CacheItemPriority.NeverRemove);
return EF.CompileAsyncQuery((TContext context, ImmutableArray<string> ids) =>
from scope in context.Set<TScope>().AsTracking()
where ids.Contains(scope.Name)
select scope);
});
return ImmutableArray.CreateRange(await query(Context, names).ToListAsync(cancellationToken));
}
/// <summary>

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

@ -9,11 +9,13 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Data;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Caching.Memory;
using OpenIddict.Core;
@ -31,9 +33,7 @@ namespace OpenIddict.EntityFrameworkCore
OpenIddictAuthorization, TContext, string>
where TContext : DbContext
{
public OpenIddictTokenStore(
[NotNull] TContext context,
[NotNull] IMemoryCache cache)
public OpenIddictTokenStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(context, cache)
{
}
@ -51,9 +51,7 @@ namespace OpenIddict.EntityFrameworkCore
where TContext : DbContext
where TKey : IEquatable<TKey>
{
public OpenIddictTokenStore(
[NotNull] TContext context,
[NotNull] IMemoryCache cache)
public OpenIddictTokenStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(context, cache)
{
}
@ -76,9 +74,7 @@ namespace OpenIddict.EntityFrameworkCore
where TContext : DbContext
where TKey : IEquatable<TKey>
{
public OpenIddictTokenStore(
[NotNull] TContext context,
[NotNull] IMemoryCache cache)
public OpenIddictTokenStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(cache)
{
if (context == null)
@ -183,19 +179,28 @@ namespace OpenIddict.EntityFrameworkCore
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
const string key = nameof(FindByApplicationIdAsync) + "\x1e" + nameof(identifier);
// 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 method is overriden to use an explicit join before applying the equality check.
// 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).AsTracking()
join application in applications.AsTracking() on token.Application.Id equals application.Id
where application.Id.Equals(key)
select token;
return ImmutableArray.CreateRange(await Query(
Applications, Tokens, ConvertIdentifierFromString(identifier)).ToListAsync(cancellationToken));
var query = Cache.GetOrCreate(key, entry =>
{
entry.SetPriority(CacheItemPriority.NeverRemove);
return EF.CompileAsyncQuery((TContext context, TKey id) =>
from token in context.Set<TToken>()
.Include(token => token.Application)
.Include(token => token.Authorization)
.AsTracking()
join application in context.Set<TApplication>().AsTracking() on token.Application.Id equals application.Id
where application.Id.Equals(id)
select token);
});
return ImmutableArray.CreateRange(await query(Context,
ConvertIdentifierFromString(identifier)).ToListAsync(cancellationToken));
}
/// <summary>
@ -214,19 +219,28 @@ namespace OpenIddict.EntityFrameworkCore
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
const string key = nameof(FindByAuthorizationIdAsync) + "\x1e" + nameof(identifier);
// Note: due to a bug in Entity Framework Core's query visitor, the tokens can't be
// filtered using token.Authorization.Id.Equals(key). To work around this issue,
// this method is overriden to use an explicit join before applying the equality check.
// 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).AsTracking()
join authorization in authorizations.AsTracking() on token.Authorization.Id equals authorization.Id
where authorization.Id.Equals(key)
select token;
return ImmutableArray.CreateRange(await Query(
Authorizations, Tokens, ConvertIdentifierFromString(identifier)).ToListAsync(cancellationToken));
var query = Cache.GetOrCreate(key, entry =>
{
entry.SetPriority(CacheItemPriority.NeverRemove);
return EF.CompileAsyncQuery<TContext, TKey, TToken>((TContext context, TKey id) =>
from token in context.Set<TToken>()
.Include(token => token.Application)
.Include(token => token.Authorization)
.AsTracking()
join authorization in context.Set<TAuthorization>().AsTracking() on token.Authorization.Id equals authorization.Id
where authorization.Id.Equals(id)
select token);
});
return ImmutableArray.CreateRange(await query(Context,
ConvertIdentifierFromString(identifier)).ToListAsync(cancellationToken));
}
/// <summary>
@ -245,17 +259,91 @@ namespace OpenIddict.EntityFrameworkCore
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
var token = (from entry in Context.ChangeTracker.Entries<TToken>()
where entry.Entity != null &&
entry.Entity.Id.Equals(ConvertIdentifierFromString(identifier))
select entry.Entity).FirstOrDefault();
const string key = nameof(FindByIdAsync) + "\x1e" + nameof(identifier);
var query = Cache.GetOrCreate(key, entry =>
{
entry.SetPriority(CacheItemPriority.NeverRemove);
return EF.CompileAsyncQuery((TContext context, TKey id) =>
(from token in context.Set<TToken>()
.Include(token => token.Application)
.Include(token => token.Authorization)
.AsTracking()
where token.Id.Equals(id)
select token).FirstOrDefault());
});
return query(Context, ConvertIdentifierFromString(identifier));
}
/// <summary>
/// Retrieves the list of tokens corresponding to the specified reference identifier.
/// Note: the reference identifier may be hashed or encrypted for security reasons.
/// </summary>
/// <param name="identifier">The reference identifier associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified reference identifier.
/// </returns>
public override Task<TToken> FindByReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
const string key = nameof(FindByReferenceIdAsync) + "\x1e" + nameof(identifier);
var query = Cache.GetOrCreate(key, entry =>
{
entry.SetPriority(CacheItemPriority.NeverRemove);
return EF.CompileAsyncQuery((TContext context, string id) =>
(from token in context.Set<TToken>()
.Include(token => token.Application)
.Include(token => token.Authorization)
.AsTracking()
where token.ReferenceId == id
select token).FirstOrDefault());
});
return query(Context, identifier);
}
if (token != null)
/// <summary>
/// Retrieves the list of tokens corresponding to the specified subject.
/// </summary>
/// <param name="subject">The subject associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified subject.
/// </returns>
public override async Task<ImmutableArray<TToken>> FindBySubjectAsync([NotNull] string subject, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(subject))
{
return Task.FromResult(token);
throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
}
return base.FindByIdAsync(identifier, cancellationToken);
const string key = nameof(FindBySubjectAsync) + "\x1e" + nameof(subject);
var query = Cache.GetOrCreate(key, entry =>
{
entry.SetPriority(CacheItemPriority.NeverRemove);
return EF.CompileAsyncQuery((TContext context, string principal) =>
from token in context.Set<TToken>()
.Include(token => token.Application)
.Include(token => token.Authorization)
.AsTracking()
where token.Subject == principal
select token);
});
return ImmutableArray.CreateRange(await query(Context, subject).ToListAsync(cancellationToken));
}
/// <summary>

Loading…
Cancel
Save