Browse Source

Backport the caching/stores changes to OpenIddict 1.x

pull/2236/head
Kévin Chalet 8 years ago
parent
commit
83fa9e6221
  1. 162
      src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs
  2. 199
      src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs
  3. 138
      src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs
  4. 247
      src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs
  5. 10
      src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
  6. 10
      src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
  7. 10
      src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs
  8. 10
      src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
  9. 1
      src/OpenIddict.Core/OpenIddictCoreExtensions.cs
  10. 9
      src/OpenIddict.EntityFramework/Configurations/OpenIddictApplicationConfiguration.cs
  11. 4
      src/OpenIddict.EntityFramework/Configurations/OpenIddictAuthorizationConfiguration.cs
  12. 8
      src/OpenIddict.EntityFramework/Configurations/OpenIddictScopeConfiguration.cs
  13. 12
      src/OpenIddict.EntityFramework/Configurations/OpenIddictTokenConfiguration.cs
  14. 1
      src/OpenIddict.EntityFramework/OpenIddict.EntityFramework.csproj
  15. 2
      src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkExtensions.cs
  16. 19
      src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictApplicationConfiguration.cs
  17. 24
      src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictAuthorizationConfiguration.cs
  18. 14
      src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictScopeConfiguration.cs
  19. 34
      src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictTokenConfiguration.cs
  20. 1
      src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj
  21. 2
      src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs
  22. 3
      src/OpenIddict.MongoDb/OpenIddict.MongoDb.csproj
  23. 15
      src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs
  24. 2
      src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs
  25. 4
      src/OpenIddict.Mvc/Internal/OpenIddictMvcBinder.cs
  26. 14
      test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkExtensionsTests.cs
  27. 14
      test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreExtensionsTests.cs
  28. 21
      test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbContextTests.cs

162
src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs

@ -5,12 +5,14 @@
*/
using System;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using OpenIddict.Abstractions;
namespace OpenIddict.Core
@ -22,8 +24,9 @@ namespace OpenIddict.Core
public class OpenIddictApplicationCache<TApplication> : IOpenIddictApplicationCache<TApplication>, IDisposable where TApplication : class
{
private readonly MemoryCache _cache;
private readonly IOpenIddictApplicationStore<TApplication> _store;
private readonly IOptions<OpenIddictCoreOptions> _options;
private readonly ConcurrentDictionary<string, Lazy<CancellationTokenSource>> _signals;
private readonly IOpenIddictApplicationStore<TApplication> _store;
public OpenIddictApplicationCache(
[NotNull] IOptions<OpenIddictCoreOptions> options,
@ -31,6 +34,7 @@ namespace OpenIddict.Core
{
_cache = new MemoryCache(new MemoryCacheOptions());
_options = options;
_signals = new ConcurrentDictionary<string, Lazy<CancellationTokenSource>>(StringComparer.Ordinal);
_store = resolver.Get<TApplication>();
}
@ -54,13 +58,50 @@ namespace OpenIddict.Core
_cache.Compact(0.25);
}
_cache.Remove(new
{
Method = nameof(FindByClientIdAsync),
Identifier = await _store.GetClientIdAsync(application, cancellationToken)
});
_cache.Remove(new
{
Method = nameof(FindByIdAsync),
Identifier = await _store.GetIdAsync(application, cancellationToken)
});
foreach (var address in await _store.GetPostLogoutRedirectUrisAsync(application, cancellationToken))
{
_cache.Remove(new
{
Method = nameof(FindByPostLogoutRedirectUriAsync),
Address = address
});
}
foreach (var address in await _store.GetRedirectUrisAsync(application, cancellationToken))
{
_cache.Remove(new
{
Method = nameof(FindByRedirectUriAsync),
Address = address
});
}
var signal = await CreateExpirationSignalAsync(application, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
using (var entry = _cache.CreateEntry(new
{
Method = nameof(FindByIdAsync),
Identifier = await _store.GetIdAsync(application, cancellationToken)
}))
{
entry.SetValue(application);
entry.AddExpirationToken(signal)
.SetValue(application);
}
using (var entry = _cache.CreateEntry(new
@ -69,14 +110,23 @@ namespace OpenIddict.Core
Identifier = await _store.GetClientIdAsync(application, cancellationToken)
}))
{
entry.SetValue(application);
entry.AddExpirationToken(signal)
.SetValue(application);
}
}
/// <summary>
/// Disposes the cache held by this instance.
/// Disposes the resources held by this instance.
/// </summary>
public void Dispose() => _cache.Dispose();
public void Dispose()
{
foreach (var signal in _signals)
{
signal.Value.Value.Dispose();
}
_cache.Dispose();
}
/// <summary>
/// Retrieves an application using its client identifier.
@ -114,6 +164,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters))
{
if (application != null)
{
var signal = await CreateExpirationSignalAsync(application, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetValue(application);
}
@ -159,6 +220,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters))
{
if (application != null)
{
var signal = await CreateExpirationSignalAsync(application, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetValue(application);
}
@ -205,6 +277,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters))
{
foreach (var application in applications)
{
var signal = await CreateExpirationSignalAsync(application, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetValue(applications);
}
@ -251,6 +334,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters))
{
foreach (var application in applications)
{
var signal = await CreateExpirationSignalAsync(application, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetValue(applications);
}
@ -275,35 +369,53 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(application));
}
_cache.Remove(new
var identifier = await _store.GetIdAsync(application, cancellationToken);
if (string.IsNullOrEmpty(identifier))
{
Method = nameof(FindByClientIdAsync),
Identifier = await _store.GetClientIdAsync(application, cancellationToken)
});
throw new InvalidOperationException("The application identifier cannot be extracted.");
}
_cache.Remove(new
if (_signals.TryGetValue(identifier, out Lazy<CancellationTokenSource> signal))
{
Method = nameof(FindByIdAsync),
Identifier = await _store.GetIdAsync(application, cancellationToken)
});
signal.Value.Cancel();
foreach (var address in await _store.GetPostLogoutRedirectUrisAsync(application, cancellationToken))
_signals.TryRemove(identifier, out signal);
}
}
/// <summary>
/// Creates an expiration signal allowing to invalidate all the
/// cache entries associated with the specified application.
/// </summary>
/// <param name="application">The application associated with the expiration signal.</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 an expiration signal for the specified application.
/// </returns>
protected virtual async Task<IChangeToken> CreateExpirationSignalAsync(
[NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
_cache.Remove(new
{
Method = nameof(FindByPostLogoutRedirectUriAsync),
Address = address
});
throw new ArgumentNullException(nameof(application));
}
foreach (var address in await _store.GetRedirectUrisAsync(application, cancellationToken))
var identifier = await _store.GetIdAsync(application, cancellationToken);
if (string.IsNullOrEmpty(identifier))
{
_cache.Remove(new
{
Method = nameof(FindByRedirectUriAsync),
Address = address
});
throw new InvalidOperationException("The application identifier cannot be extracted.");
}
var signal = _signals.GetOrAdd(identifier, delegate
{
// Note: a Lazy<CancellationTokenSource> is used here to ensure only one CancellationTokenSource
// can be created. Not doing so would result in expiration signals being potentially linked to
// multiple sources, with a single one of them being eventually tracked and thus, cancelable.
return new Lazy<CancellationTokenSource>(() => new CancellationTokenSource());
});
return new CancellationChangeToken(signal.Value.Token);
}
}
}

199
src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs

@ -5,12 +5,14 @@
*/
using System;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using OpenIddict.Abstractions;
namespace OpenIddict.Core
@ -22,6 +24,7 @@ namespace OpenIddict.Core
public class OpenIddictAuthorizationCache<TAuthorization> : IOpenIddictAuthorizationCache<TAuthorization>, IDisposable where TAuthorization : class
{
private readonly MemoryCache _cache;
private readonly ConcurrentDictionary<string, Lazy<CancellationTokenSource>> _signals;
private readonly IOpenIddictAuthorizationStore<TAuthorization> _store;
private readonly IOptions<OpenIddictCoreOptions> _options;
@ -31,6 +34,7 @@ namespace OpenIddict.Core
{
_cache = new MemoryCache(new MemoryCacheOptions());
_options = options;
_signals = new ConcurrentDictionary<string, Lazy<CancellationTokenSource>>(StringComparer.Ordinal);
_store = resolver.Get<TAuthorization>();
}
@ -54,20 +58,77 @@ namespace OpenIddict.Core
_cache.Compact(0.25);
}
_cache.Remove(new
{
Method = nameof(FindAsync),
Subject = await _store.GetSubjectAsync(authorization, cancellationToken),
Client = await _store.GetApplicationIdAsync(authorization, cancellationToken)
});
_cache.Remove(new
{
Method = nameof(FindAsync),
Subject = await _store.GetSubjectAsync(authorization, cancellationToken),
Client = await _store.GetApplicationIdAsync(authorization, cancellationToken),
Status = await _store.GetStatusAsync(authorization, cancellationToken)
});
_cache.Remove(new
{
Method = nameof(FindAsync),
Subject = await _store.GetSubjectAsync(authorization, cancellationToken),
Client = await _store.GetApplicationIdAsync(authorization, cancellationToken),
Status = await _store.GetStatusAsync(authorization, cancellationToken),
Type = await _store.GetTypeAsync(authorization, cancellationToken)
});
_cache.Remove(new
{
Method = nameof(FindByApplicationIdAsync),
Identifier = await _store.GetApplicationIdAsync(authorization, cancellationToken)
});
_cache.Remove(new
{
Method = nameof(FindByIdAsync),
Identifier = await _store.GetIdAsync(authorization, cancellationToken)
});
_cache.Remove(new
{
Method = nameof(FindBySubjectAsync),
Subject = await _store.GetSubjectAsync(authorization, cancellationToken)
});
var signal = await CreateExpirationTokenAsync(authorization, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
using (var entry = _cache.CreateEntry(new
{
Method = nameof(FindByIdAsync),
Identifier = await _store.GetIdAsync(authorization, cancellationToken)
}))
{
entry.SetValue(authorization);
entry.AddExpirationToken(signal)
.SetValue(authorization);
}
}
/// <summary>
/// Disposes the cache held by this instance.
/// Disposes the resources held by this instance.
/// </summary>
public void Dispose() => _cache.Dispose();
public void Dispose()
{
foreach (var signal in _signals)
{
signal.Value.Value.Dispose();
}
_cache.Dispose();
}
/// <summary>
/// Retrieves the authorizations corresponding to the specified
@ -114,6 +175,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters))
{
foreach (var authorization in authorizations)
{
var signal = await CreateExpirationTokenAsync(authorization, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetValue(authorizations);
}
@ -175,6 +247,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters))
{
foreach (var authorization in authorizations)
{
var signal = await CreateExpirationTokenAsync(authorization, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetValue(authorizations);
}
@ -243,6 +326,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters))
{
foreach (var authorization in authorizations)
{
var signal = await CreateExpirationTokenAsync(authorization, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetValue(authorizations);
}
@ -344,6 +438,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters))
{
foreach (var authorization in authorizations)
{
var signal = await CreateExpirationTokenAsync(authorization, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetValue(authorizations);
}
@ -389,6 +494,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters))
{
if (authorization != null)
{
var signal = await CreateExpirationTokenAsync(authorization, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetValue(authorization);
}
@ -435,6 +551,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters))
{
foreach (var authorization in authorizations)
{
var signal = await CreateExpirationTokenAsync(authorization, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetValue(authorizations);
}
@ -459,47 +586,53 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(authorization));
}
_cache.Remove(new
var identifier = await _store.GetIdAsync(authorization, cancellationToken);
if (string.IsNullOrEmpty(identifier))
{
Method = nameof(FindAsync),
Subject = await _store.GetSubjectAsync(authorization, cancellationToken),
Client = await _store.GetApplicationIdAsync(authorization, cancellationToken)
});
throw new InvalidOperationException("The application identifier cannot be extracted.");
}
_cache.Remove(new
if (_signals.TryGetValue(identifier, out Lazy<CancellationTokenSource> signal))
{
Method = nameof(FindAsync),
Subject = await _store.GetSubjectAsync(authorization, cancellationToken),
Client = await _store.GetApplicationIdAsync(authorization, cancellationToken),
Status = await _store.GetStatusAsync(authorization, cancellationToken)
});
signal.Value.Cancel();
_cache.Remove(new
{
Method = nameof(FindAsync),
Subject = await _store.GetSubjectAsync(authorization, cancellationToken),
Client = await _store.GetApplicationIdAsync(authorization, cancellationToken),
Status = await _store.GetStatusAsync(authorization, cancellationToken),
Type = await _store.GetTypeAsync(authorization, cancellationToken)
});
_signals.TryRemove(identifier, out signal);
}
}
_cache.Remove(new
/// <summary>
/// Creates an expiration signal allowing to invalidate all the
/// cache entries associated with the specified authorization.
/// </summary>
/// <param name="authorization">The authorization associated with the expiration signal.</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 an expiration signal for the specified authorization.
/// </returns>
protected virtual async Task<IChangeToken> CreateExpirationTokenAsync(
[NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
Method = nameof(FindByApplicationIdAsync),
Identifier = await _store.GetApplicationIdAsync(authorization, cancellationToken)
});
throw new ArgumentNullException(nameof(authorization));
}
_cache.Remove(new
var identifier = await _store.GetIdAsync(authorization, cancellationToken);
if (string.IsNullOrEmpty(identifier))
{
Method = nameof(FindByIdAsync),
Identifier = await _store.GetIdAsync(authorization, cancellationToken)
});
throw new InvalidOperationException("The authorization identifier cannot be extracted.");
}
_cache.Remove(new
var signal = _signals.GetOrAdd(identifier, delegate
{
Method = nameof(FindBySubjectAsync),
Subject = await _store.GetSubjectAsync(authorization, cancellationToken)
// Note: a Lazy<CancellationTokenSource> is used here to ensure only one CancellationTokenSource
// can be created. Not doing so would result in expiration signals being potentially linked to
// multiple sources, with a single one of them being eventually tracked and thus, cancelable.
return new Lazy<CancellationTokenSource>(() => new CancellationTokenSource());
});
return new CancellationChangeToken(signal.Value.Token);
}
}
}

138
src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs

@ -5,6 +5,7 @@
*/
using System;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
@ -12,6 +13,7 @@ using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using OpenIddict.Abstractions;
namespace OpenIddict.Core
@ -23,8 +25,9 @@ namespace OpenIddict.Core
public class OpenIddictScopeCache<TScope> : IOpenIddictScopeCache<TScope>, IDisposable where TScope : class
{
private readonly MemoryCache _cache;
private readonly IOpenIddictScopeStore<TScope> _store;
private readonly IOptions<OpenIddictCoreOptions> _options;
private readonly ConcurrentDictionary<string, Lazy<CancellationTokenSource>> _signals;
private readonly IOpenIddictScopeStore<TScope> _store;
public OpenIddictScopeCache(
[NotNull] IOptions<OpenIddictCoreOptions> options,
@ -32,6 +35,7 @@ namespace OpenIddict.Core
{
_cache = new MemoryCache(new MemoryCacheOptions());
_options = options;
_signals = new ConcurrentDictionary<string, Lazy<CancellationTokenSource>>(StringComparer.Ordinal);
_store = resolver.Get<TScope>();
}
@ -55,13 +59,41 @@ namespace OpenIddict.Core
_cache.Compact(0.25);
}
_cache.Remove(new
{
Method = nameof(FindByIdAsync),
Identifier = await _store.GetIdAsync(scope, cancellationToken)
});
_cache.Remove(new
{
Method = nameof(FindByNameAsync),
Name = await _store.GetNameAsync(scope, cancellationToken)
});
foreach (var resource in await _store.GetResourcesAsync(scope, cancellationToken))
{
_cache.Remove(new
{
Method = nameof(FindByResourceAsync),
Resource = resource
});
}
var signal = await CreateExpirationSignalAsync(scope, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration token.");
}
using (var entry = _cache.CreateEntry(new
{
Method = nameof(FindByIdAsync),
Identifier = await _store.GetIdAsync(scope, cancellationToken)
}))
{
entry.SetValue(scope);
entry.AddExpirationToken(signal)
.SetValue(scope);
}
using (var entry = _cache.CreateEntry(new
@ -70,14 +102,23 @@ namespace OpenIddict.Core
Name = await _store.GetNameAsync(scope, cancellationToken)
}))
{
entry.SetValue(scope);
entry.AddExpirationToken(signal)
.SetValue(scope);
}
}
/// <summary>
/// Disposes the cache held by this instance.
/// Disposes the resources held by this instance.
/// </summary>
public void Dispose() => _cache.Dispose();
public void Dispose()
{
foreach (var signal in _signals)
{
signal.Value.Value.Dispose();
}
_cache.Dispose();
}
/// <summary>
/// Retrieves a scope using its unique identifier.
@ -115,6 +156,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters))
{
if (scope != null)
{
var signal = await CreateExpirationSignalAsync(scope, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetValue(scope);
}
@ -160,6 +212,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters))
{
if (scope != null)
{
var signal = await CreateExpirationSignalAsync(scope, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetValue(scope);
}
@ -243,6 +306,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters))
{
foreach (var scope in scopes)
{
var signal = await CreateExpirationSignalAsync(scope, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetValue(scopes);
}
@ -267,26 +341,52 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(scope));
}
_cache.Remove(new
var identifier = await _store.GetIdAsync(scope, cancellationToken);
if (string.IsNullOrEmpty(identifier))
{
Method = nameof(FindByIdAsync),
Identifier = await _store.GetIdAsync(scope, cancellationToken)
});
throw new InvalidOperationException("The application identifier cannot be extracted.");
}
_cache.Remove(new
if (_signals.TryGetValue(identifier, out Lazy<CancellationTokenSource> signal))
{
Method = nameof(FindByNameAsync),
Name = await _store.GetNameAsync(scope, cancellationToken)
});
signal.Value.Cancel();
foreach (var resource in await _store.GetResourcesAsync(scope, cancellationToken))
_signals.TryRemove(identifier, out signal);
}
}
/// <summary>
/// Creates an expiration signal allowing to invalidate all the
/// cache entries associated with the specified scope.
/// </summary>
/// <param name="scope">The scope associated with the expiration signal.</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 an expiration signal for the specified scope.
/// </returns>
protected virtual async Task<IChangeToken> CreateExpirationSignalAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{
if (scope == null)
{
_cache.Remove(new
{
Method = nameof(FindByResourceAsync),
Resource = resource
});
throw new ArgumentNullException(nameof(scope));
}
var identifier = await _store.GetIdAsync(scope, cancellationToken);
if (string.IsNullOrEmpty(identifier))
{
throw new InvalidOperationException("The scope identifier cannot be extracted.");
}
var signal = _signals.GetOrAdd(identifier, delegate
{
// Note: a Lazy<CancellationTokenSource> is used here to ensure only one CancellationTokenSource
// can be created. Not doing so would result in expiration signals being potentially linked to
// multiple sources, with a single one of them being eventually tracked and thus, cancelable.
return new Lazy<CancellationTokenSource>(() => new CancellationTokenSource());
});
return new CancellationChangeToken(signal.Value.Token);
}
}
}

247
src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs

@ -5,12 +5,14 @@
*/
using System;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using OpenIddict.Abstractions;
namespace OpenIddict.Core
@ -22,8 +24,9 @@ namespace OpenIddict.Core
public class OpenIddictTokenCache<TToken> : IOpenIddictTokenCache<TToken>, IDisposable where TToken : class
{
private readonly MemoryCache _cache;
private readonly IOpenIddictTokenStore<TToken> _store;
private readonly IOptions<OpenIddictCoreOptions> _options;
private readonly ConcurrentDictionary<string, Lazy<CancellationTokenSource>> _signals;
private readonly IOpenIddictTokenStore<TToken> _store;
public OpenIddictTokenCache(
[NotNull] IOptions<OpenIddictCoreOptions> options,
@ -31,6 +34,7 @@ namespace OpenIddict.Core
{
_cache = new MemoryCache(new MemoryCacheOptions());
_options = options;
_signals = new ConcurrentDictionary<string, Lazy<CancellationTokenSource>>(StringComparer.Ordinal);
_store = resolver.Get<TToken>();
}
@ -54,13 +58,74 @@ namespace OpenIddict.Core
_cache.Compact(0.25);
}
_cache.Remove(new
{
Method = nameof(FindAsync),
Subject = await _store.GetSubjectAsync(token, cancellationToken),
Client = await _store.GetApplicationIdAsync(token, cancellationToken)
});
_cache.Remove(new
{
Method = nameof(FindAsync),
Subject = await _store.GetSubjectAsync(token, cancellationToken),
Client = await _store.GetApplicationIdAsync(token, cancellationToken),
Status = await _store.GetStatusAsync(token, cancellationToken)
});
_cache.Remove(new
{
Method = nameof(FindAsync),
Subject = await _store.GetSubjectAsync(token, cancellationToken),
Client = await _store.GetApplicationIdAsync(token, cancellationToken),
Status = await _store.GetStatusAsync(token, cancellationToken),
Type = await _store.GetTypeAsync(token, cancellationToken)
});
_cache.Remove(new
{
Method = nameof(FindByApplicationIdAsync),
Identifier = await _store.GetApplicationIdAsync(token, cancellationToken)
});
_cache.Remove(new
{
Method = nameof(FindByAuthorizationIdAsync),
Identifier = await _store.GetAuthorizationIdAsync(token, cancellationToken)
});
_cache.Remove(new
{
Method = nameof(FindByIdAsync),
Identifier = await _store.GetIdAsync(token, cancellationToken)
});
_cache.Remove(new
{
Method = nameof(FindByReferenceIdAsync),
Identifier = await _store.GetReferenceIdAsync(token, cancellationToken)
});
_cache.Remove(new
{
Method = nameof(FindBySubjectAsync),
Subject = await _store.GetSubjectAsync(token, cancellationToken)
});
var signal = await CreateExpirationSignalAsync(token, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
using (var entry = _cache.CreateEntry(new
{
Method = nameof(FindByIdAsync),
Identifier = await _store.GetIdAsync(token, cancellationToken)
}))
{
entry.SetValue(token);
entry.AddExpirationToken(signal)
.SetValue(token);
}
using (var entry = _cache.CreateEntry(new
@ -69,14 +134,23 @@ namespace OpenIddict.Core
Identifier = await _store.GetReferenceIdAsync(token, cancellationToken)
}))
{
entry.SetValue(token);
entry.AddExpirationToken(signal)
.SetValue(token);
}
}
/// <summary>
/// Disposes the cache held by this instance.
/// Disposes the resources held by this instance.
/// </summary>
public void Dispose() => _cache.Dispose();
public void Dispose()
{
foreach (var signal in _signals)
{
signal.Value.Value.Dispose();
}
_cache.Dispose();
}
/// <summary>
/// Retrieves the tokens corresponding to the specified
@ -123,6 +197,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters))
{
foreach (var token in tokens)
{
var signal = await CreateExpirationSignalAsync(token, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetValue(tokens);
}
@ -184,6 +269,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters))
{
foreach (var token in tokens)
{
var signal = await CreateExpirationSignalAsync(token, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetValue(tokens);
}
@ -252,6 +348,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters))
{
foreach (var token in tokens)
{
var signal = await CreateExpirationSignalAsync(token, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetValue(tokens);
}
@ -298,6 +405,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters))
{
foreach (var token in tokens)
{
var signal = await CreateExpirationSignalAsync(token, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetValue(tokens);
}
@ -344,6 +462,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters))
{
foreach (var token in tokens)
{
var signal = await CreateExpirationSignalAsync(token, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetValue(tokens);
}
@ -389,6 +518,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters))
{
if (token != null)
{
var signal = await CreateExpirationSignalAsync(token, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetValue(token);
}
@ -435,6 +575,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters))
{
if (token != null)
{
var signal = await CreateExpirationSignalAsync(token, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetValue(token);
}
@ -480,6 +631,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters))
{
foreach (var token in tokens)
{
var signal = await CreateExpirationSignalAsync(token, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetValue(tokens);
}
@ -504,59 +666,52 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(token));
}
_cache.Remove(new
{
Method = nameof(FindAsync),
Subject = await _store.GetSubjectAsync(token, cancellationToken),
Client = await _store.GetApplicationIdAsync(token, cancellationToken)
});
_cache.Remove(new
var identifier = await _store.GetIdAsync(token, cancellationToken);
if (string.IsNullOrEmpty(identifier))
{
Method = nameof(FindAsync),
Subject = await _store.GetSubjectAsync(token, cancellationToken),
Client = await _store.GetApplicationIdAsync(token, cancellationToken),
Status = await _store.GetStatusAsync(token, cancellationToken)
});
throw new InvalidOperationException("The application identifier cannot be extracted.");
}
_cache.Remove(new
if (_signals.TryGetValue(identifier, out Lazy<CancellationTokenSource> signal))
{
Method = nameof(FindAsync),
Subject = await _store.GetSubjectAsync(token, cancellationToken),
Client = await _store.GetApplicationIdAsync(token, cancellationToken),
Status = await _store.GetStatusAsync(token, cancellationToken),
Type = await _store.GetTypeAsync(token, cancellationToken)
});
signal.Value.Cancel();
_cache.Remove(new
{
Method = nameof(FindByApplicationIdAsync),
Identifier = await _store.GetApplicationIdAsync(token, cancellationToken)
});
_signals.TryRemove(identifier, out signal);
}
}
_cache.Remove(new
/// <summary>
/// Creates an expiration signal allowing to invalidate all the
/// cache entries associated with the specified token.
/// </summary>
/// <param name="token">The token associated with the expiration signal.</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 an expiration signal for the specified token.
/// </returns>
protected virtual async Task<IChangeToken> CreateExpirationSignalAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
Method = nameof(FindByAuthorizationIdAsync),
Identifier = await _store.GetAuthorizationIdAsync(token, cancellationToken)
});
throw new ArgumentNullException(nameof(token));
}
_cache.Remove(new
var identifier = await _store.GetIdAsync(token, cancellationToken);
if (string.IsNullOrEmpty(identifier))
{
Method = nameof(FindByIdAsync),
Identifier = await _store.GetIdAsync(token, cancellationToken)
});
throw new InvalidOperationException("The token identifier cannot be extracted.");
}
_cache.Remove(new
var signal = _signals.GetOrAdd(identifier, delegate
{
Method = nameof(FindByReferenceIdAsync),
Identifier = await _store.GetReferenceIdAsync(token, cancellationToken)
// Note: a Lazy<CancellationTokenSource> is used here to ensure only one CancellationTokenSource
// can be created. Not doing so would result in expiration signals being potentially linked to
// multiple sources, with a single one of them being eventually tracked and thus, cancelable.
return new Lazy<CancellationTokenSource>(() => new CancellationTokenSource());
});
_cache.Remove(new
{
Method = nameof(FindBySubjectAsync),
Subject = await _store.GetSubjectAsync(token, cancellationToken)
});
return new CancellationChangeToken(signal.Value.Token);
}
}
}

10
src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs

@ -158,6 +158,11 @@ namespace OpenIddict.Core
await Store.CreateAsync(application, cancellationToken);
if (!Options.Value.DisableEntityCaching)
{
await Cache.AddAsync(application, cancellationToken);
}
}
/// <summary>
@ -905,12 +910,13 @@ namespace OpenIddict.Core
throw new OpenIddictExceptions.ValidationException(builder.ToString(), results);
}
await Store.UpdateAsync(application, cancellationToken);
if (!Options.Value.DisableEntityCaching)
{
await Cache.RemoveAsync(application, cancellationToken);
await Cache.AddAsync(application, cancellationToken);
}
await Store.UpdateAsync(application, cancellationToken);
}
/// <summary>

10
src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs

@ -126,6 +126,11 @@ namespace OpenIddict.Core
}
await Store.CreateAsync(authorization, cancellationToken);
if (!Options.Value.DisableEntityCaching)
{
await Cache.AddAsync(authorization, cancellationToken);
}
}
/// <summary>
@ -1115,12 +1120,13 @@ namespace OpenIddict.Core
throw new OpenIddictExceptions.ValidationException(builder.ToString(), results);
}
await Store.UpdateAsync(authorization, cancellationToken);
if (!Options.Value.DisableEntityCaching)
{
await Cache.RemoveAsync(authorization, cancellationToken);
await Cache.AddAsync(authorization, cancellationToken);
}
await Store.UpdateAsync(authorization, cancellationToken);
}
/// <summary>

10
src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs

@ -120,6 +120,11 @@ namespace OpenIddict.Core
}
await Store.CreateAsync(scope, cancellationToken);
if (!Options.Value.DisableEntityCaching)
{
await Cache.AddAsync(scope, cancellationToken);
}
}
/// <summary>
@ -679,12 +684,13 @@ namespace OpenIddict.Core
throw new OpenIddictExceptions.ValidationException(builder.ToString(), results);
}
await Store.UpdateAsync(scope, cancellationToken);
if (!Options.Value.DisableEntityCaching)
{
await Cache.RemoveAsync(scope, cancellationToken);
await Cache.AddAsync(scope, cancellationToken);
}
await Store.UpdateAsync(scope, cancellationToken);
}
/// <summary>

10
src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs

@ -134,6 +134,11 @@ namespace OpenIddict.Core
}
await Store.CreateAsync(token, cancellationToken);
if (!Options.Value.DisableEntityCaching)
{
await Cache.AddAsync(token, cancellationToken);
}
}
/// <summary>
@ -1180,12 +1185,13 @@ namespace OpenIddict.Core
throw new OpenIddictExceptions.ValidationException(builder.ToString(), results);
}
await Store.UpdateAsync(token, cancellationToken);
if (!Options.Value.DisableEntityCaching)
{
await Cache.RemoveAsync(token, cancellationToken);
await Cache.AddAsync(token, cancellationToken);
}
await Store.UpdateAsync(token, cancellationToken);
}
/// <summary>

1
src/OpenIddict.Core/OpenIddictCoreExtensions.cs

@ -33,6 +33,7 @@ namespace Microsoft.Extensions.DependencyInjection
}
builder.Services.AddLogging();
builder.Services.AddMemoryCache();
builder.Services.AddOptions();
builder.Services.TryAddScoped(typeof(OpenIddictApplicationManager<>));

9
src/OpenIddict.EntityFramework/Configurations/OpenIddictApplicationConfiguration.cs

@ -48,14 +48,19 @@ namespace OpenIddict.EntityFramework
HasKey(application => application.Id);
Property(application => application.ClientId)
.HasMaxLength(100)
.IsRequired()
.HasMaxLength(450)
.HasColumnAnnotation(IndexAnnotation.AnnotationName, new IndexAnnotation(new IndexAttribute()));
.HasColumnAnnotation(IndexAnnotation.AnnotationName, new IndexAnnotation(new IndexAttribute
{
IsUnique = true
}));
Property(application => application.ConcurrencyToken)
.HasMaxLength(50)
.IsConcurrencyToken();
Property(application => application.Type)
.HasMaxLength(25)
.IsRequired();
HasMany(application => application.Authorizations)

4
src/OpenIddict.EntityFramework/Configurations/OpenIddictAuthorizationConfiguration.cs

@ -46,15 +46,19 @@ namespace OpenIddict.EntityFramework
HasKey(authorization => authorization.Id);
Property(authorization => authorization.ConcurrencyToken)
.HasMaxLength(50)
.IsConcurrencyToken();
Property(authorization => authorization.Status)
.HasMaxLength(25)
.IsRequired();
Property(authorization => authorization.Subject)
.HasMaxLength(450)
.IsRequired();
Property(authorization => authorization.Type)
.HasMaxLength(25)
.IsRequired();
HasMany(authorization => authorization.Tokens)

8
src/OpenIddict.EntityFramework/Configurations/OpenIddictScopeConfiguration.cs

@ -44,12 +44,16 @@ namespace OpenIddict.EntityFramework
HasKey(scope => scope.Id);
Property(scope => scope.ConcurrencyToken)
.HasMaxLength(50)
.IsConcurrencyToken();
Property(scope => scope.Name)
.HasMaxLength(200)
.IsRequired()
.HasMaxLength(450)
.HasColumnAnnotation(IndexAnnotation.AnnotationName, new IndexAnnotation(new IndexAttribute()));
.HasColumnAnnotation(IndexAnnotation.AnnotationName, new IndexAnnotation(new IndexAttribute
{
IsUnique = true
}));
ToTable("OpenIddictScopes");
}

12
src/OpenIddict.EntityFramework/Configurations/OpenIddictTokenConfiguration.cs

@ -48,16 +48,26 @@ namespace OpenIddict.EntityFramework
HasKey(token => token.Id);
Property(token => token.ConcurrencyToken)
.HasMaxLength(50)
.IsConcurrencyToken();
// Warning: the index on the ReferenceId property MUST NOT be declared as
// a unique index, as Entity Framework 6.x doesn't support creating indexes
// with null-friendly WHERE conditions, unlike Entity Framework Core 1.x/2.x.
Property(token => token.ReferenceId)
.HasMaxLength(450)
.HasMaxLength(100)
.HasColumnAnnotation(IndexAnnotation.AnnotationName, new IndexAnnotation(new IndexAttribute()));
Property(token => token.Status)
.HasMaxLength(25)
.IsRequired();
Property(token => token.Subject)
.HasMaxLength(450)
.IsRequired();
Property(token => token.Type)
.HasMaxLength(25)
.IsRequired();
ToTable("OpenIddictTokens");

1
src/OpenIddict.EntityFramework/OpenIddict.EntityFramework.csproj

@ -19,7 +19,6 @@
<ItemGroup>
<PackageReference Include="EntityFramework" Version="$(EntityFrameworkVersion)" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="$(AspNetCoreVersion)" />
<PackageReference Include="JetBrains.Annotations" Version="$(JetBrainsVersion)" PrivateAssets="All" />
</ItemGroup>

2
src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkExtensions.cs

@ -32,8 +32,6 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentNullException(nameof(builder));
}
builder.Services.AddMemoryCache();
// Since Entity Framework 6.x may be used with databases performing case-insensitive
// or culture-sensitive comparisons, ensure the additional filtering logic is enforced
// in case case-sensitive stores were registered before this extension was called.

19
src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictApplicationConfiguration.cs

@ -6,6 +6,7 @@
using System;
using System.ComponentModel;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using OpenIddict.EntityFrameworkCore.Models;
@ -26,7 +27,7 @@ namespace OpenIddict.EntityFrameworkCore
where TToken : OpenIddictToken<TKey, TApplication, TAuthorization>
where TKey : IEquatable<TKey>
{
public void Configure(EntityTypeBuilder<TApplication> builder)
public void Configure([NotNull] EntityTypeBuilder<TApplication> builder)
{
if (builder == null)
{
@ -37,28 +38,40 @@ namespace OpenIddict.EntityFrameworkCore
// Entity Framework would throw an exception due to the TKey generic parameter
// being non-nullable when using value types like short, int, long or Guid.
// If primary/foreign keys are strings, limit their length to ensure
// they can be safely used in indexes, specially when the underlying
// provider is known to not restrict the default length (e.g MySQL).
if (typeof(TKey) == typeof(string))
{
builder.Property(application => application.Id)
.HasMaxLength(50);
}
builder.HasKey(application => application.Id);
builder.HasIndex(application => application.ClientId)
.IsUnique();
builder.Property(application => application.ClientId)
.HasMaxLength(100)
.IsRequired();
builder.Property(application => application.ConcurrencyToken)
.HasMaxLength(50)
.IsConcurrencyToken();
builder.Property(application => application.Type)
.HasMaxLength(25)
.IsRequired();
builder.HasMany(application => application.Authorizations)
.WithOne(authorization => authorization.Application)
.HasForeignKey("ApplicationId")
.HasForeignKey(nameof(OpenIddictAuthorization.Application) + nameof(OpenIddictApplication.Id))
.IsRequired(required: false);
builder.HasMany(application => application.Tokens)
.WithOne(token => token.Application)
.HasForeignKey("ApplicationId")
.HasForeignKey(nameof(OpenIddictToken.Application) + nameof(OpenIddictApplication.Id))
.IsRequired(required: false);
builder.ToTable("OpenIddictApplications");

24
src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictAuthorizationConfiguration.cs

@ -6,6 +6,7 @@
using System;
using System.ComponentModel;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using OpenIddict.EntityFrameworkCore.Models;
@ -26,7 +27,7 @@ namespace OpenIddict.EntityFrameworkCore
where TToken : OpenIddictToken<TKey, TApplication, TAuthorization>
where TKey : IEquatable<TKey>
{
public void Configure(EntityTypeBuilder<TAuthorization> builder)
public void Configure([NotNull] EntityTypeBuilder<TAuthorization> builder)
{
if (builder == null)
{
@ -37,24 +38,41 @@ namespace OpenIddict.EntityFrameworkCore
// Entity Framework would throw an exception due to the TKey generic parameter
// being non-nullable when using value types like short, int, long or Guid.
// If primary/foreign keys are strings, limit their length to ensure
// they can be safely used in indexes, specially when the underlying
// provider is known to not restrict the default length (e.g MySQL).
if (typeof(TKey) == typeof(string))
{
builder.Property(typeof(string), nameof(OpenIddictAuthorization.Application) +
nameof(OpenIddictApplication.Id))
.HasMaxLength(50);
builder.Property(application => application.Id)
.HasMaxLength(50);
}
builder.HasKey(authorization => authorization.Id);
builder.HasIndex("ApplicationId",
nameof(OpenIddictAuthorization.Scopes),
builder.HasIndex(
nameof(OpenIddictAuthorization.Application) + nameof(OpenIddictApplication.Id),
nameof(OpenIddictAuthorization.Status),
nameof(OpenIddictAuthorization.Subject),
nameof(OpenIddictAuthorization.Type));
builder.Property(authorization => authorization.ConcurrencyToken)
.HasMaxLength(50)
.IsConcurrencyToken();
builder.Property(authorization => authorization.Status)
.HasMaxLength(25)
.IsRequired();
builder.Property(authorization => authorization.Subject)
.HasMaxLength(450)
.IsRequired();
builder.Property(authorization => authorization.Type)
.HasMaxLength(25)
.IsRequired();
builder.HasMany(authorization => authorization.Tokens)

14
src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictScopeConfiguration.cs

@ -6,6 +6,7 @@
using System;
using System.ComponentModel;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using OpenIddict.EntityFrameworkCore.Models;
@ -22,7 +23,7 @@ namespace OpenIddict.EntityFrameworkCore
where TScope : OpenIddictScope<TKey>
where TKey : IEquatable<TKey>
{
public void Configure(EntityTypeBuilder<TScope> builder)
public void Configure([NotNull] EntityTypeBuilder<TScope> builder)
{
if (builder == null)
{
@ -33,15 +34,26 @@ namespace OpenIddict.EntityFrameworkCore
// Entity Framework would throw an exception due to the TKey generic parameter
// being non-nullable when using value types like short, int, long or Guid.
// If primary/foreign keys are strings, limit their length to ensure
// they can be safely used in indexes, specially when the underlying
// provider is known to not restrict the default length (e.g MySQL).
if (typeof(TKey) == typeof(string))
{
builder.Property(scope => scope.Id)
.HasMaxLength(50);
}
builder.HasKey(scope => scope.Id);
builder.HasIndex(scope => scope.Name)
.IsUnique();
builder.Property(scope => scope.ConcurrencyToken)
.HasMaxLength(50)
.IsConcurrencyToken();
builder.Property(scope => scope.Name)
.HasMaxLength(200)
.IsRequired();
builder.ToTable("OpenIddictScopes");

34
src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictTokenConfiguration.cs

@ -6,6 +6,7 @@
using System;
using System.ComponentModel;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using OpenIddict.EntityFrameworkCore.Models;
@ -26,7 +27,7 @@ namespace OpenIddict.EntityFrameworkCore
where TAuthorization : OpenIddictAuthorization<TKey, TApplication, TToken>
where TKey : IEquatable<TKey>
{
public void Configure(EntityTypeBuilder<TToken> builder)
public void Configure([NotNull] EntityTypeBuilder<TToken> builder)
{
if (builder == null)
{
@ -37,23 +38,52 @@ namespace OpenIddict.EntityFrameworkCore
// Entity Framework would throw an exception due to the TKey generic parameter
// being non-nullable when using value types like short, int, long or Guid.
// If primary/foreign keys are strings, limit their length to ensure
// they can be safely used in indexes, specially when the underlying
// provider is known to not restrict the default length (e.g MySQL).
if (typeof(TKey) == typeof(string))
{
builder.Property(typeof(string), nameof(OpenIddictToken.Application) +
nameof(OpenIddictApplication.Id))
.HasMaxLength(50);
builder.Property(typeof(string), nameof(OpenIddictToken.Authorization) +
nameof(OpenIddictApplication.Id))
.HasMaxLength(50);
builder.Property(token => token.Id)
.HasMaxLength(50);
}
builder.HasKey(token => token.Id);
builder.HasIndex(token => token.ReferenceId)
.IsUnique();
builder.HasIndex("ApplicationId",
builder.HasIndex(
nameof(OpenIddictToken.Application) + nameof(OpenIddictApplication.Id),
nameof(OpenIddictToken.Status),
nameof(OpenIddictToken.Subject),
nameof(OpenIddictToken.Type));
builder.Property(token => token.ConcurrencyToken)
.HasMaxLength(50)
.IsConcurrencyToken();
builder.Property(token => token.ReferenceId)
.HasMaxLength(100);
builder.Property(token => token.Status)
.HasMaxLength(25)
.IsRequired();
builder.Property(token => token.Subject)
.HasMaxLength(450)
.IsRequired();
builder.Property(token => token.Type)
.HasMaxLength(25)
.IsRequired();
builder.ToTable("OpenIddictTokens");

1
src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj

@ -20,7 +20,6 @@
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="$(JetBrainsVersion)" PrivateAssets="All" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="$(AspNetCoreVersion)" />
</ItemGroup>
<ItemGroup>

2
src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs

@ -34,8 +34,6 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentNullException(nameof(builder));
}
builder.Services.AddMemoryCache();
// Since Entity Framework Core may be used with databases performing case-insensitive
// or culture-sensitive comparisons, ensure the additional filtering logic is enforced
// in case case-sensitive stores were registered before this extension was called.

3
src/OpenIddict.MongoDb/OpenIddict.MongoDb.csproj

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\build\packages.props" />
@ -22,7 +22,6 @@
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="$(JetBrainsVersion)" PrivateAssets="All" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="$(AspNetCoreVersion)" />
<PackageReference Include="MongoDB.Driver" Version="$(MongoDbVersion)" />
</ItemGroup>

15
src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs

@ -19,7 +19,7 @@ namespace OpenIddict.MongoDb
/// <summary>
/// Exposes the MongoDB database used by the OpenIddict stores.
/// </summary>
public class OpenIddictMongoDbContext : IOpenIddictMongoDbContext
public class OpenIddictMongoDbContext : IOpenIddictMongoDbContext, IDisposable
{
private readonly IOptions<OpenIddictMongoDbOptions> _options;
private readonly IServiceProvider _provider;
@ -35,6 +35,11 @@ namespace OpenIddict.MongoDb
_semaphore = new SemaphoreSlim(1);
}
/// <summary>
/// Disposes the semaphore held by this instance.
/// </summary>
public void Dispose() => _semaphore.Dispose();
/// <summary>
/// Gets the <see cref="IMongoDatabase"/>.
/// </summary>
@ -49,6 +54,14 @@ namespace OpenIddict.MongoDb
return new ValueTask<IMongoDatabase>(_database);
}
if (cancellationToken.IsCancellationRequested)
{
var source = new TaskCompletionSource<IMongoDatabase>();
source.SetException(new OperationCanceledException(cancellationToken));
return new ValueTask<IMongoDatabase>(source.Task);
}
async Task<IMongoDatabase> ExecuteAsync()
{
var options = _options.Value;

2
src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs

@ -299,6 +299,8 @@ namespace OpenIddict.MongoDb
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TAuthorization>(Options.Value.AuthorizationsCollectionName);
// Note: Enumerable.All() is deliberately used without the extension method syntax to ensure
// ImmutableArrayExtensions.All() (which is not supported by MongoDB) is not used instead.
return ImmutableArray.CreateRange(await collection.Find(authorization =>
authorization.Subject == subject &&
authorization.ApplicationId == ObjectId.Parse(client) &&

4
src/OpenIddict.Mvc/Internal/OpenIddictMvcBinder.cs

@ -33,9 +33,7 @@ namespace OpenIddict.Mvc.Internal
/// directly from your code. This API may change or be removed in future minor releases.
/// </summary>
public OpenIddictMvcBinder([NotNull] IOptions<OpenIddictMvcOptions> options)
{
_options = options;
}
=> _options = options;
/// <summary>
/// Tries to bind a model from the request.

14
test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkExtensionsTests.cs

@ -42,20 +42,6 @@ namespace OpenIddict.EntityFramework.Tests
Assert.Equal("configuration", exception.ParamName);
}
[Fact]
public void UseEntityFramework_RegistersCachingServices()
{
// Arrange
var services = new ServiceCollection();
var builder = new OpenIddictCoreBuilder(services);
// Act
builder.UseEntityFramework();
// Assert
Assert.Contains(services, service => service.ServiceType == typeof(IMemoryCache));
}
[Fact]
public void UseEntityFramework_RegistersDefaultEntities()
{

14
test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreExtensionsTests.cs

@ -45,20 +45,6 @@ namespace OpenIddict.EntityFrameworkCore.Tests
Assert.Equal("configuration", exception.ParamName);
}
[Fact]
public void UseEntityFrameworkCore_RegistersCachingServices()
{
// Arrange
var services = new ServiceCollection();
var builder = new OpenIddictCoreBuilder(services);
// Act
builder.UseEntityFrameworkCore();
// Assert
Assert.Contains(services, service => service.ServiceType == typeof(IMemoryCache));
}
[Fact]
public void UseEntityFrameworkCore_RegistersDefaultEntities()
{

21
test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbContextTests.cs

@ -20,6 +20,27 @@ namespace OpenIddict.MongoDb.Tests
{
public class OpenIddictMongoDbContextTests
{
[Fact]
public async Task GetDatabaseAsync_ThrowsAnExceptionForCanceledToken()
{
// Arrange
var services = new ServiceCollection();
var provider = services.BuildServiceProvider();
var options = Mock.Of<IOptions<OpenIddictMongoDbOptions>>();
var token = new CancellationToken(canceled: true);
var context = new OpenIddictMongoDbContext(options, provider);
// Act and assert
var exception = await Assert.ThrowsAsync<OperationCanceledException>(async delegate
{
await context.GetDatabaseAsync(token);
});
Assert.Equal(token, exception.CancellationToken);
}
[Fact]
public async Task GetDatabaseAsync_ThrowsAnExceptionForNullOptions()
{

Loading…
Cancel
Save