diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs
index 39ab158e..b03a3411 100644
--- a/samples/Mvc.Server/Startup.cs
+++ b/samples/Mvc.Server/Startup.cs
@@ -1,6 +1,5 @@
using System;
using System.Threading.Tasks;
-using AspNet.Security.OpenIdConnect.Primitives;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
diff --git a/src/OpenIddict.Abstractions/Caches/IOpenIddictApplicationCache.cs b/src/OpenIddict.Abstractions/Caches/IOpenIddictApplicationCache.cs
new file mode 100644
index 00000000..023d9d04
--- /dev/null
+++ b/src/OpenIddict.Abstractions/Caches/IOpenIddictApplicationCache.cs
@@ -0,0 +1,86 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/openiddict/openiddict-core for more information concerning
+ * the license and the contributors participating to this project.
+ */
+
+using System.Collections.Immutable;
+using System.Threading;
+using System.Threading.Tasks;
+using JetBrains.Annotations;
+
+namespace OpenIddict.Abstractions
+{
+ ///
+ /// Provides methods allowing to cache applications after retrieving them from the store.
+ ///
+ /// The type of the Application entity.
+ public interface IOpenIddictApplicationCache where TApplication : class
+ {
+ ///
+ /// Add the specified application to the cache.
+ ///
+ /// The application to add to the cache.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ Task AddAsync([NotNull] TApplication application, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves an application using its client identifier.
+ ///
+ /// The client identifier associated with the application.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the client application corresponding to the identifier.
+ ///
+ ValueTask FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves an application using its unique identifier.
+ ///
+ /// The unique identifier associated with the application.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the client application corresponding to the identifier.
+ ///
+ ValueTask FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves all the applications associated with the specified redirect_uri.
+ ///
+ /// The redirect_uri associated with the applications.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the client applications corresponding to the specified redirect_uri.
+ ///
+ ValueTask> FindByPostLogoutRedirectUriAsync(
+ [NotNull] string address, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves all the applications associated with the specified redirect_uri.
+ ///
+ /// The redirect_uri associated with the applications.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the client applications corresponding to the specified redirect_uri.
+ ///
+ ValueTask> FindByRedirectUriAsync(
+ [NotNull] string address, CancellationToken cancellationToken);
+
+ ///
+ /// Removes the specified application from the cache.
+ ///
+ /// The application to remove from the cache.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ Task RemoveAsync([NotNull] TApplication application, CancellationToken cancellationToken);
+ }
+}
diff --git a/src/OpenIddict.Abstractions/Caches/IOpenIddictAuthorizationCache.cs b/src/OpenIddict.Abstractions/Caches/IOpenIddictAuthorizationCache.cs
new file mode 100644
index 00000000..779fccd3
--- /dev/null
+++ b/src/OpenIddict.Abstractions/Caches/IOpenIddictAuthorizationCache.cs
@@ -0,0 +1,135 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/openiddict/openiddict-core for more information concerning
+ * the license and the contributors participating to this project.
+ */
+
+using System.Collections.Immutable;
+using System.Threading;
+using System.Threading.Tasks;
+using JetBrains.Annotations;
+
+namespace OpenIddict.Abstractions
+{
+ ///
+ /// Provides methods allowing to cache authorizations after retrieving them from the store.
+ ///
+ /// The type of the Authorization entity.
+ public interface IOpenIddictAuthorizationCache where TAuthorization : class
+ {
+ ///
+ /// Add the specified authorization to the cache.
+ ///
+ /// The authorization to add to the cache.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ Task AddAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves the authorizations corresponding to the specified
+ /// subject and associated with the application identifier.
+ ///
+ /// The subject associated with the authorization.
+ /// The client associated with the authorization.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the authorizations corresponding to the subject/client.
+ ///
+ ValueTask> FindAsync(
+ [NotNull] string subject, [NotNull] string client, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves the authorizations matching the specified parameters.
+ ///
+ /// The subject associated with the authorization.
+ /// The client associated with the authorization.
+ /// The authorization status.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the authorizations corresponding to the criteria.
+ ///
+ ValueTask> FindAsync(
+ [NotNull] string subject, [NotNull] string client, [NotNull] string status, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves the authorizations matching the specified parameters.
+ ///
+ /// The subject associated with the authorization.
+ /// The client associated with the authorization.
+ /// The authorization status.
+ /// The authorization type.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the authorizations corresponding to the criteria.
+ ///
+ ValueTask> FindAsync(
+ [NotNull] string subject, [NotNull] string client, [NotNull] string status,
+ [NotNull] string type, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves the authorizations matching the specified parameters.
+ ///
+ /// The subject associated with the authorization.
+ /// The client associated with the authorization.
+ /// The authorization status.
+ /// The authorization type.
+ /// The minimal scopes associated with the authorization.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the authorizations corresponding to the criteria.
+ ///
+ ValueTask> FindAsync(
+ [NotNull] string subject, [NotNull] string client, [NotNull] string status,
+ [NotNull] string type, ImmutableArray scopes, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves the list of authorizations corresponding to the specified application identifier.
+ ///
+ /// The application identifier associated with the authorizations.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the authorizations corresponding to the specified application.
+ ///
+ ValueTask> FindByApplicationIdAsync(
+ [NotNull] string identifier, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves an authorization using its unique identifier.
+ ///
+ /// The unique identifier associated with the authorization.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the authorization corresponding to the identifier.
+ ///
+ ValueTask FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves all the authorizations corresponding to the specified subject.
+ ///
+ /// The subject associated with the authorization.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the authorizations corresponding to the specified subject.
+ ///
+ ValueTask> FindBySubjectAsync([NotNull] string subject, CancellationToken cancellationToken);
+
+ ///
+ /// Removes the specified authorization from the cache.
+ ///
+ /// The authorization to remove from the cache.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ Task RemoveAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken);
+ }
+}
diff --git a/src/OpenIddict.Abstractions/Caches/IOpenIddictScopeCache.cs b/src/OpenIddict.Abstractions/Caches/IOpenIddictScopeCache.cs
new file mode 100644
index 00000000..44dc3cfe
--- /dev/null
+++ b/src/OpenIddict.Abstractions/Caches/IOpenIddictScopeCache.cs
@@ -0,0 +1,84 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/openiddict/openiddict-core for more information concerning
+ * the license and the contributors participating to this project.
+ */
+
+using System.Collections.Immutable;
+using System.Threading;
+using System.Threading.Tasks;
+using JetBrains.Annotations;
+
+namespace OpenIddict.Abstractions
+{
+ ///
+ /// Provides methods allowing to cache scopes after retrieving them from the store.
+ ///
+ /// The type of the Scope entity.
+ public interface IOpenIddictScopeCache where TScope : class
+ {
+ ///
+ /// Add the specified scope to the cache.
+ ///
+ /// The scope to add to the cache.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ Task AddAsync([NotNull] TScope scope, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves a scope using its unique identifier.
+ ///
+ /// The unique identifier associated with the scope.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the scope corresponding to the identifier.
+ ///
+ ValueTask FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves a scope using its name.
+ ///
+ /// The name associated with the scope.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the scope corresponding to the specified name.
+ ///
+ ValueTask FindByNameAsync([NotNull] string name, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves a list of scopes using their name.
+ ///
+ /// The names associated with the scopes.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the scopes corresponding to the specified names.
+ ///
+ ValueTask> FindByNamesAsync(ImmutableArray names, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves all the scopes that contain the specified resource.
+ ///
+ /// The resource associated with the scopes.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the scopes associated with the specified resource.
+ ///
+ ValueTask> FindByResourceAsync([NotNull] string resource, CancellationToken cancellationToken);
+
+ ///
+ /// Removes the specified scope from the cache.
+ ///
+ /// The scope to remove from the cache.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ Task RemoveAsync([NotNull] TScope scope, CancellationToken cancellationToken);
+ }
+}
diff --git a/src/OpenIddict.Abstractions/Caches/IOpenIddictTokenCache.cs b/src/OpenIddict.Abstractions/Caches/IOpenIddictTokenCache.cs
new file mode 100644
index 00000000..2b4aef2c
--- /dev/null
+++ b/src/OpenIddict.Abstractions/Caches/IOpenIddictTokenCache.cs
@@ -0,0 +1,141 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/openiddict/openiddict-core for more information concerning
+ * the license and the contributors participating to this project.
+ */
+
+using System.Collections.Immutable;
+using System.Threading;
+using System.Threading.Tasks;
+using JetBrains.Annotations;
+
+namespace OpenIddict.Abstractions
+{
+ ///
+ /// Provides methods allowing to cache tokens after retrieving them from the store.
+ ///
+ /// The type of the Token entity.
+ public interface IOpenIddictTokenCache where TToken : class
+ {
+ ///
+ /// Add the specified token to the cache.
+ ///
+ /// The token to add to the cache.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ Task AddAsync([NotNull] TToken token, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves the tokens corresponding to the specified
+ /// subject and associated with the application identifier.
+ ///
+ /// The subject associated with the token.
+ /// The client associated with the token.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the subject/client.
+ ///
+ ValueTask> FindAsync([NotNull] string subject,
+ [NotNull] string client, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves the tokens matching the specified parameters.
+ ///
+ /// The subject associated with the token.
+ /// The client associated with the token.
+ /// The token status.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the criteria.
+ ///
+ ValueTask> FindAsync(
+ [NotNull] string subject, [NotNull] string client,
+ [NotNull] string status, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves the tokens matching the specified parameters.
+ ///
+ /// The subject associated with the token.
+ /// The client associated with the token.
+ /// The token status.
+ /// The token type.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the criteria.
+ ///
+ ValueTask> FindAsync(
+ [NotNull] string subject, [NotNull] string client,
+ [NotNull] string status, [NotNull] string type, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves the list of tokens corresponding to the specified application identifier.
+ ///
+ /// The application identifier associated with the tokens.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the specified application.
+ ///
+ ValueTask> FindByApplicationIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves the list of tokens corresponding to the specified authorization identifier.
+ ///
+ /// The authorization identifier associated with the tokens.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the specified authorization.
+ ///
+ ValueTask> FindByAuthorizationIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves a token using its unique identifier.
+ ///
+ /// The unique identifier associated with the token.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the token corresponding to the unique identifier.
+ ///
+ ValueTask FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves the list of tokens corresponding to the specified reference identifier.
+ /// Note: the reference identifier may be hashed or encrypted for security reasons.
+ ///
+ /// The reference identifier associated with the tokens.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the specified reference identifier.
+ ///
+ ValueTask FindByReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves the list of tokens corresponding to the specified subject.
+ ///
+ /// The subject associated with the tokens.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the specified subject.
+ ///
+ ValueTask> FindBySubjectAsync([NotNull] string subject, CancellationToken cancellationToken);
+
+ ///
+ /// Removes the specified token from the cache.
+ ///
+ /// The token to remove from the cache.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ Task RemoveAsync([NotNull] TToken token, CancellationToken cancellationToken);
+ }
+}
diff --git a/src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs b/src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs
index e9b059df..7ff16ec2 100644
--- a/src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs
+++ b/src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs
@@ -49,6 +49,8 @@ namespace OpenIddict.Abstractions
///
/// Gets or sets the reference identifier associated with the token.
+ /// Note: depending on the application manager used when creating it,
+ /// this property may be hashed or encrypted for security reasons.
///
public string ReferenceId { get; set; }
diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
index 5dbf934c..20ecd1d7 100644
--- a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
+++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
@@ -318,7 +318,7 @@ namespace OpenIddict.Abstractions
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the token type associated with the specified token.
///
- ValueTask GetTokenTypeAsync([NotNull] object token, CancellationToken cancellationToken = default);
+ ValueTask GetTypeAsync([NotNull] object token, CancellationToken cancellationToken = default);
///
/// Determines whether a given token has already been redemeed.
@@ -382,17 +382,6 @@ namespace OpenIddict.Abstractions
///
Task> ListAsync([NotNull] Func, TState, IQueryable> query, [CanBeNull] TState state, CancellationToken cancellationToken = default);
- ///
- /// Obfuscates the specified reference identifier so it can be safely stored in a database.
- /// By default, this method returns a simple hashed representation computed using SHA256.
- ///
- /// The client identifier.
- /// The that can be used to abort the operation.
- ///
- /// A that can be used to monitor the asynchronous operation.
- ///
- Task ObfuscateReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default);
-
///
/// Populates the specified descriptor using the properties exposed by the token.
///
diff --git a/src/OpenIddict.Abstractions/OpenIddictConstants.cs b/src/OpenIddict.Abstractions/OpenIddictConstants.cs
index 6337b175..9b73b2ba 100644
--- a/src/OpenIddict.Abstractions/OpenIddictConstants.cs
+++ b/src/OpenIddict.Abstractions/OpenIddictConstants.cs
@@ -272,15 +272,12 @@ namespace OpenIddict.Abstractions
public static class Properties
{
- public const string Application = ".application";
public const string AuthenticationTicket = ".authentication_ticket";
public const string Error = ".error";
public const string ErrorDescription = ".error_description";
public const string ErrorUri = ".error_uri";
public const string InternalAuthorizationId = ".internal_authorization_id";
public const string InternalTokenId = ".internal_token_id";
- public const string ReferenceToken = ".reference_token";
- public const string Token = ".token";
}
public static class PropertyTypes
diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs
index 1bdbf7c0..722bbbe6 100644
--- a/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs
+++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs
@@ -297,7 +297,7 @@ namespace OpenIddict.Abstractions
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the token type associated with the specified token.
///
- ValueTask GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken);
+ ValueTask GetTypeAsync([NotNull] TToken token, CancellationToken cancellationToken);
///
/// Instantiates a new token.
diff --git a/src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs b/src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs
new file mode 100644
index 00000000..2c2f0c48
--- /dev/null
+++ b/src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs
@@ -0,0 +1,309 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/openiddict/openiddict-core for more information concerning
+ * the license and the contributors participating to this project.
+ */
+
+using System;
+using System.Collections.Immutable;
+using System.Threading;
+using System.Threading.Tasks;
+using JetBrains.Annotations;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Options;
+using OpenIddict.Abstractions;
+
+namespace OpenIddict.Core
+{
+ ///
+ /// Provides methods allowing to cache applications after retrieving them from the store.
+ ///
+ /// The type of the Application entity.
+ public class OpenIddictApplicationCache : IOpenIddictApplicationCache, IDisposable where TApplication : class
+ {
+ private readonly MemoryCache _cache;
+ private readonly IOpenIddictApplicationStore _store;
+ private readonly IOptions _options;
+
+ public OpenIddictApplicationCache(
+ [NotNull] IOptions options,
+ [NotNull] IOpenIddictApplicationStoreResolver resolver)
+ {
+ _cache = new MemoryCache(new MemoryCacheOptions());
+ _options = options;
+ _store = resolver.Get();
+ }
+
+ ///
+ /// Add the specified application to the cache.
+ ///
+ /// The application to add to the cache.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ public async Task AddAsync([NotNull] TApplication application, CancellationToken cancellationToken)
+ {
+ if (application == null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ if (_cache.Count >= _options.Value.EntityCacheLimit)
+ {
+ _cache.Compact(0.25);
+ }
+
+ using (var entry = _cache.CreateEntry(new
+ {
+ Method = nameof(FindByIdAsync),
+ Identifier = await _store.GetIdAsync(application, cancellationToken)
+ }))
+ {
+ entry.SetValue(application);
+ }
+
+ using (var entry = _cache.CreateEntry(new
+ {
+ Method = nameof(FindByClientIdAsync),
+ Identifier = await _store.GetClientIdAsync(application, cancellationToken)
+ }))
+ {
+ entry.SetValue(application);
+ }
+ }
+
+ ///
+ /// Disposes the cache held by this instance.
+ ///
+ public void Dispose() => _cache.Dispose();
+
+ ///
+ /// Retrieves an application using its client identifier.
+ ///
+ /// The client identifier associated with the application.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the client application corresponding to the identifier.
+ ///
+ public ValueTask FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(identifier))
+ {
+ throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
+ }
+
+ var parameters = new
+ {
+ Method = nameof(FindByClientIdAsync),
+ Identifier = identifier
+ };
+
+ if (_cache.TryGetValue(parameters, out TApplication application))
+ {
+ return new ValueTask(application);
+ }
+
+ async Task ExecuteAsync()
+ {
+ if ((application = await _store.FindByClientIdAsync(identifier, cancellationToken)) != null)
+ {
+ await AddAsync(application, cancellationToken);
+ }
+
+ using (var entry = _cache.CreateEntry(parameters))
+ {
+ entry.SetValue(application);
+ }
+
+ return application;
+ }
+
+ return new ValueTask(ExecuteAsync());
+ }
+
+ ///
+ /// Retrieves an application using its unique identifier.
+ ///
+ /// The unique identifier associated with the application.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the client application corresponding to the identifier.
+ ///
+ public ValueTask FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(identifier))
+ {
+ throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
+ }
+
+ var parameters = new
+ {
+ Method = nameof(FindByIdAsync),
+ Identifier = identifier
+ };
+
+ if (_cache.TryGetValue(parameters, out TApplication application))
+ {
+ return new ValueTask(application);
+ }
+
+ async Task ExecuteAsync()
+ {
+ if ((application = await _store.FindByIdAsync(identifier, cancellationToken)) != null)
+ {
+ await AddAsync(application, cancellationToken);
+ }
+
+ using (var entry = _cache.CreateEntry(parameters))
+ {
+ entry.SetValue(application);
+ }
+
+ return application;
+ }
+
+ return new ValueTask(ExecuteAsync());
+ }
+
+ ///
+ /// Retrieves all the applications associated with the specified post_logout_redirect_uri.
+ ///
+ /// The post_logout_redirect_uri associated with the applications.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the client applications corresponding to the specified post_logout_redirect_uri.
+ ///
+ public ValueTask> FindByPostLogoutRedirectUriAsync(
+ [NotNull] string address, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(address))
+ {
+ throw new ArgumentException("The address cannot be null or empty.", nameof(address));
+ }
+
+ var parameters = new
+ {
+ Method = nameof(FindByPostLogoutRedirectUriAsync),
+ Address = address
+ };
+
+ if (_cache.TryGetValue(parameters, out ImmutableArray applications))
+ {
+ return new ValueTask>(applications);
+ }
+
+ async Task> ExecuteAsync()
+ {
+ foreach (var application in (applications = await _store.FindByPostLogoutRedirectUriAsync(address, cancellationToken)))
+ {
+ await AddAsync(application, cancellationToken);
+ }
+
+ using (var entry = _cache.CreateEntry(parameters))
+ {
+ entry.SetValue(applications);
+ }
+
+ return applications;
+ }
+
+ return new ValueTask>(ExecuteAsync());
+ }
+
+ ///
+ /// Retrieves all the applications associated with the specified redirect_uri.
+ ///
+ /// The redirect_uri associated with the applications.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the client applications corresponding to the specified redirect_uri.
+ ///
+ public ValueTask> FindByRedirectUriAsync(
+ [NotNull] string address, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(address))
+ {
+ throw new ArgumentException("The address cannot be null or empty.", nameof(address));
+ }
+
+ var parameters = new
+ {
+ Method = nameof(FindByRedirectUriAsync),
+ Address = address
+ };
+
+ if (_cache.TryGetValue(parameters, out ImmutableArray applications))
+ {
+ return new ValueTask>(applications);
+ }
+
+ async Task> ExecuteAsync()
+ {
+ foreach (var application in (applications = await _store.FindByRedirectUriAsync(address, cancellationToken)))
+ {
+ await AddAsync(application, cancellationToken);
+ }
+
+ using (var entry = _cache.CreateEntry(parameters))
+ {
+ entry.SetValue(applications);
+ }
+
+ return applications;
+ }
+
+ return new ValueTask>(ExecuteAsync());
+ }
+
+ ///
+ /// Removes the specified application from the cache.
+ ///
+ /// The application to remove from the cache.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ public async Task RemoveAsync([NotNull] TApplication application, CancellationToken cancellationToken)
+ {
+ if (application == null)
+ {
+ 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
+ });
+ }
+ }
+ }
+}
diff --git a/src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs b/src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs
new file mode 100644
index 00000000..5c4e31fc
--- /dev/null
+++ b/src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs
@@ -0,0 +1,505 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/openiddict/openiddict-core for more information concerning
+ * the license and the contributors participating to this project.
+ */
+
+using System;
+using System.Collections.Immutable;
+using System.Threading;
+using System.Threading.Tasks;
+using JetBrains.Annotations;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Options;
+using OpenIddict.Abstractions;
+
+namespace OpenIddict.Core
+{
+ ///
+ /// Provides methods allowing to cache authorizations after retrieving them from the store.
+ ///
+ /// The type of the Authorization entity.
+ public class OpenIddictAuthorizationCache : IOpenIddictAuthorizationCache, IDisposable where TAuthorization : class
+ {
+ private readonly MemoryCache _cache;
+ private readonly IOpenIddictAuthorizationStore _store;
+ private readonly IOptions _options;
+
+ public OpenIddictAuthorizationCache(
+ [NotNull] IOptions options,
+ [NotNull] IOpenIddictAuthorizationStoreResolver resolver)
+ {
+ _cache = new MemoryCache(new MemoryCacheOptions());
+ _options = options;
+ _store = resolver.Get();
+ }
+
+ ///
+ /// Add the specified authorization to the cache.
+ ///
+ /// The authorization to add to the cache.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ public async Task AddAsync(TAuthorization authorization, CancellationToken cancellationToken)
+ {
+ if (authorization == null)
+ {
+ throw new ArgumentNullException(nameof(authorization));
+ }
+
+ if (_cache.Count >= _options.Value.EntityCacheLimit)
+ {
+ _cache.Compact(0.25);
+ }
+
+ using (var entry = _cache.CreateEntry(new
+ {
+ Method = nameof(FindByIdAsync),
+ Identifier = await _store.GetIdAsync(authorization, cancellationToken)
+ }))
+ {
+ entry.SetValue(authorization);
+ }
+ }
+
+ ///
+ /// Disposes the cache held by this instance.
+ ///
+ public void Dispose() => _cache.Dispose();
+
+ ///
+ /// Retrieves the authorizations corresponding to the specified
+ /// subject and associated with the application identifier.
+ ///
+ /// The subject associated with the authorization.
+ /// The client associated with the authorization.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the authorizations corresponding to the subject/client.
+ ///
+ public ValueTask> FindAsync(
+ [NotNull] string subject, [NotNull] string client, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(subject))
+ {
+ throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
+ }
+
+ if (string.IsNullOrEmpty(client))
+ {
+ throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client));
+ }
+
+ var parameters = new
+ {
+ Method = nameof(FindAsync),
+ Subject = subject,
+ Client = client
+ };
+
+ if (_cache.TryGetValue(parameters, out ImmutableArray authorizations))
+ {
+ return new ValueTask>(authorizations);
+ }
+
+ async Task> ExecuteAsync()
+ {
+ foreach (var authorization in (authorizations = await _store.FindAsync(subject, client, cancellationToken)))
+ {
+ await AddAsync(authorization, cancellationToken);
+ }
+
+ using (var entry = _cache.CreateEntry(parameters))
+ {
+ entry.SetValue(authorizations);
+ }
+
+ return authorizations;
+ }
+
+ return new ValueTask>(ExecuteAsync());
+ }
+
+ ///
+ /// Retrieves the authorizations matching the specified parameters.
+ ///
+ /// The subject associated with the authorization.
+ /// The client associated with the authorization.
+ /// The authorization status.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the authorizations corresponding to the criteria.
+ ///
+ public ValueTask> FindAsync(
+ [NotNull] string subject, [NotNull] string client,
+ [NotNull] string status, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(subject))
+ {
+ throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
+ }
+
+ if (string.IsNullOrEmpty(client))
+ {
+ throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client));
+ }
+
+ if (string.IsNullOrEmpty(status))
+ {
+ throw new ArgumentException("The status cannot be null or empty.", nameof(status));
+ }
+
+ var parameters = new
+ {
+ Method = nameof(FindAsync),
+ Subject = subject,
+ Client = client,
+ Status = status
+ };
+
+ if (_cache.TryGetValue(parameters, out ImmutableArray authorizations))
+ {
+ return new ValueTask>(authorizations);
+ }
+
+ async Task> ExecuteAsync()
+ {
+ foreach (var authorization in (authorizations = await _store.FindAsync(subject, client, status, cancellationToken)))
+ {
+ await AddAsync(authorization, cancellationToken);
+ }
+
+ using (var entry = _cache.CreateEntry(parameters))
+ {
+ entry.SetValue(authorizations);
+ }
+
+ return authorizations;
+ }
+
+ return new ValueTask>(ExecuteAsync());
+ }
+
+ ///
+ /// Retrieves the authorizations matching the specified parameters.
+ ///
+ /// The subject associated with the authorization.
+ /// The client associated with the authorization.
+ /// The authorization status.
+ /// The authorization type.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the authorizations corresponding to the criteria.
+ ///
+ public ValueTask> FindAsync(
+ [NotNull] string subject, [NotNull] string client,
+ [NotNull] string status, [NotNull] string type, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(subject))
+ {
+ throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
+ }
+
+ if (string.IsNullOrEmpty(client))
+ {
+ throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client));
+ }
+
+ if (string.IsNullOrEmpty(status))
+ {
+ throw new ArgumentException("The status cannot be null or empty.", nameof(status));
+ }
+
+ if (string.IsNullOrEmpty(type))
+ {
+ throw new ArgumentException("The type cannot be null or empty.", nameof(type));
+ }
+
+ var parameters = new
+ {
+ Method = nameof(FindAsync),
+ Subject = subject,
+ Client = client,
+ Status = status,
+ Type = type
+ };
+
+ if (_cache.TryGetValue(parameters, out ImmutableArray authorizations))
+ {
+ return new ValueTask>(authorizations);
+ }
+
+ async Task> ExecuteAsync()
+ {
+ foreach (var authorization in (authorizations = await _store.FindAsync(subject, client, status, type, cancellationToken)))
+ {
+ await AddAsync(authorization, cancellationToken);
+ }
+
+ using (var entry = _cache.CreateEntry(parameters))
+ {
+ entry.SetValue(authorizations);
+ }
+
+ return authorizations;
+ }
+
+ return new ValueTask>(ExecuteAsync());
+ }
+
+ ///
+ /// Retrieves the authorizations matching the specified parameters.
+ ///
+ /// The subject associated with the authorization.
+ /// The client associated with the authorization.
+ /// The authorization status.
+ /// The authorization type.
+ /// The minimal scopes associated with the authorization.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the authorizations corresponding to the criteria.
+ ///
+ public ValueTask> FindAsync(
+ [NotNull] string subject, [NotNull] string client,
+ [NotNull] string status, [NotNull] string type,
+ ImmutableArray scopes, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(subject))
+ {
+ throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
+ }
+
+ if (string.IsNullOrEmpty(client))
+ {
+ throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client));
+ }
+
+ if (string.IsNullOrEmpty(status))
+ {
+ throw new ArgumentException("The status cannot be null or empty.", nameof(status));
+ }
+
+ if (string.IsNullOrEmpty(type))
+ {
+ throw new ArgumentException("The type cannot be null or empty.", nameof(type));
+ }
+
+ // Note: this method is only partially cached.
+
+ async Task> ExecuteAsync()
+ {
+ var authorizations = await _store.FindAsync(subject, client, status, type, scopes, cancellationToken);
+
+ foreach (var authorization in authorizations)
+ {
+ await AddAsync(authorization, cancellationToken);
+ }
+
+ return authorizations;
+ }
+
+ return new ValueTask>(ExecuteAsync());
+ }
+
+ ///
+ /// Retrieves the list of authorizations corresponding to the specified application identifier.
+ ///
+ /// The application identifier associated with the authorizations.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the authorizations corresponding to the specified application.
+ ///
+ public ValueTask> FindByApplicationIdAsync(
+ [NotNull] string identifier, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(identifier))
+ {
+ throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
+ }
+
+ var parameters = new
+ {
+ Method = nameof(FindByApplicationIdAsync),
+ Identifier = identifier
+ };
+
+ if (_cache.TryGetValue(parameters, out ImmutableArray authorizations))
+ {
+ return new ValueTask>(authorizations);
+ }
+
+ async Task> ExecuteAsync()
+ {
+ foreach (var authorization in (authorizations = await _store.FindByApplicationIdAsync(identifier, cancellationToken)))
+ {
+ await AddAsync(authorization, cancellationToken);
+ }
+
+ using (var entry = _cache.CreateEntry(parameters))
+ {
+ entry.SetValue(authorizations);
+ }
+
+ return authorizations;
+ }
+
+ return new ValueTask>(ExecuteAsync());
+ }
+
+ ///
+ /// Retrieves an authorization using its unique identifier.
+ ///
+ /// The unique identifier associated with the authorization.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the authorization corresponding to the identifier.
+ ///
+ public ValueTask FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(identifier))
+ {
+ throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
+ }
+
+ var parameters = new
+ {
+ Method = nameof(FindByIdAsync),
+ Identifier = identifier
+ };
+
+ if (_cache.TryGetValue(parameters, out TAuthorization authorization))
+ {
+ return new ValueTask(authorization);
+ }
+
+ async Task ExecuteAsync()
+ {
+ if ((authorization = await _store.FindByIdAsync(identifier, cancellationToken)) != null)
+ {
+ await AddAsync(authorization, cancellationToken);
+ }
+
+ using (var entry = _cache.CreateEntry(parameters))
+ {
+ entry.SetValue(authorization);
+ }
+
+ return authorization;
+ }
+
+ return new ValueTask(ExecuteAsync());
+ }
+
+ ///
+ /// Retrieves all the authorizations corresponding to the specified subject.
+ ///
+ /// The subject associated with the authorization.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the authorizations corresponding to the specified subject.
+ ///
+ public ValueTask> FindBySubjectAsync(
+ [NotNull] string subject, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(subject))
+ {
+ throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
+ }
+
+ var parameters = new
+ {
+ Method = nameof(FindBySubjectAsync),
+ Subject = subject
+ };
+
+ if (_cache.TryGetValue(parameters, out ImmutableArray authorizations))
+ {
+ return new ValueTask>(authorizations);
+ }
+
+ async Task> ExecuteAsync()
+ {
+ foreach (var authorization in (authorizations = await _store.FindBySubjectAsync(subject, cancellationToken)))
+ {
+ await AddAsync(authorization, cancellationToken);
+ }
+
+ using (var entry = _cache.CreateEntry(parameters))
+ {
+ entry.SetValue(authorizations);
+ }
+
+ return authorizations;
+ }
+
+ return new ValueTask>(ExecuteAsync());
+ }
+
+ ///
+ /// Removes the specified authorization from the cache.
+ ///
+ /// The authorization to remove from the cache.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ public async Task RemoveAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
+ {
+ if (authorization == null)
+ {
+ 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)
+ });
+ }
+ }
+}
diff --git a/src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs b/src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs
new file mode 100644
index 00000000..435b18ea
--- /dev/null
+++ b/src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs
@@ -0,0 +1,292 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/openiddict/openiddict-core for more information concerning
+ * the license and the contributors participating to this project.
+ */
+
+using System;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using JetBrains.Annotations;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Options;
+using OpenIddict.Abstractions;
+
+namespace OpenIddict.Core
+{
+ ///
+ /// Provides methods allowing to cache scopes after retrieving them from the store.
+ ///
+ /// The type of the Scope entity.
+ public class OpenIddictScopeCache : IOpenIddictScopeCache, IDisposable where TScope : class
+ {
+ private readonly MemoryCache _cache;
+ private readonly IOpenIddictScopeStore _store;
+ private readonly IOptions _options;
+
+ public OpenIddictScopeCache(
+ [NotNull] IOptions options,
+ [NotNull] IOpenIddictScopeStoreResolver resolver)
+ {
+ _cache = new MemoryCache(new MemoryCacheOptions());
+ _options = options;
+ _store = resolver.Get();
+ }
+
+ ///
+ /// Add the specified scope to the cache.
+ ///
+ /// The scope to add to the cache.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ public async Task AddAsync([NotNull] TScope scope, CancellationToken cancellationToken)
+ {
+ if (scope == null)
+ {
+ throw new ArgumentNullException(nameof(scope));
+ }
+
+ if (_cache.Count >= _options.Value.EntityCacheLimit)
+ {
+ _cache.Compact(0.25);
+ }
+
+ using (var entry = _cache.CreateEntry(new
+ {
+ Method = nameof(FindByIdAsync),
+ Identifier = await _store.GetIdAsync(scope, cancellationToken)
+ }))
+ {
+ entry.SetValue(scope);
+ }
+
+ using (var entry = _cache.CreateEntry(new
+ {
+ Method = nameof(FindByNameAsync),
+ Name = await _store.GetNameAsync(scope, cancellationToken)
+ }))
+ {
+ entry.SetValue(scope);
+ }
+ }
+
+ ///
+ /// Disposes the cache held by this instance.
+ ///
+ public void Dispose() => _cache.Dispose();
+
+ ///
+ /// Retrieves a scope using its unique identifier.
+ ///
+ /// The unique identifier associated with the scope.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the scope corresponding to the identifier.
+ ///
+ public ValueTask FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(identifier))
+ {
+ throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
+ }
+
+ var parameters = new
+ {
+ Method = nameof(FindByIdAsync),
+ Identifier = identifier
+ };
+
+ if (_cache.TryGetValue(parameters, out TScope scope))
+ {
+ return new ValueTask(scope);
+ }
+
+ async Task ExecuteAsync()
+ {
+ if ((scope = await _store.FindByIdAsync(identifier, cancellationToken)) != null)
+ {
+ await AddAsync(scope, cancellationToken);
+ }
+
+ using (var entry = _cache.CreateEntry(parameters))
+ {
+ entry.SetValue(scope);
+ }
+
+ return scope;
+ }
+
+ return new ValueTask(ExecuteAsync());
+ }
+
+ ///
+ /// Retrieves a scope using its name.
+ ///
+ /// The name associated with the scope.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the scope corresponding to the specified name.
+ ///
+ public ValueTask FindByNameAsync([NotNull] string name, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException("The scope name cannot be null or empty.", nameof(name));
+ }
+
+ var parameters = new
+ {
+ Method = nameof(FindByNameAsync),
+ Name = name
+ };
+
+ if (_cache.TryGetValue(parameters, out TScope scope))
+ {
+ return new ValueTask(scope);
+ }
+
+ async Task ExecuteAsync()
+ {
+ if ((scope = await _store.FindByNameAsync(name, cancellationToken)) != null)
+ {
+ await AddAsync(scope, cancellationToken);
+ }
+
+ using (var entry = _cache.CreateEntry(parameters))
+ {
+ entry.SetValue(scope);
+ }
+
+ return scope;
+ }
+
+ return new ValueTask(ExecuteAsync());
+ }
+
+ ///
+ /// Retrieves a list of scopes using their name.
+ ///
+ /// The names associated with the scopes.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the scopes corresponding to the specified names.
+ ///
+ public ValueTask> FindByNamesAsync(ImmutableArray names, CancellationToken cancellationToken)
+ {
+ if (names.IsDefaultOrEmpty)
+ {
+ return new ValueTask>(ImmutableArray.Create());
+ }
+
+ if (names.Any(name => string.IsNullOrEmpty(name)))
+ {
+ throw new ArgumentException("Scope names cannot be null or empty.", nameof(names));
+ }
+
+ // Note: this method is only partially cached.
+
+ async Task> ExecuteAsync()
+ {
+ var scopes = await _store.FindByNamesAsync(names, cancellationToken);
+
+ foreach (var scope in scopes)
+ {
+ await AddAsync(scope, cancellationToken);
+ }
+
+ return scopes;
+ }
+
+ return new ValueTask>(ExecuteAsync());
+ }
+
+ ///
+ /// Retrieves all the scopes that contain the specified resource.
+ ///
+ /// The resource associated with the scopes.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the scopes associated with the specified resource.
+ ///
+ public ValueTask> FindByResourceAsync([NotNull] string resource, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(resource))
+ {
+ throw new ArgumentException("The resource cannot be null or empty.", nameof(resource));
+ }
+
+ var parameters = new
+ {
+ Method = nameof(FindByResourceAsync),
+ Resource = resource
+ };
+
+ if (_cache.TryGetValue(parameters, out ImmutableArray scopes))
+ {
+ return new ValueTask>(scopes);
+ }
+
+ async Task> ExecuteAsync()
+ {
+ foreach (var scope in (scopes = await _store.FindByResourceAsync(resource, cancellationToken)))
+ {
+ await AddAsync(scope, cancellationToken);
+ }
+
+ using (var entry = _cache.CreateEntry(parameters))
+ {
+ entry.SetValue(scopes);
+ }
+
+ return scopes;
+ }
+
+ return new ValueTask>(ExecuteAsync());
+ }
+
+ ///
+ /// Removes the specified scope from the cache.
+ ///
+ /// The scope to remove from the cache.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ public async Task RemoveAsync([NotNull] TScope scope, CancellationToken cancellationToken)
+ {
+ if (scope == null)
+ {
+ 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
+ });
+ }
+ }
+ }
+}
diff --git a/src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs b/src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs
new file mode 100644
index 00000000..9f9e4c2e
--- /dev/null
+++ b/src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs
@@ -0,0 +1,562 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/openiddict/openiddict-core for more information concerning
+ * the license and the contributors participating to this project.
+ */
+
+using System;
+using System.Collections.Immutable;
+using System.Threading;
+using System.Threading.Tasks;
+using JetBrains.Annotations;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Options;
+using OpenIddict.Abstractions;
+
+namespace OpenIddict.Core
+{
+ ///
+ /// Provides methods allowing to cache tokens after retrieving them from the store.
+ ///
+ /// The type of the Token entity.
+ public class OpenIddictTokenCache : IOpenIddictTokenCache, IDisposable where TToken : class
+ {
+ private readonly MemoryCache _cache;
+ private readonly IOpenIddictTokenStore _store;
+ private readonly IOptions _options;
+
+ public OpenIddictTokenCache(
+ [NotNull] IOptions options,
+ [NotNull] IOpenIddictTokenStoreResolver resolver)
+ {
+ _cache = new MemoryCache(new MemoryCacheOptions());
+ _options = options;
+ _store = resolver.Get();
+ }
+
+ ///
+ /// Add the specified token to the cache.
+ ///
+ /// The token to add to the cache.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ public async Task AddAsync([NotNull] TToken token, CancellationToken cancellationToken)
+ {
+ if (token == null)
+ {
+ throw new ArgumentNullException(nameof(token));
+ }
+
+ if (_cache.Count >= _options.Value.EntityCacheLimit)
+ {
+ _cache.Compact(0.25);
+ }
+
+ using (var entry = _cache.CreateEntry(new
+ {
+ Method = nameof(FindByIdAsync),
+ Identifier = await _store.GetIdAsync(token, cancellationToken)
+ }))
+ {
+ entry.SetValue(token);
+ }
+
+ using (var entry = _cache.CreateEntry(new
+ {
+ Method = nameof(FindByReferenceIdAsync),
+ Identifier = await _store.GetReferenceIdAsync(token, cancellationToken)
+ }))
+ {
+ entry.SetValue(token);
+ }
+ }
+
+ ///
+ /// Disposes the cache held by this instance.
+ ///
+ public void Dispose() => _cache.Dispose();
+
+ ///
+ /// Retrieves the tokens corresponding to the specified
+ /// subject and associated with the application identifier.
+ ///
+ /// The subject associated with the token.
+ /// The client associated with the token.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the subject/client.
+ ///
+ public ValueTask> FindAsync([NotNull] string subject,
+ [NotNull] string client, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(subject))
+ {
+ throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
+ }
+
+ if (string.IsNullOrEmpty(client))
+ {
+ throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client));
+ }
+
+ var parameters = new
+ {
+ Method = nameof(FindAsync),
+ Subject = subject,
+ Client = client
+ };
+
+ if (_cache.TryGetValue(parameters, out ImmutableArray tokens))
+ {
+ return new ValueTask>(tokens);
+ }
+
+ async Task> ExecuteAsync()
+ {
+ foreach (var token in (tokens = await _store.FindAsync(subject, client, cancellationToken)))
+ {
+ await AddAsync(token, cancellationToken);
+ }
+
+ using (var entry = _cache.CreateEntry(parameters))
+ {
+ entry.SetValue(tokens);
+ }
+
+ return tokens;
+ }
+
+ return new ValueTask>(ExecuteAsync());
+ }
+
+ ///
+ /// Retrieves the tokens matching the specified parameters.
+ ///
+ /// The subject associated with the token.
+ /// The client associated with the token.
+ /// The token status.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the criteria.
+ ///
+ public ValueTask> FindAsync(
+ [NotNull] string subject, [NotNull] string client,
+ [NotNull] string status, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(subject))
+ {
+ throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
+ }
+
+ if (string.IsNullOrEmpty(client))
+ {
+ throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client));
+ }
+
+ if (string.IsNullOrEmpty(status))
+ {
+ throw new ArgumentException("The status cannot be null or empty.", nameof(status));
+ }
+
+ var parameters = new
+ {
+ Method = nameof(FindAsync),
+ Subject = subject,
+ Client = client,
+ Status = status
+ };
+
+ if (_cache.TryGetValue(parameters, out ImmutableArray tokens))
+ {
+ return new ValueTask>(tokens);
+ }
+
+ async Task> ExecuteAsync()
+ {
+ foreach (var token in (tokens = await _store.FindAsync(subject, client, status, cancellationToken)))
+ {
+ await AddAsync(token, cancellationToken);
+ }
+
+ using (var entry = _cache.CreateEntry(parameters))
+ {
+ entry.SetValue(tokens);
+ }
+
+ return tokens;
+ }
+
+ return new ValueTask>(ExecuteAsync());
+ }
+
+ ///
+ /// Retrieves the tokens matching the specified parameters.
+ ///
+ /// The subject associated with the token.
+ /// The client associated with the token.
+ /// The token status.
+ /// The token type.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the criteria.
+ ///
+ public ValueTask> FindAsync(
+ [NotNull] string subject, [NotNull] string client,
+ [NotNull] string status, [NotNull] string type, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(subject))
+ {
+ throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
+ }
+
+ if (string.IsNullOrEmpty(client))
+ {
+ throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client));
+ }
+
+ if (string.IsNullOrEmpty(status))
+ {
+ throw new ArgumentException("The status cannot be null or empty.", nameof(status));
+ }
+
+ if (string.IsNullOrEmpty(type))
+ {
+ throw new ArgumentException("The type cannot be null or empty.", nameof(type));
+ }
+
+ var parameters = new
+ {
+ Method = nameof(FindAsync),
+ Subject = subject,
+ Client = client,
+ Status = status,
+ Type = type
+ };
+
+ if (_cache.TryGetValue(parameters, out ImmutableArray tokens))
+ {
+ return new ValueTask>(tokens);
+ }
+
+ async Task> ExecuteAsync()
+ {
+ foreach (var token in (tokens = await _store.FindAsync(subject, client, status, type, cancellationToken)))
+ {
+ await AddAsync(token, cancellationToken);
+ }
+
+ using (var entry = _cache.CreateEntry(parameters))
+ {
+ entry.SetValue(tokens);
+ }
+
+ return tokens;
+ }
+
+ return new ValueTask>(ExecuteAsync());
+ }
+
+ ///
+ /// Retrieves the list of tokens corresponding to the specified application identifier.
+ ///
+ /// The application identifier associated with the tokens.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the specified application.
+ ///
+ public ValueTask> FindByApplicationIdAsync(
+ [NotNull] string identifier, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(identifier))
+ {
+ throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
+ }
+
+ var parameters = new
+ {
+ Method = nameof(FindByApplicationIdAsync),
+ Identifier = identifier
+ };
+
+ if (_cache.TryGetValue(parameters, out ImmutableArray tokens))
+ {
+ return new ValueTask>(tokens);
+ }
+
+ async Task> ExecuteAsync()
+ {
+ foreach (var token in (tokens = await _store.FindByApplicationIdAsync(identifier, cancellationToken)))
+ {
+ await AddAsync(token, cancellationToken);
+ }
+
+ using (var entry = _cache.CreateEntry(parameters))
+ {
+ entry.SetValue(tokens);
+ }
+
+ return tokens;
+ }
+
+ return new ValueTask>(ExecuteAsync());
+ }
+
+ ///
+ /// Retrieves the list of tokens corresponding to the specified authorization identifier.
+ ///
+ /// The authorization identifier associated with the tokens.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the specified authorization.
+ ///
+ public ValueTask> FindByAuthorizationIdAsync(
+ [NotNull] string identifier, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(identifier))
+ {
+ throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
+ }
+
+ var parameters = new
+ {
+ Method = nameof(FindByAuthorizationIdAsync),
+ Identifier = identifier
+ };
+
+ if (_cache.TryGetValue(parameters, out ImmutableArray tokens))
+ {
+ return new ValueTask>(tokens);
+ }
+
+ async Task> ExecuteAsync()
+ {
+ foreach (var token in (tokens = await _store.FindByAuthorizationIdAsync(identifier, cancellationToken)))
+ {
+ await AddAsync(token, cancellationToken);
+ }
+
+ using (var entry = _cache.CreateEntry(parameters))
+ {
+ entry.SetValue(tokens);
+ }
+
+ return tokens;
+ }
+
+ return new ValueTask>(ExecuteAsync());
+ }
+
+ ///
+ /// Retrieves a token using its unique identifier.
+ ///
+ /// The unique identifier associated with the token.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the token corresponding to the unique identifier.
+ ///
+ public ValueTask FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(identifier))
+ {
+ throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
+ }
+
+ var parameters = new
+ {
+ Method = nameof(FindByIdAsync),
+ Identifier = identifier
+ };
+
+ if (_cache.TryGetValue(parameters, out TToken token))
+ {
+ return new ValueTask(token);
+ }
+
+ async Task ExecuteAsync()
+ {
+ if ((token = await _store.FindByIdAsync(identifier, cancellationToken)) != null)
+ {
+ await AddAsync(token, cancellationToken);
+ }
+
+ using (var entry = _cache.CreateEntry(parameters))
+ {
+ entry.SetValue(token);
+ }
+
+ return token;
+ }
+
+ return new ValueTask(ExecuteAsync());
+ }
+
+ ///
+ /// Retrieves the list of tokens corresponding to the specified reference identifier.
+ /// Note: the reference identifier may be hashed or encrypted for security reasons.
+ ///
+ /// The reference identifier associated with the tokens.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the specified reference identifier.
+ ///
+ public ValueTask FindByReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(identifier))
+ {
+ throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
+ }
+
+ var parameters = new
+ {
+ Method = nameof(FindByReferenceIdAsync),
+ Identifier = identifier
+ };
+
+ if (_cache.TryGetValue(parameters, out TToken token))
+ {
+ return new ValueTask(token);
+ }
+
+ async Task ExecuteAsync()
+ {
+ if ((token = await _store.FindByReferenceIdAsync(identifier, cancellationToken)) != null)
+ {
+ await AddAsync(token, cancellationToken);
+ }
+
+ using (var entry = _cache.CreateEntry(parameters))
+ {
+ entry.SetValue(token);
+ }
+
+ return token;
+ }
+
+ return new ValueTask(ExecuteAsync());
+ }
+
+ ///
+ /// Retrieves the list of tokens corresponding to the specified subject.
+ ///
+ /// The subject associated with the tokens.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the specified subject.
+ ///
+ public ValueTask> FindBySubjectAsync([NotNull] string subject, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(subject))
+ {
+ throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
+ }
+
+ var parameters = new
+ {
+ Method = nameof(FindBySubjectAsync),
+ Identifier = subject
+ };
+
+ if (_cache.TryGetValue(parameters, out ImmutableArray tokens))
+ {
+ return new ValueTask>(tokens);
+ }
+
+ async Task> ExecuteAsync()
+ {
+ foreach (var token in (tokens = await _store.FindBySubjectAsync(subject, cancellationToken)))
+ {
+ await AddAsync(token, cancellationToken);
+ }
+
+ using (var entry = _cache.CreateEntry(parameters))
+ {
+ entry.SetValue(tokens);
+ }
+
+ return tokens;
+ }
+
+ return new ValueTask>(ExecuteAsync());
+ }
+
+ ///
+ /// Removes the specified token from the cache.
+ ///
+ /// The token to remove from the cache.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ public async Task RemoveAsync([NotNull] TToken token, CancellationToken cancellationToken)
+ {
+ if (token == null)
+ {
+ 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)
+ });
+ }
+ }
+}
diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
index d6dda21a..d14203c5 100644
--- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
+++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
@@ -26,15 +26,22 @@ namespace OpenIddict.Core
public class OpenIddictApplicationManager : IOpenIddictApplicationManager where TApplication : class
{
public OpenIddictApplicationManager(
+ [NotNull] IOpenIddictApplicationCache cache,
[NotNull] IOpenIddictApplicationStoreResolver resolver,
[NotNull] ILogger> logger,
[NotNull] IOptions options)
{
+ Cache = cache;
Store = resolver.Get();
Logger = logger;
Options = options;
}
+ ///
+ /// Gets the cache associated with the current manager.
+ ///
+ protected IOpenIddictApplicationCache Cache { get; }
+
///
/// Gets the logger associated with the current manager.
///
@@ -115,7 +122,7 @@ namespace OpenIddict.Core
if (!string.IsNullOrEmpty(await Store.GetClientSecretAsync(application, cancellationToken)))
{
- throw new ArgumentException("The client secret hash cannot be directly set on the application entity.");
+ throw new ArgumentException("The client secret hash cannot be set on the application entity.", nameof(application));
}
// If no client type was specified, assume it's a public application if no secret was provided.
@@ -175,7 +182,7 @@ namespace OpenIddict.Core
var application = await Store.InstantiateAsync(cancellationToken);
if (application == null)
{
- throw new InvalidOperationException("An error occurred while trying to create a new application");
+ throw new InvalidOperationException("An error occurred while trying to create a new application.");
}
await PopulateAsync(application, descriptor, cancellationToken);
@@ -202,58 +209,89 @@ namespace OpenIddict.Core
///
/// A that can be used to monitor the asynchronous operation.
///
- public virtual Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken = default)
+ public virtual async Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken = default)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
- return Store.DeleteAsync(application, cancellationToken);
+ if (!Options.Value.DisableEntityCaching)
+ {
+ await Cache.RemoveAsync(application, cancellationToken);
+ }
+
+ await Store.DeleteAsync(application, cancellationToken);
}
///
- /// Retrieves an application using its unique identifier.
+ /// Retrieves an application using its client identifier.
///
- /// The unique identifier associated with the application.
+ /// The client identifier associated with the application.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
///
- public virtual Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default)
+ public virtual async Task FindByClientIdAsync(
+ [NotNull] string identifier, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
- return Store.FindByIdAsync(identifier, cancellationToken);
+ var application = Options.Value.DisableEntityCaching ?
+ await Store.FindByClientIdAsync(identifier, cancellationToken) :
+ await Cache.FindByClientIdAsync(identifier, cancellationToken);
+
+ if (application == null)
+ {
+ return null;
+ }
+
+ // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
+ // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
+ // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
+ if (!Options.Value.DisableAdditionalFiltering &&
+ !string.Equals(await Store.GetClientIdAsync(application, cancellationToken), identifier, StringComparison.Ordinal))
+ {
+ return null;
+ }
+
+ return application;
}
///
- /// Retrieves an application using its client identifier.
+ /// Retrieves an application using its unique identifier.
///
- /// The client identifier associated with the application.
+ /// The unique identifier associated with the application.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
///
- public virtual async Task FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default)
+ public virtual async Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
+ var application = Options.Value.DisableEntityCaching ?
+ await Store.FindByIdAsync(identifier, cancellationToken) :
+ await Cache.FindByIdAsync(identifier, cancellationToken);
+
+ if (application == null)
+ {
+ return null;
+ }
+
// SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
-
- var application = await Store.FindByClientIdAsync(identifier, cancellationToken);
- if (application == null ||
- !string.Equals(await Store.GetClientIdAsync(application, cancellationToken), identifier, StringComparison.Ordinal))
+ if (!Options.Value.DisableAdditionalFiltering &&
+ !string.Equals(await Store.GetIdAsync(application, cancellationToken), identifier, StringComparison.Ordinal))
{
return null;
}
@@ -278,16 +316,24 @@ namespace OpenIddict.Core
throw new ArgumentException("The address cannot be null or empty.", nameof(address));
}
- // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
- // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
- // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
+ var applications = Options.Value.DisableEntityCaching ?
+ await Store.FindByPostLogoutRedirectUriAsync(address, cancellationToken) :
+ await Cache.FindByPostLogoutRedirectUriAsync(address, cancellationToken);
- var applications = await Store.FindByPostLogoutRedirectUriAsync(address, cancellationToken);
if (applications.IsEmpty)
{
return ImmutableArray.Create();
}
+ if (Options.Value.DisableAdditionalFiltering)
+ {
+ return applications;
+ }
+
+ // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
+ // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
+ // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
+
var builder = ImmutableArray.CreateBuilder(applications.Length);
foreach (var application in applications)
@@ -324,16 +370,24 @@ namespace OpenIddict.Core
throw new ArgumentException("The address cannot be null or empty.", nameof(address));
}
- // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
- // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
- // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
+ var applications = Options.Value.DisableEntityCaching ?
+ await Store.FindByRedirectUriAsync(address, cancellationToken) :
+ await Cache.FindByRedirectUriAsync(address, cancellationToken);
- var applications = await Store.FindByRedirectUriAsync(address, cancellationToken);
if (applications.IsEmpty)
{
return ImmutableArray.Create();
}
+ if (Options.Value.DisableAdditionalFiltering)
+ {
+ return applications;
+ }
+
+ // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
+ // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
+ // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
+
var builder = ImmutableArray.CreateBuilder(applications.Length);
foreach (var application in applications)
@@ -366,6 +420,11 @@ namespace OpenIddict.Core
public virtual Task GetAsync(
[NotNull] Func, IQueryable> query, CancellationToken cancellationToken = default)
{
+ if (query == null)
+ {
+ throw new ArgumentNullException(nameof(query));
+ }
+
return GetAsync((applications, state) => state(applications), query, cancellationToken);
}
@@ -402,7 +461,8 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the client identifier associated with the application.
///
- public virtual ValueTask GetClientIdAsync([NotNull] TApplication application, CancellationToken cancellationToken = default)
+ public virtual ValueTask GetClientIdAsync(
+ [NotNull] TApplication application, CancellationToken cancellationToken = default)
{
if (application == null)
{
@@ -441,7 +501,8 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the consent type of the application (by default, "explicit").
///
- public virtual ValueTask GetConsentTypeAsync([NotNull] TApplication application, CancellationToken cancellationToken = default)
+ public virtual ValueTask GetConsentTypeAsync(
+ [NotNull] TApplication application, CancellationToken cancellationToken = default)
{
if (application == null)
{
@@ -471,7 +532,8 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the display name associated with the application.
///
- public virtual ValueTask GetDisplayNameAsync([NotNull] TApplication application, CancellationToken cancellationToken = default)
+ public virtual ValueTask GetDisplayNameAsync(
+ [NotNull] TApplication application, CancellationToken cancellationToken = default)
{
if (application == null)
{
@@ -679,6 +741,11 @@ namespace OpenIddict.Core
public virtual Task> ListAsync(
[NotNull] Func, IQueryable> query, CancellationToken cancellationToken = default)
{
+ if (query == null)
+ {
+ throw new ArgumentNullException(nameof(query));
+ }
+
return ListAsync((applications, state) => state(applications), query, cancellationToken);
}
@@ -838,6 +905,11 @@ namespace OpenIddict.Core
throw new OpenIddictExceptions.ValidationException(builder.ToString(), results);
}
+ if (!Options.Value.DisableEntityCaching)
+ {
+ await Cache.RemoveAsync(application, cancellationToken);
+ }
+
await Store.UpdateAsync(application, cancellationToken);
}
@@ -900,7 +972,7 @@ namespace OpenIddict.Core
var comparand = await Store.GetClientSecretAsync(application, cancellationToken);
await PopulateAsync(application, descriptor, cancellationToken);
- // If the client secret was updated, re-obfuscate it before persisting the changes.
+ // If the client secret was updated, use the overload accepting a secret parameter.
var secret = await Store.GetClientSecretAsync(application, cancellationToken);
if (!string.Equals(secret, comparand, StringComparison.Ordinal))
{
diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
index 79b78ec0..35bd1583 100644
--- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
+++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
@@ -26,15 +26,22 @@ namespace OpenIddict.Core
public class OpenIddictAuthorizationManager : IOpenIddictAuthorizationManager where TAuthorization : class
{
public OpenIddictAuthorizationManager(
+ [NotNull] IOpenIddictAuthorizationCache cache,
[NotNull] IOpenIddictAuthorizationStoreResolver resolver,
[NotNull] ILogger> logger,
[NotNull] IOptions options)
{
+ Cache = cache;
Store = resolver.Get();
Logger = logger;
Options = options;
}
+ ///
+ /// Gets the cache associated with the current manager.
+ ///
+ protected IOpenIddictAuthorizationCache Cache { get; }
+
///
/// Gets the logger associated with the current manager.
///
@@ -217,14 +224,19 @@ namespace OpenIddict.Core
///
/// A that can be used to monitor the asynchronous operation.
///
- public virtual Task DeleteAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
+ public virtual async Task DeleteAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
- return Store.DeleteAsync(authorization, cancellationToken);
+ if (!Options.Value.DisableEntityCaching)
+ {
+ await Cache.RemoveAsync(authorization, cancellationToken);
+ }
+
+ await Store.DeleteAsync(authorization, cancellationToken);
}
///
@@ -251,14 +263,22 @@ namespace OpenIddict.Core
throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client));
}
+ var authorizations = Options.Value.DisableEntityCaching ?
+ await Store.FindAsync(subject, client, cancellationToken) :
+ await Cache.FindAsync(subject, client, cancellationToken);
+
+ if (authorizations.IsEmpty)
+ {
+ return ImmutableArray.Create();
+ }
+
// SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
- var authorizations = await Store.FindAsync(subject, client, cancellationToken);
- if (authorizations.IsEmpty)
+ if (Options.Value.DisableAdditionalFiltering)
{
- return ImmutableArray.Create();
+ return authorizations;
}
var builder = ImmutableArray.CreateBuilder(authorizations.Length);
@@ -306,16 +326,24 @@ namespace OpenIddict.Core
throw new ArgumentException("The status cannot be null or empty.", nameof(status));
}
- // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
- // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
- // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
+ var authorizations = Options.Value.DisableEntityCaching ?
+ await Store.FindAsync(subject, client, status, cancellationToken) :
+ await Cache.FindAsync(subject, client, status, cancellationToken);
- var authorizations = await Store.FindAsync(subject, client, status, cancellationToken);
if (authorizations.IsEmpty)
{
return ImmutableArray.Create();
}
+ if (Options.Value.DisableAdditionalFiltering)
+ {
+ return authorizations;
+ }
+
+ // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
+ // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
+ // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
+
var builder = ImmutableArray.CreateBuilder(authorizations.Length);
foreach (var authorization in authorizations)
@@ -367,16 +395,24 @@ namespace OpenIddict.Core
throw new ArgumentException("The type cannot be null or empty.", nameof(type));
}
- // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
- // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
- // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
+ var authorizations = Options.Value.DisableEntityCaching ?
+ await Store.FindAsync(subject, client, status, type, cancellationToken) :
+ await Cache.FindAsync(subject, client, status, type, cancellationToken);
- var authorizations = await Store.FindAsync(subject, client, status, type, cancellationToken);
if (authorizations.IsEmpty)
{
return ImmutableArray.Create();
}
+ if (Options.Value.DisableAdditionalFiltering)
+ {
+ return authorizations;
+ }
+
+ // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
+ // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
+ // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
+
var builder = ImmutableArray.CreateBuilder(authorizations.Length);
foreach (var authorization in authorizations)
@@ -430,16 +466,24 @@ namespace OpenIddict.Core
throw new ArgumentException("The type cannot be null or empty.", nameof(type));
}
- // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
- // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
- // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
+ var authorizations = Options.Value.DisableEntityCaching ?
+ await Store.FindAsync(subject, client, status, type, scopes, cancellationToken) :
+ await Cache.FindAsync(subject, client, status, type, scopes, cancellationToken);
- var authorizations = await Store.FindAsync(subject, client, status, type, scopes, cancellationToken);
if (authorizations.IsEmpty)
{
return ImmutableArray.Create();
}
+ if (Options.Value.DisableAdditionalFiltering)
+ {
+ return authorizations;
+ }
+
+ // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
+ // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
+ // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
+
var builder = ImmutableArray.CreateBuilder(authorizations.Length);
foreach (var authorization in authorizations)
@@ -465,7 +509,7 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the specified application.
///
- public virtual Task> FindByApplicationIdAsync(
+ public virtual async Task> FindByApplicationIdAsync(
[NotNull] string identifier, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(identifier))
@@ -473,7 +517,37 @@ namespace OpenIddict.Core
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
- return Store.FindByApplicationIdAsync(identifier, cancellationToken);
+ var authorizations = Options.Value.DisableEntityCaching ?
+ await Store.FindByApplicationIdAsync(identifier, cancellationToken) :
+ await Cache.FindByApplicationIdAsync(identifier, cancellationToken);
+
+ if (authorizations.IsEmpty)
+ {
+ return ImmutableArray.Create();
+ }
+
+ if (Options.Value.DisableAdditionalFiltering)
+ {
+ return authorizations;
+ }
+
+ // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
+ // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
+ // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
+
+ var builder = ImmutableArray.CreateBuilder(authorizations.Length);
+
+ foreach (var authorization in authorizations)
+ {
+ if (string.Equals(await Store.GetApplicationIdAsync(authorization, cancellationToken), identifier, StringComparison.Ordinal))
+ {
+ builder.Add(authorization);
+ }
+ }
+
+ return builder.Count == builder.Capacity ?
+ builder.MoveToImmutable() :
+ builder.ToImmutable();
}
///
@@ -485,14 +559,32 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the authorization corresponding to the identifier.
///
- public virtual Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default)
+ public virtual async Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
- return Store.FindByIdAsync(identifier, cancellationToken);
+ var authorization = Options.Value.DisableEntityCaching ?
+ await Store.FindByIdAsync(identifier, cancellationToken) :
+ await Cache.FindByIdAsync(identifier, cancellationToken);
+
+ if (authorization == null)
+ {
+ return null;
+ }
+
+ // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
+ // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
+ // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
+ if (!Options.Value.DisableAdditionalFiltering &&
+ !string.Equals(await Store.GetIdAsync(authorization, cancellationToken), identifier, StringComparison.Ordinal))
+ {
+ return null;
+ }
+
+ return authorization;
}
///
@@ -512,16 +604,24 @@ namespace OpenIddict.Core
throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
}
- // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
- // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
- // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
+ var authorizations = Options.Value.DisableEntityCaching ?
+ await Store.FindBySubjectAsync(subject, cancellationToken) :
+ await Cache.FindBySubjectAsync(subject, cancellationToken);
- var authorizations = await Store.FindBySubjectAsync(subject, cancellationToken);
if (authorizations.IsEmpty)
{
return ImmutableArray.Create();
}
+ if (Options.Value.DisableAdditionalFiltering)
+ {
+ return authorizations;
+ }
+
+ // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
+ // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
+ // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
+
var builder = ImmutableArray.CreateBuilder(authorizations.Length);
foreach (var authorization in authorizations)
@@ -570,6 +670,11 @@ namespace OpenIddict.Core
public virtual Task GetAsync(
[NotNull] Func, IQueryable> query, CancellationToken cancellationToken = default)
{
+ if (query == null)
+ {
+ throw new ArgumentNullException(nameof(query));
+ }
+
return GetAsync((authorizations, state) => state(authorizations), query, cancellationToken);
}
@@ -625,7 +730,8 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the scopes associated with the specified authorization.
///
- public virtual ValueTask> GetScopesAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
+ public virtual ValueTask> GetScopesAsync(
+ [NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
{
if (authorization == null)
{
@@ -644,7 +750,8 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the status associated with the specified authorization.
///
- public virtual ValueTask GetStatusAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
+ public virtual ValueTask GetStatusAsync(
+ [NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
{
if (authorization == null)
{
@@ -663,7 +770,8 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the subject associated with the specified authorization.
///
- public virtual ValueTask GetSubjectAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
+ public virtual ValueTask GetSubjectAsync(
+ [NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
{
if (authorization == null)
{
@@ -682,7 +790,8 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the type associated with the specified authorization.
///
- public virtual ValueTask GetTypeAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
+ public virtual ValueTask GetTypeAsync(
+ [NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
{
if (authorization == null)
{
@@ -740,7 +849,8 @@ namespace OpenIddict.Core
/// The authorization.
/// The that can be used to abort the operation.
/// true if the authorization is permanent, false otherwise.
- public async Task IsPermanentAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
+ public async Task IsPermanentAsync(
+ [NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
{
if (authorization == null)
{
@@ -762,7 +872,8 @@ namespace OpenIddict.Core
/// The authorization.
/// The that can be used to abort the operation.
/// true if the authorization has been revoked, false otherwise.
- public virtual async Task IsRevokedAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
+ public virtual async Task IsRevokedAsync(
+ [NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
{
if (authorization == null)
{
@@ -784,7 +895,8 @@ namespace OpenIddict.Core
/// The authorization.
/// The that can be used to abort the operation.
/// true if the authorization is valid, false otherwise.
- public virtual async Task IsValidAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
+ public virtual async Task IsValidAsync(
+ [NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
{
if (authorization == null)
{
@@ -829,6 +941,11 @@ namespace OpenIddict.Core
public virtual Task> ListAsync(
[NotNull] Func, IQueryable> query, CancellationToken cancellationToken = default)
{
+ if (query == null)
+ {
+ throw new ArgumentNullException(nameof(query));
+ }
+
return ListAsync((authorizations, state) => state(authorizations), query, cancellationToken);
}
@@ -998,6 +1115,11 @@ namespace OpenIddict.Core
throw new OpenIddictExceptions.ValidationException(builder.ToString(), results);
}
+ if (!Options.Value.DisableEntityCaching)
+ {
+ await Cache.RemoveAsync(authorization, cancellationToken);
+ }
+
await Store.UpdateAsync(authorization, cancellationToken);
}
diff --git a/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs b/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs
index 7e481815..f2dfd5c9 100644
--- a/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs
+++ b/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs
@@ -26,15 +26,22 @@ namespace OpenIddict.Core
public class OpenIddictScopeManager : IOpenIddictScopeManager where TScope : class
{
public OpenIddictScopeManager(
+ [NotNull] IOpenIddictScopeCache cache,
[NotNull] IOpenIddictScopeStoreResolver resolver,
[NotNull] ILogger> logger,
[NotNull] IOptions options)
{
+ Cache = cache;
Store = resolver.Get();
Logger = logger;
Options = options;
}
+ ///
+ /// Gets the cache associated with the current manager.
+ ///
+ protected IOpenIddictScopeCache Cache { get; }
+
///