Browse Source

Update the caches to use expiration tokens to remove invalid entries

pull/711/head
Kévin Chalet 8 years ago
parent
commit
3a9b08b109
  1. 166
      src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs
  2. 203
      src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs
  3. 142
      src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs
  4. 251
      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

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

@ -5,12 +5,14 @@
*/ */
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
namespace OpenIddict.Core namespace OpenIddict.Core
@ -21,7 +23,8 @@ namespace OpenIddict.Core
/// <typeparam name="TApplication">The type of the Application entity.</typeparam> /// <typeparam name="TApplication">The type of the Application entity.</typeparam>
public class OpenIddictApplicationCache<TApplication> : IOpenIddictApplicationCache<TApplication>, IDisposable where TApplication : class public class OpenIddictApplicationCache<TApplication> : IOpenIddictApplicationCache<TApplication>, IDisposable where TApplication : class
{ {
private readonly IMemoryCache _cache; private readonly MemoryCache _cache;
private readonly ConcurrentDictionary<string, Lazy<CancellationTokenSource>> _signals;
private readonly IOpenIddictApplicationStore<TApplication> _store; private readonly IOpenIddictApplicationStore<TApplication> _store;
public OpenIddictApplicationCache( public OpenIddictApplicationCache(
@ -33,6 +36,7 @@ namespace OpenIddict.Core
SizeLimit = options.CurrentValue.EntityCacheLimit SizeLimit = options.CurrentValue.EntityCacheLimit
}); });
_signals = new ConcurrentDictionary<string, Lazy<CancellationTokenSource>>(StringComparer.Ordinal);
_store = resolver.Get<TApplication>(); _store = resolver.Get<TApplication>();
} }
@ -51,14 +55,51 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(application)); throw new ArgumentNullException(nameof(application));
} }
_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 using (var entry = _cache.CreateEntry(new
{ {
Method = nameof(FindByIdAsync), Method = nameof(FindByIdAsync),
Identifier = await _store.GetIdAsync(application, cancellationToken) Identifier = await _store.GetIdAsync(application, cancellationToken)
})) }))
{ {
entry.SetSize(1L); entry.AddExpirationToken(signal)
entry.SetValue(application); .SetSize(1L)
.SetValue(application);
} }
using (var entry = _cache.CreateEntry(new using (var entry = _cache.CreateEntry(new
@ -67,15 +108,24 @@ namespace OpenIddict.Core
Identifier = await _store.GetClientIdAsync(application, cancellationToken) Identifier = await _store.GetClientIdAsync(application, cancellationToken)
})) }))
{ {
entry.SetSize(1L); entry.AddExpirationToken(signal)
entry.SetValue(application); .SetSize(1L)
.SetValue(application);
} }
} }
/// <summary> /// <summary>
/// Disposes the cache held by this instance. /// Disposes the resources held by this instance.
/// </summary> /// </summary>
public void Dispose() => _cache.Dispose(); public void Dispose()
{
foreach (var signal in _signals)
{
signal.Value.Value.Dispose();
}
_cache.Dispose();
}
/// <summary> /// <summary>
/// Retrieves an application using its client identifier. /// Retrieves an application using its client identifier.
@ -113,6 +163,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters)) 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.SetSize(1L); entry.SetSize(1L);
entry.SetValue(application); entry.SetValue(application);
} }
@ -159,6 +220,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters)) 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.SetSize(1L); entry.SetSize(1L);
entry.SetValue(application); entry.SetValue(application);
} }
@ -206,6 +278,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters)) 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.SetSize(applications.Length); entry.SetSize(applications.Length);
entry.SetValue(applications); entry.SetValue(applications);
} }
@ -253,6 +336,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters)) 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.SetSize(applications.Length); entry.SetSize(applications.Length);
entry.SetValue(applications); entry.SetValue(applications);
} }
@ -278,35 +372,53 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(application)); throw new ArgumentNullException(nameof(application));
} }
_cache.Remove(new var identifier = await _store.GetIdAsync(application, cancellationToken);
if (string.IsNullOrEmpty(identifier))
{ {
Method = nameof(FindByClientIdAsync), throw new InvalidOperationException("The application identifier cannot be extracted.");
Identifier = await _store.GetClientIdAsync(application, cancellationToken) }
});
_cache.Remove(new if (_signals.TryGetValue(identifier, out Lazy<CancellationTokenSource> signal))
{ {
Method = nameof(FindByIdAsync), signal.Value.Cancel();
Identifier = await _store.GetIdAsync(application, cancellationToken)
});
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 throw new ArgumentNullException(nameof(application));
{
Method = nameof(FindByPostLogoutRedirectUriAsync),
Address = address
});
} }
foreach (var address in await _store.GetRedirectUrisAsync(application, cancellationToken)) var identifier = await _store.GetIdAsync(application, cancellationToken);
if (string.IsNullOrEmpty(identifier))
{ {
_cache.Remove(new throw new InvalidOperationException("The application identifier cannot be extracted.");
{
Method = nameof(FindByRedirectUriAsync),
Address = address
});
} }
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);
} }
} }
} }

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

@ -5,12 +5,14 @@
*/ */
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
namespace OpenIddict.Core namespace OpenIddict.Core
@ -21,7 +23,8 @@ namespace OpenIddict.Core
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam> /// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
public class OpenIddictAuthorizationCache<TAuthorization> : IOpenIddictAuthorizationCache<TAuthorization>, IDisposable where TAuthorization : class public class OpenIddictAuthorizationCache<TAuthorization> : IOpenIddictAuthorizationCache<TAuthorization>, IDisposable where TAuthorization : class
{ {
private readonly IMemoryCache _cache; private readonly MemoryCache _cache;
private readonly ConcurrentDictionary<string, Lazy<CancellationTokenSource>> _signals;
private readonly IOpenIddictAuthorizationStore<TAuthorization> _store; private readonly IOpenIddictAuthorizationStore<TAuthorization> _store;
public OpenIddictAuthorizationCache( public OpenIddictAuthorizationCache(
@ -33,6 +36,7 @@ namespace OpenIddict.Core
SizeLimit = options.CurrentValue.EntityCacheLimit SizeLimit = options.CurrentValue.EntityCacheLimit
}); });
_signals = new ConcurrentDictionary<string, Lazy<CancellationTokenSource>>(StringComparer.Ordinal);
_store = resolver.Get<TAuthorization>(); _store = resolver.Get<TAuthorization>();
} }
@ -51,21 +55,78 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(authorization)); throw new ArgumentNullException(nameof(authorization));
} }
_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 using (var entry = _cache.CreateEntry(new
{ {
Method = nameof(FindByIdAsync), Method = nameof(FindByIdAsync),
Identifier = await _store.GetIdAsync(authorization, cancellationToken) Identifier = await _store.GetIdAsync(authorization, cancellationToken)
})) }))
{ {
entry.SetSize(1L); entry.AddExpirationToken(signal)
entry.SetValue(authorization); .SetSize(1L)
.SetValue(authorization);
} }
} }
/// <summary> /// <summary>
/// Disposes the cache held by this instance. /// Disposes the resources held by this instance.
/// </summary> /// </summary>
public void Dispose() => _cache.Dispose(); public void Dispose()
{
foreach (var signal in _signals)
{
signal.Value.Value.Dispose();
}
_cache.Dispose();
}
/// <summary> /// <summary>
/// Retrieves the authorizations corresponding to the specified /// Retrieves the authorizations corresponding to the specified
@ -112,6 +173,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters)) 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.SetSize(authorizations.Length); entry.SetSize(authorizations.Length);
entry.SetValue(authorizations); entry.SetValue(authorizations);
} }
@ -174,6 +246,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters)) 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.SetSize(authorizations.Length); entry.SetSize(authorizations.Length);
entry.SetValue(authorizations); entry.SetValue(authorizations);
} }
@ -243,6 +326,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters)) 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.SetSize(authorizations.Length); entry.SetSize(authorizations.Length);
entry.SetValue(authorizations); entry.SetValue(authorizations);
} }
@ -345,6 +439,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters)) 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.SetSize(authorizations.Length); entry.SetSize(authorizations.Length);
entry.SetValue(authorizations); entry.SetValue(authorizations);
} }
@ -391,6 +496,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters)) 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.SetSize(1L); entry.SetSize(1L);
entry.SetValue(authorization); entry.SetValue(authorization);
} }
@ -438,6 +554,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters)) 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.SetSize(authorizations.Length); entry.SetSize(authorizations.Length);
entry.SetValue(authorizations); entry.SetValue(authorizations);
} }
@ -463,47 +590,53 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(authorization)); throw new ArgumentNullException(nameof(authorization));
} }
_cache.Remove(new var identifier = await _store.GetIdAsync(authorization, cancellationToken);
if (string.IsNullOrEmpty(identifier))
{ {
Method = nameof(FindAsync), throw new InvalidOperationException("The application identifier cannot be extracted.");
Subject = await _store.GetSubjectAsync(authorization, cancellationToken), }
Client = await _store.GetApplicationIdAsync(authorization, cancellationToken)
});
_cache.Remove(new if (_signals.TryGetValue(identifier, out Lazy<CancellationTokenSource> signal))
{ {
Method = nameof(FindAsync), signal.Value.Cancel();
Subject = await _store.GetSubjectAsync(authorization, cancellationToken),
Client = await _store.GetApplicationIdAsync(authorization, cancellationToken),
Status = await _store.GetStatusAsync(authorization, cancellationToken)
});
_cache.Remove(new _signals.TryRemove(identifier, out signal);
{ }
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 /// <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), throw new ArgumentNullException(nameof(authorization));
Identifier = await _store.GetApplicationIdAsync(authorization, cancellationToken) }
});
_cache.Remove(new var identifier = await _store.GetIdAsync(authorization, cancellationToken);
if (string.IsNullOrEmpty(identifier))
{ {
Method = nameof(FindByIdAsync), throw new InvalidOperationException("The authorization identifier cannot be extracted.");
Identifier = await _store.GetIdAsync(authorization, cancellationToken) }
});
_cache.Remove(new var signal = _signals.GetOrAdd(identifier, delegate
{ {
Method = nameof(FindBySubjectAsync), // Note: a Lazy<CancellationTokenSource> is used here to ensure only one CancellationTokenSource
Subject = await _store.GetSubjectAsync(authorization, cancellationToken) // 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);
} }
} }
} }

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

@ -5,6 +5,7 @@
*/ */
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@ -12,6 +13,7 @@ using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
namespace OpenIddict.Core namespace OpenIddict.Core
@ -22,7 +24,8 @@ namespace OpenIddict.Core
/// <typeparam name="TScope">The type of the Scope entity.</typeparam> /// <typeparam name="TScope">The type of the Scope entity.</typeparam>
public class OpenIddictScopeCache<TScope> : IOpenIddictScopeCache<TScope>, IDisposable where TScope : class public class OpenIddictScopeCache<TScope> : IOpenIddictScopeCache<TScope>, IDisposable where TScope : class
{ {
private readonly IMemoryCache _cache; private readonly MemoryCache _cache;
private readonly ConcurrentDictionary<string, Lazy<CancellationTokenSource>> _signals;
private readonly IOpenIddictScopeStore<TScope> _store; private readonly IOpenIddictScopeStore<TScope> _store;
public OpenIddictScopeCache( public OpenIddictScopeCache(
@ -34,6 +37,7 @@ namespace OpenIddict.Core
SizeLimit = options.CurrentValue.EntityCacheLimit SizeLimit = options.CurrentValue.EntityCacheLimit
}); });
_signals = new ConcurrentDictionary<string, Lazy<CancellationTokenSource>>(StringComparer.Ordinal);
_store = resolver.Get<TScope>(); _store = resolver.Get<TScope>();
} }
@ -52,14 +56,42 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(scope)); throw new ArgumentNullException(nameof(scope));
} }
_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 using (var entry = _cache.CreateEntry(new
{ {
Method = nameof(FindByIdAsync), Method = nameof(FindByIdAsync),
Identifier = await _store.GetIdAsync(scope, cancellationToken) Identifier = await _store.GetIdAsync(scope, cancellationToken)
})) }))
{ {
entry.SetSize(1L); entry.AddExpirationToken(signal)
entry.SetValue(scope); .SetSize(1L)
.SetValue(scope);
} }
using (var entry = _cache.CreateEntry(new using (var entry = _cache.CreateEntry(new
@ -68,15 +100,24 @@ namespace OpenIddict.Core
Name = await _store.GetNameAsync(scope, cancellationToken) Name = await _store.GetNameAsync(scope, cancellationToken)
})) }))
{ {
entry.SetSize(1L); entry.AddExpirationToken(signal)
entry.SetValue(scope); .SetSize(1L)
.SetValue(scope);
} }
} }
/// <summary> /// <summary>
/// Disposes the cache held by this instance. /// Disposes the resources held by this instance.
/// </summary> /// </summary>
public void Dispose() => _cache.Dispose(); public void Dispose()
{
foreach (var signal in _signals)
{
signal.Value.Value.Dispose();
}
_cache.Dispose();
}
/// <summary> /// <summary>
/// Retrieves a scope using its unique identifier. /// Retrieves a scope using its unique identifier.
@ -114,6 +155,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters)) 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.SetSize(1L); entry.SetSize(1L);
entry.SetValue(scope); entry.SetValue(scope);
} }
@ -160,6 +212,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters)) 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.SetSize(1L); entry.SetSize(1L);
entry.SetValue(scope); entry.SetValue(scope);
} }
@ -244,6 +307,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters)) 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.SetSize(scopes.Length); entry.SetSize(scopes.Length);
entry.SetValue(scopes); entry.SetValue(scopes);
} }
@ -269,26 +343,52 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(scope)); throw new ArgumentNullException(nameof(scope));
} }
_cache.Remove(new var identifier = await _store.GetIdAsync(scope, cancellationToken);
if (string.IsNullOrEmpty(identifier))
{ {
Method = nameof(FindByIdAsync), throw new InvalidOperationException("The application identifier cannot be extracted.");
Identifier = await _store.GetIdAsync(scope, cancellationToken) }
});
_cache.Remove(new if (_signals.TryGetValue(identifier, out Lazy<CancellationTokenSource> signal))
{ {
Method = nameof(FindByNameAsync), signal.Value.Cancel();
Name = await _store.GetNameAsync(scope, cancellationToken)
});
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 throw new ArgumentNullException(nameof(scope));
{
Method = nameof(FindByResourceAsync),
Resource = resource
});
} }
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);
} }
} }
} }

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

@ -5,12 +5,14 @@
*/ */
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
namespace OpenIddict.Core namespace OpenIddict.Core
@ -21,7 +23,8 @@ namespace OpenIddict.Core
/// <typeparam name="TToken">The type of the Token entity.</typeparam> /// <typeparam name="TToken">The type of the Token entity.</typeparam>
public class OpenIddictTokenCache<TToken> : IOpenIddictTokenCache<TToken>, IDisposable where TToken : class public class OpenIddictTokenCache<TToken> : IOpenIddictTokenCache<TToken>, IDisposable where TToken : class
{ {
private readonly IMemoryCache _cache; private readonly MemoryCache _cache;
private readonly ConcurrentDictionary<string, Lazy<CancellationTokenSource>> _signals;
private readonly IOpenIddictTokenStore<TToken> _store; private readonly IOpenIddictTokenStore<TToken> _store;
public OpenIddictTokenCache( public OpenIddictTokenCache(
@ -33,6 +36,7 @@ namespace OpenIddict.Core
SizeLimit = options.CurrentValue.EntityCacheLimit SizeLimit = options.CurrentValue.EntityCacheLimit
}); });
_signals = new ConcurrentDictionary<string, Lazy<CancellationTokenSource>>(StringComparer.Ordinal);
_store = resolver.Get<TToken>(); _store = resolver.Get<TToken>();
} }
@ -51,14 +55,75 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(token)); 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
{
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 using (var entry = _cache.CreateEntry(new
{ {
Method = nameof(FindByIdAsync), Method = nameof(FindByIdAsync),
Identifier = await _store.GetIdAsync(token, cancellationToken) Identifier = await _store.GetIdAsync(token, cancellationToken)
})) }))
{ {
entry.SetSize(1L); entry.AddExpirationToken(signal)
entry.SetValue(token); .SetSize(1L)
.SetValue(token);
} }
using (var entry = _cache.CreateEntry(new using (var entry = _cache.CreateEntry(new
@ -67,15 +132,24 @@ namespace OpenIddict.Core
Identifier = await _store.GetReferenceIdAsync(token, cancellationToken) Identifier = await _store.GetReferenceIdAsync(token, cancellationToken)
})) }))
{ {
entry.SetSize(1L); entry.AddExpirationToken(signal)
entry.SetValue(token); .SetSize(1L)
.SetValue(token);
} }
} }
/// <summary> /// <summary>
/// Disposes the cache held by this instance. /// Disposes the resources held by this instance.
/// </summary> /// </summary>
public void Dispose() => _cache.Dispose(); public void Dispose()
{
foreach (var signal in _signals)
{
signal.Value.Value.Dispose();
}
_cache.Dispose();
}
/// <summary> /// <summary>
/// Retrieves the tokens corresponding to the specified /// Retrieves the tokens corresponding to the specified
@ -122,6 +196,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters)) 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.SetSize(tokens.Length); entry.SetSize(tokens.Length);
entry.SetValue(tokens); entry.SetValue(tokens);
} }
@ -184,6 +269,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters)) 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.SetSize(tokens.Length); entry.SetSize(tokens.Length);
entry.SetValue(tokens); entry.SetValue(tokens);
} }
@ -253,6 +349,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters)) 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.SetSize(tokens.Length); entry.SetSize(tokens.Length);
entry.SetValue(tokens); entry.SetValue(tokens);
} }
@ -300,6 +407,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters)) 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.SetSize(tokens.Length); entry.SetSize(tokens.Length);
entry.SetValue(tokens); entry.SetValue(tokens);
} }
@ -347,6 +465,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters)) 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.SetSize(tokens.Length); entry.SetSize(tokens.Length);
entry.SetValue(tokens); entry.SetValue(tokens);
} }
@ -393,6 +522,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters)) 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.SetSize(1L); entry.SetSize(1L);
entry.SetValue(token); entry.SetValue(token);
} }
@ -440,6 +580,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters)) 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.SetSize(1L); entry.SetSize(1L);
entry.SetValue(token); entry.SetValue(token);
} }
@ -486,6 +637,17 @@ namespace OpenIddict.Core
using (var entry = _cache.CreateEntry(parameters)) 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.SetSize(tokens.Length); entry.SetSize(tokens.Length);
entry.SetValue(tokens); entry.SetValue(tokens);
} }
@ -511,59 +673,52 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(token)); throw new ArgumentNullException(nameof(token));
} }
_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)
});
_cache.Remove(new
{ {
Method = nameof(FindAsync), throw new InvalidOperationException("The application identifier cannot be extracted.");
Subject = await _store.GetSubjectAsync(token, cancellationToken), }
Client = await _store.GetApplicationIdAsync(token, cancellationToken),
Status = await _store.GetStatusAsync(token, cancellationToken)
});
_cache.Remove(new if (_signals.TryGetValue(identifier, out Lazy<CancellationTokenSource> signal))
{ {
Method = nameof(FindAsync), signal.Value.Cancel();
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 _signals.TryRemove(identifier, out signal);
{ }
Method = nameof(FindByApplicationIdAsync), }
Identifier = await _store.GetApplicationIdAsync(token, cancellationToken)
});
_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), throw new ArgumentNullException(nameof(token));
Identifier = await _store.GetAuthorizationIdAsync(token, cancellationToken) }
});
_cache.Remove(new var identifier = await _store.GetIdAsync(token, cancellationToken);
if (string.IsNullOrEmpty(identifier))
{ {
Method = nameof(FindByIdAsync), throw new InvalidOperationException("The token identifier cannot be extracted.");
Identifier = await _store.GetIdAsync(token, cancellationToken) }
});
_cache.Remove(new var signal = _signals.GetOrAdd(identifier, delegate
{ {
Method = nameof(FindByReferenceIdAsync), // Note: a Lazy<CancellationTokenSource> is used here to ensure only one CancellationTokenSource
Identifier = await _store.GetReferenceIdAsync(token, cancellationToken) // 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 return new CancellationChangeToken(signal.Value.Token);
{
Method = nameof(FindBySubjectAsync),
Subject = await _store.GetSubjectAsync(token, cancellationToken)
});
} }
} }
} }

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

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

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

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

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

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

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

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

Loading…
Cancel
Save