/*
* 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.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using OpenIddict.Abstractions;
namespace OpenIddict.Core
{
///
/// 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 ConcurrentDictionary _signals;
private readonly IOpenIddictAuthorizationStore _store;
public OpenIddictAuthorizationCache(
[NotNull] IOptionsMonitor options,
[NotNull] IOpenIddictAuthorizationStoreResolver resolver)
{
_cache = new MemoryCache(new MemoryCacheOptions
{
SizeLimit = options.CurrentValue.EntityCacheLimit
});
_signals = new ConcurrentDictionary(StringComparer.Ordinal);
_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 ValueTask AddAsync(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)
});
await CreateEntryAsync(new
{
Method = nameof(FindByIdAsync),
Identifier = await _store.GetIdAsync(authorization, cancellationToken)
}, authorization, cancellationToken);
}
///
/// Disposes the resources held by this instance.
///
public void Dispose()
{
foreach (var signal in _signals)
{
signal.Value.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.
/// The authorizations corresponding to the subject/client.
public IAsyncEnumerable 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));
}
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var parameters = new
{
Method = nameof(FindAsync),
Subject = subject,
Client = client
};
if (!_cache.TryGetValue(parameters, out ImmutableArray authorizations))
{
var builder = ImmutableArray.CreateBuilder();
await foreach (var authorization in _store.FindAsync(subject, client, cancellationToken))
{
builder.Add(authorization);
await AddAsync(authorization, cancellationToken);
}
authorizations = builder.ToImmutable();
await CreateEntryAsync(parameters, authorizations, cancellationToken);
}
foreach (var authorization in authorizations)
{
yield return authorization;
}
}
}
///
/// 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.
/// The authorizations corresponding to the criteria.
public IAsyncEnumerable 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));
}
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var parameters = new
{
Method = nameof(FindAsync),
Subject = subject,
Client = client,
Status = status
};
if (!_cache.TryGetValue(parameters, out ImmutableArray authorizations))
{
var builder = ImmutableArray.CreateBuilder();
await foreach (var authorization in _store.FindAsync(subject, client, status, cancellationToken))
{
builder.Add(authorization);
await AddAsync(authorization, cancellationToken);
}
authorizations = builder.ToImmutable();
await CreateEntryAsync(parameters, authorizations, cancellationToken);
}
foreach (var authorization in authorizations)
{
yield return authorization;
}
}
}
///
/// 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.
/// The authorizations corresponding to the criteria.
public IAsyncEnumerable 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));
}
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var parameters = new
{
Method = nameof(FindAsync),
Subject = subject,
Client = client,
Status = status,
Type = type
};
if (!_cache.TryGetValue(parameters, out ImmutableArray authorizations))
{
var builder = ImmutableArray.CreateBuilder();
await foreach (var authorization in _store.FindAsync(subject, client, status, type, cancellationToken))
{
builder.Add(authorization);
await AddAsync(authorization, cancellationToken);
}
authorizations = builder.ToImmutable();
await CreateEntryAsync(parameters, authorizations, cancellationToken);
}
foreach (var authorization in authorizations)
{
yield return authorization;
}
}
}
///
/// 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.
/// The authorizations corresponding to the criteria.
public IAsyncEnumerable 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.
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var authorization in _store.FindAsync(subject, client, status, type, scopes, cancellationToken))
{
await AddAsync(authorization, cancellationToken);
yield return authorization;
}
}
}
///
/// 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.
/// The authorizations corresponding to the specified application.
public IAsyncEnumerable FindByApplicationIdAsync(
[NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var parameters = new
{
Method = nameof(FindByApplicationIdAsync),
Identifier = identifier
};
if (!_cache.TryGetValue(parameters, out ImmutableArray authorizations))
{
var builder = ImmutableArray.CreateBuilder();
await foreach (var authorization in _store.FindByApplicationIdAsync(identifier, cancellationToken))
{
builder.Add(authorization);
await AddAsync(authorization, cancellationToken);
}
authorizations = builder.ToImmutable();
await CreateEntryAsync(parameters, authorizations, cancellationToken);
}
foreach (var authorization in authorizations)
{
yield return authorization;
}
}
}
///
/// 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);
}
return new ValueTask(ExecuteAsync());
async Task ExecuteAsync()
{
if ((authorization = await _store.FindByIdAsync(identifier, cancellationToken)) != null)
{
await AddAsync(authorization, cancellationToken);
}
await CreateEntryAsync(parameters, authorization, cancellationToken);
return authorization;
}
}
///
/// Retrieves all the authorizations corresponding to the specified subject.
///
/// The subject associated with the authorization.
/// The that can be used to abort the operation.
/// The authorizations corresponding to the specified subject.
public IAsyncEnumerable FindBySubjectAsync(
[NotNull] string subject, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(subject))
{
throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
}
return ExecuteAsync(cancellationToken);
async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var parameters = new
{
Method = nameof(FindBySubjectAsync),
Subject = subject
};
if (!_cache.TryGetValue(parameters, out ImmutableArray authorizations))
{
var builder = ImmutableArray.CreateBuilder();
await foreach (var authorization in _store.FindBySubjectAsync(subject, cancellationToken))
{
builder.Add(authorization);
await AddAsync(authorization, cancellationToken);
}
authorizations = builder.ToImmutable();
await CreateEntryAsync(parameters, authorizations, cancellationToken);
}
foreach (var authorization in authorizations)
{
yield return authorization;
}
}
}
///
/// 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 ValueTask RemoveAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
var identifier = await _store.GetIdAsync(authorization, cancellationToken);
if (string.IsNullOrEmpty(identifier))
{
throw new InvalidOperationException("The application identifier cannot be extracted.");
}
if (_signals.TryRemove(identifier, out CancellationTokenSource signal))
{
signal.Cancel();
signal.Dispose();
}
}
///
/// Creates a cache entry for the specified key.
///
/// The cache key.
/// The authorization to store in the cache entry, if applicable.
/// The that can be used to abort the operation.
/// A that can be used to monitor the asynchronous operation.
protected virtual async ValueTask CreateEntryAsync(
[NotNull] object key, [CanBeNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
using var entry = _cache.CreateEntry(key);
if (authorization != null)
{
var signal = await CreateExpirationSignalAsync(authorization, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetSize(1L);
entry.SetValue(authorization);
}
///
/// Creates a cache entry for the specified key.
///
/// The cache key.
/// The authorizations to store in the cache entry.
/// The that can be used to abort the operation.
/// A that can be used to monitor the asynchronous operation.
protected virtual async ValueTask CreateEntryAsync(
[NotNull] object key, [CanBeNull] ImmutableArray authorizations, CancellationToken cancellationToken)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
using var entry = _cache.CreateEntry(key);
foreach (var authorization in authorizations)
{
var signal = await CreateExpirationSignalAsync(authorization, cancellationToken);
if (signal == null)
{
throw new InvalidOperationException("An error occurred while creating an expiration signal.");
}
entry.AddExpirationToken(signal);
}
entry.SetSize(authorizations.Length);
entry.SetValue(authorizations);
}
///
/// Creates an expiration signal allowing to invalidate all the
/// cache entries associated with the specified authorization.
///
/// The authorization associated with the expiration signal.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns an expiration signal for the specified authorization.
///
protected virtual async ValueTask CreateExpirationSignalAsync(
[NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
var identifier = await _store.GetIdAsync(authorization, cancellationToken);
if (string.IsNullOrEmpty(identifier))
{
throw new InvalidOperationException("The authorization identifier cannot be extracted.");
}
var signal = _signals.GetOrAdd(identifier, _ => new CancellationTokenSource());
return new CancellationChangeToken(signal.Token);
}
}
}