/*
* 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)
});
}
}
}