99 changed files with 6174 additions and 298 deletions
@ -0,0 +1,62 @@ |
|||||
|
/* |
||||
|
* 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; |
||||
|
|
||||
|
namespace OpenIddict.Abstractions; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Provides methods allowing to cache resources after retrieving them from the store.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TResource">The type of the resource entity.</typeparam>
|
||||
|
public interface IOpenIddictResourceCache<TResource> where TResource : class |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Add the specified resource to the cache.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource to add to the cache.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
|
||||
|
ValueTask AddAsync(TResource resource, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves a resource using its unique identifier.
|
||||
|
/// </summary>
|
||||
|
/// <param name="identifier">The unique identifier associated with the resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the resource corresponding to the identifier.
|
||||
|
/// </returns>
|
||||
|
ValueTask<TResource?> FindByIdAsync(string identifier, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves a resource using its name.
|
||||
|
/// </summary>
|
||||
|
/// <param name="name">The name associated with the resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the resource corresponding to the specified name.
|
||||
|
/// </returns>
|
||||
|
ValueTask<TResource?> FindByNameAsync(string name, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves a list of resources using their name.
|
||||
|
/// </summary>
|
||||
|
/// <param name="names">The names associated with the resources.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>The resources corresponding to the specified names.</returns>
|
||||
|
IAsyncEnumerable<TResource> FindByNamesAsync(ImmutableArray<string> names, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Removes the specified resource from the cache.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource to remove from the cache.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
|
||||
|
ValueTask RemoveAsync(TResource resource, CancellationToken cancellationToken); |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
using System.Globalization; |
||||
|
using System.Text.Json; |
||||
|
|
||||
|
namespace OpenIddict.Abstractions; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents an OpenIddict resource descriptor.
|
||||
|
/// </summary>
|
||||
|
public class OpenIddictResourceDescriptor |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets or sets the description associated with the resource.
|
||||
|
/// </summary>
|
||||
|
public string? Description { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the localized descriptions associated with the resource.
|
||||
|
/// </summary>
|
||||
|
public Dictionary<CultureInfo, string> Descriptions { get; } = []; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the display name associated with the resource.
|
||||
|
/// </summary>
|
||||
|
public string? DisplayName { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the localized display names associated with the resource.
|
||||
|
/// </summary>
|
||||
|
public Dictionary<CultureInfo, string> DisplayNames { get; } = []; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the unique name associated with the resource.
|
||||
|
/// </summary>
|
||||
|
public string? Name { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the additional properties associated with the resource.
|
||||
|
/// </summary>
|
||||
|
public Dictionary<string, JsonElement> Properties { get; } = new(StringComparer.Ordinal); |
||||
|
} |
||||
@ -0,0 +1,367 @@ |
|||||
|
/* |
||||
|
* 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.ComponentModel.DataAnnotations; |
||||
|
using System.Globalization; |
||||
|
using System.Text.Json; |
||||
|
|
||||
|
namespace OpenIddict.Abstractions; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Provides methods allowing to manage the Resources stored in the store.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// Note: this interface is not meant to be implemented by custom managers,
|
||||
|
/// that should inherit from the generic OpenIddictResourceManager class.
|
||||
|
/// It is primarily intended to be used by services that cannot easily
|
||||
|
/// depend on the generic resource manager. The actual resource entity type is
|
||||
|
/// automatically determined at runtime based on the OpenIddict core options.
|
||||
|
/// </remarks>
|
||||
|
public interface IOpenIddictResourceManager |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Determines the number of resources that exist in the database.
|
||||
|
/// </summary>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the number of resources in the database.
|
||||
|
/// </returns>
|
||||
|
ValueTask<long> CountAsync(CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Determines the number of resources that match the specified query.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TResult">The result type.</typeparam>
|
||||
|
/// <param name="query">The query to execute.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the number of resources that match the specified query.
|
||||
|
/// </returns>
|
||||
|
ValueTask<long> CountAsync<TResult>(Func<IQueryable<object>, IQueryable<TResult>> query, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Determines the number of resources that match the specified query.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TState">The state type.</typeparam>
|
||||
|
/// <typeparam name="TResult">The result type.</typeparam>
|
||||
|
/// <param name="query">The query to execute.</param>
|
||||
|
/// <param name="state">The optional state.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the number of resources that match the specified query.
|
||||
|
/// </returns>
|
||||
|
ValueTask<long> CountAsync<TState, TResult>( |
||||
|
Func<IQueryable<object>, TState, IQueryable<TResult>> query, |
||||
|
TState state, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates a new resource based on the specified descriptor.
|
||||
|
/// </summary>
|
||||
|
/// <param name="descriptor">The resource descriptor.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation, whose result returns the resource.
|
||||
|
/// </returns>
|
||||
|
ValueTask<object> CreateAsync(OpenIddictResourceDescriptor descriptor, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates a new resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource to create.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
|
||||
|
/// </returns>
|
||||
|
ValueTask CreateAsync(object resource, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Removes an existing resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource to delete.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
|
||||
|
/// </returns>
|
||||
|
ValueTask DeleteAsync(object resource, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves a resource using its unique identifier.
|
||||
|
/// </summary>
|
||||
|
/// <param name="identifier">The unique identifier associated with the resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the resource corresponding to the identifier.
|
||||
|
/// </returns>
|
||||
|
ValueTask<object?> FindByIdAsync(string identifier, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves a resource using its name.
|
||||
|
/// </summary>
|
||||
|
/// <param name="name">The name associated with the resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the resource corresponding to the specified name.
|
||||
|
/// </returns>
|
||||
|
ValueTask<object?> FindByNameAsync(string name, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves a list of resources using their name.
|
||||
|
/// </summary>
|
||||
|
/// <param name="names">The names associated with the resources.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>The resources corresponding to the specified names.</returns>
|
||||
|
IAsyncEnumerable<object> FindByNamesAsync(ImmutableArray<string> names, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Executes the specified query and returns the first element.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TResult">The result type.</typeparam>
|
||||
|
/// <param name="query">The query to execute.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the first element returned when executing the query.
|
||||
|
/// </returns>
|
||||
|
ValueTask<TResult?> GetAsync<TResult>( |
||||
|
Func<IQueryable<object>, IQueryable<TResult>> query, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Executes the specified query and returns the first element.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TState">The state type.</typeparam>
|
||||
|
/// <typeparam name="TResult">The result type.</typeparam>
|
||||
|
/// <param name="query">The query to execute.</param>
|
||||
|
/// <param name="state">The optional state.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the first element returned when executing the query.
|
||||
|
/// </returns>
|
||||
|
ValueTask<TResult?> GetAsync<TState, TResult>( |
||||
|
Func<IQueryable<object>, TState, IQueryable<TResult>> query, |
||||
|
TState state, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the description associated with a resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the description associated with the specified resource.
|
||||
|
/// </returns>
|
||||
|
ValueTask<string?> GetDescriptionAsync(object resource, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the localized descriptions associated with an resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns all the localized descriptions associated with the resource.
|
||||
|
/// </returns>
|
||||
|
ValueTask<ImmutableDictionary<CultureInfo, string>> GetDescriptionsAsync(object resource, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the display name associated with a resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the display name associated with the resource.
|
||||
|
/// </returns>
|
||||
|
ValueTask<string?> GetDisplayNameAsync(object resource, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the localized display names associated with an resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns all the localized display names associated with the resource.
|
||||
|
/// </returns>
|
||||
|
ValueTask<ImmutableDictionary<CultureInfo, string>> GetDisplayNamesAsync(object resource, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the unique identifier associated with a resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the unique identifier associated with the resource.
|
||||
|
/// </returns>
|
||||
|
ValueTask<string?> GetIdAsync(object resource, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the localized description associated with an resource
|
||||
|
/// and corresponding to the current UI culture or one of its parents.
|
||||
|
/// If no matching value can be found, the non-localized value is returned.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the matching localized description associated with the resource.
|
||||
|
/// </returns>
|
||||
|
ValueTask<string?> GetLocalizedDescriptionAsync(object resource, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the localized description associated with an resource
|
||||
|
/// and corresponding to the specified culture or one of its parents.
|
||||
|
/// If no matching value can be found, the non-localized value is returned.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="culture">The culture (typically <see cref="CultureInfo.CurrentUICulture"/>).</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the matching localized description associated with the resource.
|
||||
|
/// </returns>
|
||||
|
ValueTask<string?> GetLocalizedDescriptionAsync(object resource, CultureInfo culture, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the localized display name associated with an resource
|
||||
|
/// and corresponding to the current UI culture or one of its parents.
|
||||
|
/// If no matching value can be found, the non-localized value is returned.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the display name associated with the resource.
|
||||
|
/// </returns>
|
||||
|
ValueTask<string?> GetLocalizedDisplayNameAsync(object resource, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the localized display name associated with an resource
|
||||
|
/// and corresponding to the specified culture or one of its parents.
|
||||
|
/// If no matching value can be found, the non-localized value is returned.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="culture">The culture (typically <see cref="CultureInfo.CurrentUICulture"/>).</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the display name associated with the resource.
|
||||
|
/// </returns>
|
||||
|
ValueTask<string?> GetLocalizedDisplayNameAsync(object resource, CultureInfo culture, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the name associated with a resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the name associated with the specified resource.
|
||||
|
/// </returns>
|
||||
|
ValueTask<string?> GetNameAsync(object resource, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the additional properties associated with a resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns all the additional properties associated with the resource.
|
||||
|
/// </returns>
|
||||
|
ValueTask<ImmutableDictionary<string, JsonElement>> GetPropertiesAsync(object resource, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Executes the specified query and returns all the corresponding elements.
|
||||
|
/// </summary>
|
||||
|
/// <param name="count">The number of results to return.</param>
|
||||
|
/// <param name="offset">The number of results to skip.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>All the elements returned when executing the specified query.</returns>
|
||||
|
IAsyncEnumerable<object> ListAsync( |
||||
|
int? count = null, int? offset = null, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Executes the specified query and returns all the corresponding elements.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TResult">The result type.</typeparam>
|
||||
|
/// <param name="query">The query to execute.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>All the elements returned when executing the specified query.</returns>
|
||||
|
IAsyncEnumerable<TResult> ListAsync<TResult>( |
||||
|
Func<IQueryable<object>, IQueryable<TResult>> query, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Executes the specified query and returns all the corresponding elements.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TState">The state type.</typeparam>
|
||||
|
/// <typeparam name="TResult">The result type.</typeparam>
|
||||
|
/// <param name="query">The query to execute.</param>
|
||||
|
/// <param name="state">The optional state.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>All the elements returned when executing the specified query.</returns>
|
||||
|
IAsyncEnumerable<TResult> ListAsync<TState, TResult>( |
||||
|
Func<IQueryable<object>, TState, IQueryable<TResult>> query, |
||||
|
TState state, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Populates the specified descriptor using the properties exposed by the resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="descriptor">The descriptor.</param>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
|
||||
|
/// </returns>
|
||||
|
ValueTask PopulateAsync(OpenIddictResourceDescriptor descriptor, object resource, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Populates the resource using the specified descriptor.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="descriptor">The descriptor.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
|
||||
|
/// </returns>
|
||||
|
ValueTask PopulateAsync(object resource, OpenIddictResourceDescriptor descriptor, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Updates an existing resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource to update.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
|
||||
|
/// </returns>
|
||||
|
ValueTask UpdateAsync(object resource, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Updates an existing resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource to update.</param>
|
||||
|
/// <param name="descriptor">The descriptor used to update the resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
|
||||
|
/// </returns>
|
||||
|
ValueTask UpdateAsync(object resource, OpenIddictResourceDescriptor descriptor, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Validates the resource to ensure it's in a consistent state.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>The validation error encountered when validating the resource.</returns>
|
||||
|
IAsyncEnumerable<ValidationResult> ValidateAsync(object resource, CancellationToken cancellationToken = default); |
||||
|
} |
||||
@ -0,0 +1,280 @@ |
|||||
|
/* |
||||
|
* 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.Globalization; |
||||
|
using System.Text.Json; |
||||
|
|
||||
|
namespace OpenIddict.Abstractions; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Provides methods allowing to manage the resources stored in a database.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TResource">The type of the resource entity.</typeparam>
|
||||
|
public interface IOpenIddictResourceStore<TResource> where TResource : class |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Determines the number of resources that exist in the database.
|
||||
|
/// </summary>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the number of resources in the database.
|
||||
|
/// </returns>
|
||||
|
ValueTask<long> CountAsync(CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Determines the number of resources that match the specified query.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TState">The state type.</typeparam>
|
||||
|
/// <typeparam name="TResult">The result type.</typeparam>
|
||||
|
/// <param name="query">The query to execute.</param>
|
||||
|
/// <param name="state">The optional state.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the number of resources that match the specified query.
|
||||
|
/// </returns>
|
||||
|
ValueTask<long> CountAsync<TState, TResult>( |
||||
|
Func<IQueryable<TResource>, TState, IQueryable<TResult>> query, |
||||
|
TState state, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates a new resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource to create.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
|
||||
|
ValueTask CreateAsync(TResource resource, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Removes an existing resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource to delete.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
|
||||
|
ValueTask DeleteAsync(TResource resource, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves a resource using its unique identifier.
|
||||
|
/// </summary>
|
||||
|
/// <param name="identifier">The unique identifier associated with the resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the resource corresponding to the identifier.
|
||||
|
/// </returns>
|
||||
|
ValueTask<TResource?> FindByIdAsync(string identifier, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves a resource using its name.
|
||||
|
/// </summary>
|
||||
|
/// <param name="name">The name associated with the resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the resource corresponding to the specified name.
|
||||
|
/// </returns>
|
||||
|
ValueTask<TResource?> FindByNameAsync(string name, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves a list of resources using their name.
|
||||
|
/// </summary>
|
||||
|
/// <param name="names">The names associated with the resources.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>The resources corresponding to the specified names.</returns>
|
||||
|
IAsyncEnumerable<TResource> FindByNamesAsync(ImmutableArray<string> names, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Executes the specified query and returns the first element.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TState">The state type.</typeparam>
|
||||
|
/// <typeparam name="TResult">The result type.</typeparam>
|
||||
|
/// <param name="query">The query to execute.</param>
|
||||
|
/// <param name="state">The optional state.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the first element returned when executing the query.
|
||||
|
/// </returns>
|
||||
|
ValueTask<TResult?> GetAsync<TState, TResult>( |
||||
|
Func<IQueryable<TResource>, TState, IQueryable<TResult>> query, |
||||
|
TState state, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the description associated with a resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the description associated with the specified resource.
|
||||
|
/// </returns>
|
||||
|
ValueTask<string?> GetDescriptionAsync(TResource resource, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the localized descriptions associated with a resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns all the localized descriptions associated with the specified resource.
|
||||
|
/// </returns>
|
||||
|
ValueTask<ImmutableDictionary<CultureInfo, string>> GetDescriptionsAsync(TResource resource, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the display name associated with a resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the display name associated with the resource.
|
||||
|
/// </returns>
|
||||
|
ValueTask<string?> GetDisplayNameAsync(TResource resource, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the localized display names associated with a resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns all the localized display names associated with the resource.
|
||||
|
/// </returns>
|
||||
|
ValueTask<ImmutableDictionary<CultureInfo, string>> GetDisplayNamesAsync(TResource resource, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the unique identifier associated with a resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the unique identifier associated with the resource.
|
||||
|
/// </returns>
|
||||
|
ValueTask<string?> GetIdAsync(TResource resource, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the name associated with a resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the name associated with the specified resource.
|
||||
|
/// </returns>
|
||||
|
ValueTask<string?> GetNameAsync(TResource resource, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the additional properties associated with a resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, whose
|
||||
|
/// result returns all the additional properties associated with the resource.
|
||||
|
/// </returns>
|
||||
|
ValueTask<ImmutableDictionary<string, JsonElement>> GetPropertiesAsync(TResource resource, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Instantiates a new resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the instantiated resource, that can be persisted in the database.
|
||||
|
/// </returns>
|
||||
|
ValueTask<TResource> InstantiateAsync(CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Executes the specified query and returns all the corresponding elements.
|
||||
|
/// </summary>
|
||||
|
/// <param name="count">The number of results to return.</param>
|
||||
|
/// <param name="offset">The number of results to skip.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>All the elements returned when executing the specified query.</returns>
|
||||
|
IAsyncEnumerable<TResource> ListAsync(int? count, int? offset, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Executes the specified query and returns all the corresponding elements.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TState">The state type.</typeparam>
|
||||
|
/// <typeparam name="TResult">The result type.</typeparam>
|
||||
|
/// <param name="query">The query to execute.</param>
|
||||
|
/// <param name="state">The optional state.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>All the elements returned when executing the specified query.</returns>
|
||||
|
IAsyncEnumerable<TResult> ListAsync<TState, TResult>( |
||||
|
Func<IQueryable<TResource>, TState, IQueryable<TResult>> query, |
||||
|
TState state, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sets the description associated with a resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="description">The description associated with the authorization.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
|
||||
|
ValueTask SetDescriptionAsync(TResource resource, string? description, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sets the localized descriptions associated with a resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="descriptions">The localized descriptions associated with the authorization.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
|
||||
|
ValueTask SetDescriptionsAsync(TResource resource, |
||||
|
ImmutableDictionary<CultureInfo, string> descriptions, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sets the display name associated with a resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="name">The display name associated with the resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
|
||||
|
ValueTask SetDisplayNameAsync(TResource resource, string? name, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sets the localized display names associated with a resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="names">The localized display names associated with the resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
|
||||
|
ValueTask SetDisplayNamesAsync(TResource resource, |
||||
|
ImmutableDictionary<CultureInfo, string> names, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sets the name associated with a resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="name">The name associated with the authorization.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
|
||||
|
ValueTask SetNameAsync(TResource resource, string? name, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sets the additional properties associated with a resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="properties">The additional properties associated with the resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
|
||||
|
ValueTask SetPropertiesAsync(TResource resource, |
||||
|
ImmutableDictionary<string, JsonElement> properties, CancellationToken cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Updates an existing resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource to update.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
|
||||
|
ValueTask UpdateAsync(TResource resource, CancellationToken cancellationToken); |
||||
|
} |
||||
@ -0,0 +1,258 @@ |
|||||
|
/* |
||||
|
* 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.Concurrent; |
||||
|
using System.Collections.Immutable; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using Microsoft.Extensions.Caching.Memory; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Microsoft.Extensions.Primitives; |
||||
|
|
||||
|
namespace OpenIddict.Core; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Provides methods allowing to cache resources after retrieving them from the store.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TResource">The type of the Resource entity.</typeparam>
|
||||
|
public sealed class OpenIddictResourceCache<TResource> : IOpenIddictResourceCache<TResource>, IDisposable where TResource : class |
||||
|
{ |
||||
|
private readonly MemoryCache _cache; |
||||
|
private readonly ConcurrentDictionary<string, CancellationTokenSource> _signals; |
||||
|
private readonly IOpenIddictResourceStore<TResource> _store; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates a new instance of the <see cref="OpenIddictResourceCache{TResource}"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="options">The options.</param>
|
||||
|
/// <param name="store">The store.</param>
|
||||
|
public OpenIddictResourceCache( |
||||
|
IOptionsMonitor<OpenIddictCoreOptions> options, |
||||
|
IOpenIddictResourceStore<TResource> store) |
||||
|
{ |
||||
|
_cache = new MemoryCache(new MemoryCacheOptions |
||||
|
{ |
||||
|
SizeLimit = (options ?? throw new ArgumentNullException(nameof(options))).CurrentValue.EntityCacheLimit |
||||
|
}); |
||||
|
|
||||
|
_signals = new ConcurrentDictionary<string, CancellationTokenSource>(StringComparer.Ordinal); |
||||
|
_store = store ?? throw new ArgumentNullException(nameof(store)); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public async ValueTask AddAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
_cache.Remove(new |
||||
|
{ |
||||
|
Method = nameof(FindByIdAsync), |
||||
|
Identifier = await _store.GetIdAsync(resource, cancellationToken) |
||||
|
}); |
||||
|
|
||||
|
_cache.Remove(new |
||||
|
{ |
||||
|
Method = nameof(FindByNameAsync), |
||||
|
Name = await _store.GetNameAsync(resource, cancellationToken) |
||||
|
}); |
||||
|
|
||||
|
await CreateEntryAsync(new |
||||
|
{ |
||||
|
Method = nameof(FindByIdAsync), |
||||
|
Identifier = await _store.GetIdAsync(resource, cancellationToken) |
||||
|
}, resource, cancellationToken); |
||||
|
|
||||
|
await CreateEntryAsync(new |
||||
|
{ |
||||
|
Method = nameof(FindByNameAsync), |
||||
|
Name = await _store.GetNameAsync(resource, cancellationToken) |
||||
|
}, resource, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
foreach (var signal in _signals) |
||||
|
{ |
||||
|
signal.Value.Dispose(); |
||||
|
} |
||||
|
|
||||
|
_cache.Dispose(); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public ValueTask<TResource?> FindByIdAsync(string identifier, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentException.ThrowIfNullOrEmpty(identifier); |
||||
|
|
||||
|
var parameters = new |
||||
|
{ |
||||
|
Method = nameof(FindByIdAsync), |
||||
|
Identifier = identifier |
||||
|
}; |
||||
|
|
||||
|
if (_cache.TryGetValue(parameters, out TResource? resource)) |
||||
|
{ |
||||
|
return new(resource); |
||||
|
} |
||||
|
|
||||
|
return new(ExecuteAsync()); |
||||
|
|
||||
|
async Task<TResource?> ExecuteAsync() |
||||
|
{ |
||||
|
if ((resource = await _store.FindByIdAsync(identifier, cancellationToken)) is not null) |
||||
|
{ |
||||
|
await AddAsync(resource, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
await CreateEntryAsync(parameters, resource, cancellationToken); |
||||
|
|
||||
|
return resource; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public ValueTask<TResource?> FindByNameAsync(string name, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentException.ThrowIfNullOrEmpty(name); |
||||
|
|
||||
|
var parameters = new |
||||
|
{ |
||||
|
Method = nameof(FindByNameAsync), |
||||
|
Name = name |
||||
|
}; |
||||
|
|
||||
|
if (_cache.TryGetValue(parameters, out TResource? resource)) |
||||
|
{ |
||||
|
return new(resource); |
||||
|
} |
||||
|
|
||||
|
async Task<TResource?> ExecuteAsync() |
||||
|
{ |
||||
|
if ((resource = await _store.FindByNameAsync(name, cancellationToken)) is not null) |
||||
|
{ |
||||
|
await AddAsync(resource, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
await CreateEntryAsync(parameters, resource, cancellationToken); |
||||
|
|
||||
|
return resource; |
||||
|
} |
||||
|
|
||||
|
return new(ExecuteAsync()); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public IAsyncEnumerable<TResource> FindByNamesAsync(ImmutableArray<string> names, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
if (names.Any(string.IsNullOrEmpty)) |
||||
|
{ |
||||
|
throw new ArgumentException(SR.GetResourceString(SR.ID0203), nameof(names)); |
||||
|
} |
||||
|
|
||||
|
// Note: this method is only partially cached.
|
||||
|
|
||||
|
return ExecuteAsync(cancellationToken); |
||||
|
|
||||
|
async IAsyncEnumerable<TResource> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) |
||||
|
{ |
||||
|
await foreach (var resource in _store.FindByNamesAsync(names, cancellationToken)) |
||||
|
{ |
||||
|
await AddAsync(resource, cancellationToken); |
||||
|
|
||||
|
yield return resource; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public async ValueTask RemoveAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
var identifier = await _store.GetIdAsync(resource, cancellationToken); |
||||
|
if (string.IsNullOrEmpty(identifier)) |
||||
|
{ |
||||
|
throw new InvalidOperationException(SR.GetResourceString(SR.ID0196)); |
||||
|
} |
||||
|
|
||||
|
if (_signals.TryRemove(identifier, out CancellationTokenSource? signal)) |
||||
|
{ |
||||
|
signal.Cancel(); |
||||
|
signal.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates a cache entry for the specified key.
|
||||
|
/// </summary>
|
||||
|
/// <param name="key">The cache key.</param>
|
||||
|
/// <param name="resource">The resource to store in the cache entry, if applicable.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
|
||||
|
private async ValueTask CreateEntryAsync(object key, TResource? resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(key); |
||||
|
|
||||
|
using var entry = _cache.CreateEntry(key); |
||||
|
|
||||
|
if (resource is not null) |
||||
|
{ |
||||
|
entry.AddExpirationToken(await CreateExpirationSignalAsync(resource, cancellationToken) ?? |
||||
|
throw new InvalidOperationException(SR.GetResourceString(SR.ID0197))); |
||||
|
} |
||||
|
|
||||
|
entry.Size = 1L; |
||||
|
entry.Value = resource; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates a cache entry for the specified key.
|
||||
|
/// </summary>
|
||||
|
/// <param name="key">The cache key.</param>
|
||||
|
/// <param name="resources">The resources to store in the cache entry.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
|
||||
|
private async ValueTask CreateEntryAsync(object key, ImmutableArray<TResource> resources, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(key); |
||||
|
|
||||
|
using var entry = _cache.CreateEntry(key); |
||||
|
|
||||
|
foreach (var resource in resources) |
||||
|
{ |
||||
|
entry.AddExpirationToken(await CreateExpirationSignalAsync(resource, cancellationToken) ?? |
||||
|
throw new InvalidOperationException(SR.GetResourceString(SR.ID0197))); |
||||
|
} |
||||
|
|
||||
|
entry.Size = resources.Length; |
||||
|
entry.Value = resources; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates an expiration signal allowing to invalidate all the
|
||||
|
/// cache entries associated with the specified resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource associated with the expiration signal.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns an expiration signal for the specified resource.
|
||||
|
/// </returns>
|
||||
|
private async ValueTask<IChangeToken> CreateExpirationSignalAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
var identifier = await _store.GetIdAsync(resource, cancellationToken); |
||||
|
if (string.IsNullOrEmpty(identifier)) |
||||
|
{ |
||||
|
throw new InvalidOperationException(SR.GetResourceString(SR.ID0204)); |
||||
|
} |
||||
|
|
||||
|
var signal = _signals.GetOrAdd(identifier, _ => new CancellationTokenSource()); |
||||
|
|
||||
|
return new CancellationChangeToken(signal.Token); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,936 @@ |
|||||
|
/* |
||||
|
* 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.ComponentModel.DataAnnotations; |
||||
|
using System.Globalization; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using System.Text; |
||||
|
using System.Text.Json; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using ValidationException = OpenIddict.Abstractions.OpenIddictExceptions.ValidationException; |
||||
|
|
||||
|
namespace OpenIddict.Core; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Provides methods allowing to manage the resources stored in the store.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// Applications that do not want to depend on a specific entity type can use the non-generic
|
||||
|
/// <see cref="IOpenIddictResourceManager"/> instead, for which the actual entity type is resolved at runtime.
|
||||
|
/// </remarks>
|
||||
|
/// <typeparam name="TResource">The type of the resource entity.</typeparam>
|
||||
|
public class OpenIddictResourceManager<TResource> : IOpenIddictResourceManager where TResource : class |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Creates a new instance of the <see cref="OpenIddictResourceManager{TResource}"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="cache">The cache.</param>
|
||||
|
/// <param name="logger">The logger.</param>
|
||||
|
/// <param name="options">The options.</param>
|
||||
|
/// <param name="store">The store.</param>
|
||||
|
public OpenIddictResourceManager( |
||||
|
IOpenIddictResourceCache<TResource> cache, |
||||
|
ILogger<OpenIddictResourceManager<TResource>> logger, |
||||
|
IOptionsMonitor<OpenIddictCoreOptions> options, |
||||
|
IOpenIddictResourceStore<TResource> store) |
||||
|
{ |
||||
|
Cache = cache ?? throw new ArgumentNullException(nameof(cache)); |
||||
|
Logger = logger ?? throw new ArgumentNullException(nameof(logger)); |
||||
|
Options = options ?? throw new ArgumentNullException(nameof(options)); |
||||
|
Store = store ?? throw new ArgumentNullException(nameof(store)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the cache associated with the current manager.
|
||||
|
/// </summary>
|
||||
|
protected IOpenIddictResourceCache<TResource> Cache { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the logger associated with the current manager.
|
||||
|
/// </summary>
|
||||
|
protected ILogger Logger { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the options associated with the current manager.
|
||||
|
/// </summary>
|
||||
|
protected IOptionsMonitor<OpenIddictCoreOptions> Options { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the store associated with the current manager.
|
||||
|
/// </summary>
|
||||
|
protected IOpenIddictResourceStore<TResource> Store { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Determines the number of resources that exist in the database.
|
||||
|
/// </summary>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the number of resources in the database.
|
||||
|
/// </returns>
|
||||
|
public virtual ValueTask<long> CountAsync(CancellationToken cancellationToken = default) |
||||
|
=> Store.CountAsync(cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Determines the number of resources that match the specified query.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TResult">The result type.</typeparam>
|
||||
|
/// <param name="query">The query to execute.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the number of resources that match the specified query.
|
||||
|
/// </returns>
|
||||
|
public virtual ValueTask<long> CountAsync<TResult>( |
||||
|
Func<IQueryable<TResource>, IQueryable<TResult>> query, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(query); |
||||
|
|
||||
|
return CountAsync(static (resources, query) => query(resources), query, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Determines the number of resources that match the specified query.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TState">The state type.</typeparam>
|
||||
|
/// <typeparam name="TResult">The result type.</typeparam>
|
||||
|
/// <param name="query">The query to execute.</param>
|
||||
|
/// <param name="state">The optional state.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the number of resources that match the specified query.
|
||||
|
/// </returns>
|
||||
|
public virtual ValueTask<long> CountAsync<TState, TResult>( |
||||
|
Func<IQueryable<TResource>, TState, IQueryable<TResult>> query, |
||||
|
TState state, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(query); |
||||
|
|
||||
|
return Store.CountAsync(query, state, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates a new resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource to create.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
|
||||
|
/// </returns>
|
||||
|
public virtual async ValueTask CreateAsync(TResource resource, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
var results = await GetValidationResultsAsync(resource, cancellationToken); |
||||
|
if (results.Any(result => result != ValidationResult.Success)) |
||||
|
{ |
||||
|
var builder = new StringBuilder(); |
||||
|
builder.AppendLine(SR.GetResourceString(SR.ID0207)); |
||||
|
builder.AppendLine(); |
||||
|
|
||||
|
foreach (var result in results) |
||||
|
{ |
||||
|
builder.AppendLine(result.ErrorMessage); |
||||
|
} |
||||
|
|
||||
|
throw new ValidationException(builder.ToString(), results); |
||||
|
} |
||||
|
|
||||
|
await Store.CreateAsync(resource, cancellationToken); |
||||
|
|
||||
|
if (!Options.CurrentValue.DisableEntityCaching) |
||||
|
{ |
||||
|
await Cache.AddAsync(resource, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
async Task<ImmutableArray<ValidationResult>> GetValidationResultsAsync( |
||||
|
TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
var builder = ImmutableArray.CreateBuilder<ValidationResult>(); |
||||
|
|
||||
|
await foreach (var result in ValidateAsync(resource, cancellationToken)) |
||||
|
{ |
||||
|
builder.Add(result); |
||||
|
} |
||||
|
|
||||
|
return builder.ToImmutable(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates a new resource based on the specified descriptor.
|
||||
|
/// </summary>
|
||||
|
/// <param name="descriptor">The resource descriptor.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation, whose result returns the resource.
|
||||
|
/// </returns>
|
||||
|
public virtual async ValueTask<TResource> CreateAsync( |
||||
|
OpenIddictResourceDescriptor descriptor, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(descriptor); |
||||
|
|
||||
|
var resource = await Store.InstantiateAsync(cancellationToken) ?? |
||||
|
throw new InvalidOperationException(SR.GetResourceString(SR.ID0208)); |
||||
|
|
||||
|
await PopulateAsync(resource, descriptor, cancellationToken); |
||||
|
await CreateAsync(resource, cancellationToken); |
||||
|
|
||||
|
return resource; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Removes an existing resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource to delete.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
|
||||
|
/// </returns>
|
||||
|
public virtual async ValueTask DeleteAsync(TResource resource, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
if (!Options.CurrentValue.DisableEntityCaching) |
||||
|
{ |
||||
|
await Cache.RemoveAsync(resource, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
await Store.DeleteAsync(resource, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves a resource using its unique identifier.
|
||||
|
/// </summary>
|
||||
|
/// <param name="identifier">The unique identifier associated with the resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the resource corresponding to the identifier.
|
||||
|
/// </returns>
|
||||
|
public virtual async ValueTask<TResource?> FindByIdAsync(string identifier, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
ArgumentException.ThrowIfNullOrEmpty(identifier); |
||||
|
|
||||
|
var resource = Options.CurrentValue.DisableEntityCaching ? |
||||
|
await Store.FindByIdAsync(identifier, cancellationToken) : |
||||
|
await Cache.FindByIdAsync(identifier, cancellationToken); |
||||
|
|
||||
|
if (resource is 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.CurrentValue.DisableAdditionalFiltering && |
||||
|
!string.Equals(await Store.GetIdAsync(resource, cancellationToken), identifier, StringComparison.Ordinal)) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
return resource; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves a resource using its name.
|
||||
|
/// </summary>
|
||||
|
/// <param name="name">The name associated with the resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the resource corresponding to the specified name.
|
||||
|
/// </returns>
|
||||
|
public virtual async ValueTask<TResource?> FindByNameAsync(string name, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
ArgumentException.ThrowIfNullOrEmpty(name); |
||||
|
|
||||
|
var resource = Options.CurrentValue.DisableEntityCaching ? |
||||
|
await Store.FindByNameAsync(name, cancellationToken) : |
||||
|
await Cache.FindByNameAsync(name, cancellationToken); |
||||
|
|
||||
|
if (resource is 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.CurrentValue.DisableAdditionalFiltering && |
||||
|
!string.Equals(await Store.GetNameAsync(resource, cancellationToken), name, StringComparison.Ordinal)) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
return resource; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves a list of resources using their name.
|
||||
|
/// </summary>
|
||||
|
/// <param name="names">The names associated with the resources.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>The resources corresponding to the specified names.</returns>
|
||||
|
public virtual IAsyncEnumerable<TResource> FindByNamesAsync( |
||||
|
ImmutableArray<string> names, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
if (names.Any(string.IsNullOrEmpty)) |
||||
|
{ |
||||
|
throw new ArgumentException(SR.GetResourceString(SR.ID0203), nameof(names)); |
||||
|
} |
||||
|
|
||||
|
var resources = Options.CurrentValue.DisableEntityCaching ? |
||||
|
Store.FindByNamesAsync(names, cancellationToken) : |
||||
|
Cache.FindByNamesAsync(names, cancellationToken); |
||||
|
|
||||
|
if (Options.CurrentValue.DisableAdditionalFiltering) |
||||
|
{ |
||||
|
return resources; |
||||
|
} |
||||
|
|
||||
|
// 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.
|
||||
|
|
||||
|
return ExecuteAsync(cancellationToken); |
||||
|
|
||||
|
async IAsyncEnumerable<TResource> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) |
||||
|
{ |
||||
|
await foreach (var resource in resources) |
||||
|
{ |
||||
|
var name = await Store.GetNameAsync(resource, cancellationToken); |
||||
|
if (!string.IsNullOrEmpty(name) && names.Contains(name, StringComparer.Ordinal)) |
||||
|
{ |
||||
|
yield return resource; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Executes the specified query and returns the first element.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TResult">The result type.</typeparam>
|
||||
|
/// <param name="query">The query to execute.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the first element returned when executing the query.
|
||||
|
/// </returns>
|
||||
|
public virtual ValueTask<TResult?> GetAsync<TResult>( |
||||
|
Func<IQueryable<TResource>, IQueryable<TResult>> query, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(query); |
||||
|
|
||||
|
return GetAsync(static (resources, query) => query(resources), query, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Executes the specified query and returns the first element.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TState">The state type.</typeparam>
|
||||
|
/// <typeparam name="TResult">The result type.</typeparam>
|
||||
|
/// <param name="query">The query to execute.</param>
|
||||
|
/// <param name="state">The optional state.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the first element returned when executing the query.
|
||||
|
/// </returns>
|
||||
|
public virtual ValueTask<TResult?> GetAsync<TState, TResult>( |
||||
|
Func<IQueryable<TResource>, TState, IQueryable<TResult>> query, |
||||
|
TState state, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(query); |
||||
|
|
||||
|
return Store.GetAsync(query, state, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the description associated with a resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the description associated with the specified resource.
|
||||
|
/// </returns>
|
||||
|
public virtual ValueTask<string?> GetDescriptionAsync(TResource resource, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
return Store.GetDescriptionAsync(resource, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the localized descriptions associated with an resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns all the localized descriptions associated with the resource.
|
||||
|
/// </returns>
|
||||
|
public virtual async ValueTask<ImmutableDictionary<CultureInfo, string>> GetDescriptionsAsync( |
||||
|
TResource resource, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
var descriptions = await Store.GetDescriptionsAsync(resource, cancellationToken); |
||||
|
if (descriptions is not { Count: > 0 }) |
||||
|
{ |
||||
|
return ImmutableDictionary.Create<CultureInfo, string>(); |
||||
|
} |
||||
|
|
||||
|
return descriptions; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the display name associated with a resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the display name associated with the resource.
|
||||
|
/// </returns>
|
||||
|
public virtual ValueTask<string?> GetDisplayNameAsync(TResource resource, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
return Store.GetDisplayNameAsync(resource, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the localized display names associated with an resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns all the localized display names associated with the resource.
|
||||
|
/// </returns>
|
||||
|
public virtual async ValueTask<ImmutableDictionary<CultureInfo, string>> GetDisplayNamesAsync( |
||||
|
TResource resource, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
var names = await Store.GetDisplayNamesAsync(resource, cancellationToken); |
||||
|
if (names is not { Count: > 0 }) |
||||
|
{ |
||||
|
return ImmutableDictionary.Create<CultureInfo, string>(); |
||||
|
} |
||||
|
|
||||
|
return names; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the unique identifier associated with a resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the unique identifier associated with the resource.
|
||||
|
/// </returns>
|
||||
|
public virtual ValueTask<string?> GetIdAsync(TResource resource, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
return Store.GetIdAsync(resource, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the localized display name associated with an resource
|
||||
|
/// and corresponding to the current UI culture or one of its parents.
|
||||
|
/// If no matching value can be found, the non-localized value is returned.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the matching display name associated with the resource.
|
||||
|
/// </returns>
|
||||
|
public virtual ValueTask<string?> GetLocalizedDisplayNameAsync(TResource resource, CancellationToken cancellationToken = default) |
||||
|
=> GetLocalizedDisplayNameAsync(resource, CultureInfo.CurrentUICulture, cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the localized display name associated with an resource
|
||||
|
/// and corresponding to the specified culture or one of its parents.
|
||||
|
/// If no matching value can be found, the non-localized value is returned.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="culture">The culture (typically <see cref="CultureInfo.CurrentUICulture"/>).</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the matching display name associated with the resource.
|
||||
|
/// </returns>
|
||||
|
public virtual async ValueTask<string?> GetLocalizedDisplayNameAsync( |
||||
|
TResource resource, CultureInfo culture, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
ArgumentNullException.ThrowIfNull(culture); |
||||
|
|
||||
|
var names = await Store.GetDisplayNamesAsync(resource, cancellationToken); |
||||
|
if (names is not { Count: > 0 }) |
||||
|
{ |
||||
|
return await Store.GetDisplayNameAsync(resource, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
do |
||||
|
{ |
||||
|
if (names.TryGetValue(culture, out var name)) |
||||
|
{ |
||||
|
return name; |
||||
|
} |
||||
|
|
||||
|
culture = culture.Parent; |
||||
|
} |
||||
|
|
||||
|
while (culture != CultureInfo.InvariantCulture); |
||||
|
|
||||
|
return await Store.GetDisplayNameAsync(resource, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the localized description associated with an resource
|
||||
|
/// and corresponding to the current UI culture or one of its parents.
|
||||
|
/// If no matching value can be found, the non-localized value is returned.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the matching localized description associated with the resource.
|
||||
|
/// </returns>
|
||||
|
public virtual ValueTask<string?> GetLocalizedDescriptionAsync(TResource resource, CancellationToken cancellationToken = default) |
||||
|
=> GetLocalizedDescriptionAsync(resource, CultureInfo.CurrentUICulture, cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the localized description associated with an resource
|
||||
|
/// and corresponding to the specified culture or one of its parents.
|
||||
|
/// If no matching value can be found, the non-localized value is returned.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="culture">The culture (typically <see cref="CultureInfo.CurrentUICulture"/>).</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the matching localized description associated with the resource.
|
||||
|
/// </returns>
|
||||
|
public virtual async ValueTask<string?> GetLocalizedDescriptionAsync( |
||||
|
TResource resource, CultureInfo culture, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
ArgumentNullException.ThrowIfNull(culture); |
||||
|
|
||||
|
var descriptions = await Store.GetDescriptionsAsync(resource, cancellationToken); |
||||
|
if (descriptions is not { Count: > 0 }) |
||||
|
{ |
||||
|
return await Store.GetDescriptionAsync(resource, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
do |
||||
|
{ |
||||
|
if (descriptions.TryGetValue(culture, out var description)) |
||||
|
{ |
||||
|
return description; |
||||
|
} |
||||
|
|
||||
|
culture = culture.Parent; |
||||
|
} |
||||
|
|
||||
|
while (culture != CultureInfo.InvariantCulture); |
||||
|
|
||||
|
return await Store.GetDescriptionAsync(resource, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the name associated with a resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns the name associated with the specified resource.
|
||||
|
/// </returns>
|
||||
|
public virtual ValueTask<string?> GetNameAsync(TResource resource, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
return Store.GetNameAsync(resource, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieves the additional properties associated with a resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
|
||||
|
/// whose result returns all the additional properties associated with the resource.
|
||||
|
/// </returns>
|
||||
|
public virtual ValueTask<ImmutableDictionary<string, JsonElement>> GetPropertiesAsync( |
||||
|
TResource resource, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
return Store.GetPropertiesAsync(resource, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Executes the specified query and returns all the corresponding elements.
|
||||
|
/// </summary>
|
||||
|
/// <param name="count">The number of results to return.</param>
|
||||
|
/// <param name="offset">The number of results to skip.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>All the elements returned when executing the specified query.</returns>
|
||||
|
public virtual IAsyncEnumerable<TResource> ListAsync( |
||||
|
int? count = null, int? offset = null, CancellationToken cancellationToken = default) |
||||
|
=> Store.ListAsync(count, offset, cancellationToken); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Executes the specified query and returns all the corresponding elements.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TResult">The result type.</typeparam>
|
||||
|
/// <param name="query">The query to execute.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>All the elements returned when executing the specified query.</returns>
|
||||
|
public virtual IAsyncEnumerable<TResult> ListAsync<TResult>( |
||||
|
Func<IQueryable<TResource>, IQueryable<TResult>> query, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(query); |
||||
|
|
||||
|
return ListAsync(static (resources, query) => query(resources), query, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Executes the specified query and returns all the corresponding elements.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TState">The state type.</typeparam>
|
||||
|
/// <typeparam name="TResult">The result type.</typeparam>
|
||||
|
/// <param name="query">The query to execute.</param>
|
||||
|
/// <param name="state">The optional state.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>All the elements returned when executing the specified query.</returns>
|
||||
|
public virtual IAsyncEnumerable<TResult> ListAsync<TState, TResult>( |
||||
|
Func<IQueryable<TResource>, TState, IQueryable<TResult>> query, |
||||
|
TState state, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(query); |
||||
|
|
||||
|
return Store.ListAsync(query, state, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Populates the resource using the specified descriptor.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="descriptor">The descriptor.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
|
||||
|
/// </returns>
|
||||
|
public virtual async ValueTask PopulateAsync(TResource resource, |
||||
|
OpenIddictResourceDescriptor descriptor, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
ArgumentNullException.ThrowIfNull(descriptor); |
||||
|
|
||||
|
await Store.SetDescriptionAsync(resource, descriptor.Description, cancellationToken); |
||||
|
await Store.SetDescriptionsAsync(resource, descriptor.Descriptions.ToImmutableDictionary(), cancellationToken); |
||||
|
await Store.SetDisplayNameAsync(resource, descriptor.DisplayName, cancellationToken); |
||||
|
await Store.SetDisplayNamesAsync(resource, descriptor.DisplayNames.ToImmutableDictionary(), cancellationToken); |
||||
|
await Store.SetNameAsync(resource, descriptor.Name, cancellationToken); |
||||
|
await Store.SetPropertiesAsync(resource, descriptor.Properties.ToImmutableDictionary(), cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Populates the specified descriptor using the properties exposed by the resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="descriptor">The descriptor.</param>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
|
||||
|
/// </returns>
|
||||
|
public virtual async ValueTask PopulateAsync( |
||||
|
OpenIddictResourceDescriptor descriptor, |
||||
|
TResource resource, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(descriptor); |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
descriptor.Description = await Store.GetDescriptionAsync(resource, cancellationToken); |
||||
|
descriptor.DisplayName = await Store.GetDisplayNameAsync(resource, cancellationToken); |
||||
|
descriptor.Name = await Store.GetNameAsync(resource, cancellationToken); |
||||
|
|
||||
|
descriptor.DisplayNames.Clear(); |
||||
|
foreach (var pair in await Store.GetDisplayNamesAsync(resource, cancellationToken)) |
||||
|
{ |
||||
|
descriptor.DisplayNames.Add(pair.Key, pair.Value); |
||||
|
} |
||||
|
|
||||
|
descriptor.Descriptions.Clear(); |
||||
|
foreach (var pair in await Store.GetDescriptionsAsync(resource, cancellationToken)) |
||||
|
{ |
||||
|
descriptor.Descriptions.Add(pair.Key, pair.Value); |
||||
|
} |
||||
|
|
||||
|
descriptor.Properties.Clear(); |
||||
|
foreach (var pair in await Store.GetPropertiesAsync(resource, cancellationToken)) |
||||
|
{ |
||||
|
descriptor.Properties.Add(pair.Key, pair.Value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Updates an existing resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource to update.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
|
||||
|
/// </returns>
|
||||
|
public virtual async ValueTask UpdateAsync(TResource resource, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
var results = await GetValidationResultsAsync(resource, cancellationToken); |
||||
|
if (results.Any(result => result != ValidationResult.Success)) |
||||
|
{ |
||||
|
var builder = new StringBuilder(); |
||||
|
builder.AppendLine(SR.GetResourceString(SR.ID0215)); |
||||
|
builder.AppendLine(); |
||||
|
|
||||
|
foreach (var result in results) |
||||
|
{ |
||||
|
builder.AppendLine(result.ErrorMessage); |
||||
|
} |
||||
|
|
||||
|
throw new ValidationException(builder.ToString(), results); |
||||
|
} |
||||
|
|
||||
|
if (!Options.CurrentValue.DisableEntityCaching) |
||||
|
{ |
||||
|
await Cache.RemoveAsync(resource, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
await Store.UpdateAsync(resource, cancellationToken); |
||||
|
|
||||
|
if (!Options.CurrentValue.DisableEntityCaching) |
||||
|
{ |
||||
|
await Cache.AddAsync(resource, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
async Task<ImmutableArray<ValidationResult>> GetValidationResultsAsync( |
||||
|
TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
var builder = ImmutableArray.CreateBuilder<ValidationResult>(); |
||||
|
|
||||
|
await foreach (var result in ValidateAsync(resource, cancellationToken)) |
||||
|
{ |
||||
|
builder.Add(result); |
||||
|
} |
||||
|
|
||||
|
return builder.ToImmutable(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Updates an existing resource.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource to update.</param>
|
||||
|
/// <param name="descriptor">The descriptor used to update the resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>
|
||||
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
|
||||
|
/// </returns>
|
||||
|
public virtual async ValueTask UpdateAsync(TResource resource, |
||||
|
OpenIddictResourceDescriptor descriptor, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
ArgumentNullException.ThrowIfNull(descriptor); |
||||
|
|
||||
|
await PopulateAsync(resource, descriptor, cancellationToken); |
||||
|
await UpdateAsync(resource, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Validates the resource to ensure it's in a consistent state.
|
||||
|
/// </summary>
|
||||
|
/// <param name="resource">The resource.</param>
|
||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
|
||||
|
/// <returns>The validation error encountered when validating the resource.</returns>
|
||||
|
public virtual IAsyncEnumerable<ValidationResult> ValidateAsync(TResource resource, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
return ExecuteAsync(cancellationToken); |
||||
|
|
||||
|
async IAsyncEnumerable<ValidationResult> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) |
||||
|
{ |
||||
|
// Ensure the name is not null or empty, does not contain a
|
||||
|
// space and is not already used for a different resource entity.
|
||||
|
var name = await Store.GetNameAsync(resource, cancellationToken); |
||||
|
if (string.IsNullOrEmpty(name)) |
||||
|
{ |
||||
|
yield return new ValidationResult(SR.GetResourceString(SR.ID2206)); |
||||
|
} |
||||
|
|
||||
|
// Note: resources MUST be absolute URIs and cannot contain a fragment.
|
||||
|
//
|
||||
|
// See https://datatracker.ietf.org/doc/html/rfc8693#section-2.1 for more information.
|
||||
|
else if (!Uri.TryCreate(name, UriKind.Absolute, out Uri? uri) || |
||||
|
OpenIddictHelpers.IsImplicitFileUri(uri) || !string.IsNullOrEmpty(uri.Fragment)) |
||||
|
{ |
||||
|
yield return new ValidationResult(SR.GetResourceString(SR.ID2207)); |
||||
|
} |
||||
|
|
||||
|
else |
||||
|
{ |
||||
|
// Note: depending on the database/table/query collation used by the store, a resource
|
||||
|
// whose name doesn't exactly match the specified value may be returned (e.g because
|
||||
|
// the casing is different). To avoid issues when the resource name is part of an index
|
||||
|
// using the same collation, an error is added even if the two names don't exactly match.
|
||||
|
var other = await Store.FindByNameAsync(name, cancellationToken); |
||||
|
if (other is not null && !string.Equals( |
||||
|
await Store.GetIdAsync(other, cancellationToken), |
||||
|
await Store.GetIdAsync(resource, cancellationToken), StringComparison.Ordinal)) |
||||
|
{ |
||||
|
yield return new ValidationResult(SR.GetResourceString(SR.ID2208)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
ValueTask<long> IOpenIddictResourceManager.CountAsync(CancellationToken cancellationToken) |
||||
|
=> CountAsync(cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
ValueTask<long> IOpenIddictResourceManager.CountAsync<TResult>(Func<IQueryable<object>, IQueryable<TResult>> query, CancellationToken cancellationToken) |
||||
|
=> CountAsync(query, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
ValueTask<long> IOpenIddictResourceManager.CountAsync<TState, TResult>(Func<IQueryable<object>, TState, IQueryable<TResult>> query, TState state, CancellationToken cancellationToken) |
||||
|
=> CountAsync(query, state, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
async ValueTask<object> IOpenIddictResourceManager.CreateAsync(OpenIddictResourceDescriptor descriptor, CancellationToken cancellationToken) |
||||
|
=> await CreateAsync(descriptor, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
ValueTask IOpenIddictResourceManager.CreateAsync(object resource, CancellationToken cancellationToken) |
||||
|
=> CreateAsync((TResource) resource, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
ValueTask IOpenIddictResourceManager.DeleteAsync(object resource, CancellationToken cancellationToken) |
||||
|
=> DeleteAsync((TResource) resource, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
async ValueTask<object?> IOpenIddictResourceManager.FindByIdAsync(string identifier, CancellationToken cancellationToken) |
||||
|
=> await FindByIdAsync(identifier, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
async ValueTask<object?> IOpenIddictResourceManager.FindByNameAsync(string name, CancellationToken cancellationToken) |
||||
|
=> await FindByNameAsync(name, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
IAsyncEnumerable<object> IOpenIddictResourceManager.FindByNamesAsync(ImmutableArray<string> names, CancellationToken cancellationToken) |
||||
|
=> FindByNamesAsync(names, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
ValueTask<TResult?> IOpenIddictResourceManager.GetAsync<TResult>(Func<IQueryable<object>, IQueryable<TResult>> query, CancellationToken cancellationToken) where TResult : default |
||||
|
=> GetAsync(query, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
ValueTask<TResult?> IOpenIddictResourceManager.GetAsync<TState, TResult>(Func<IQueryable<object>, TState, IQueryable<TResult>> query, TState state, CancellationToken cancellationToken) where TResult : default |
||||
|
=> GetAsync(query, state, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
ValueTask<string?> IOpenIddictResourceManager.GetDescriptionAsync(object resource, CancellationToken cancellationToken) |
||||
|
=> GetDescriptionAsync((TResource) resource, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
ValueTask<ImmutableDictionary<CultureInfo, string>> IOpenIddictResourceManager.GetDescriptionsAsync(object resource, CancellationToken cancellationToken) |
||||
|
=> GetDescriptionsAsync((TResource) resource, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
ValueTask<string?> IOpenIddictResourceManager.GetDisplayNameAsync(object resource, CancellationToken cancellationToken) |
||||
|
=> GetDisplayNameAsync((TResource) resource, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
ValueTask<ImmutableDictionary<CultureInfo, string>> IOpenIddictResourceManager.GetDisplayNamesAsync(object resource, CancellationToken cancellationToken) |
||||
|
=> GetDisplayNamesAsync((TResource) resource, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
ValueTask<string?> IOpenIddictResourceManager.GetIdAsync(object resource, CancellationToken cancellationToken) |
||||
|
=> GetIdAsync((TResource) resource, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
ValueTask<string?> IOpenIddictResourceManager.GetLocalizedDescriptionAsync(object resource, CancellationToken cancellationToken) |
||||
|
=> GetLocalizedDescriptionAsync((TResource) resource, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
ValueTask<string?> IOpenIddictResourceManager.GetLocalizedDescriptionAsync(object resource, CultureInfo culture, CancellationToken cancellationToken) |
||||
|
=> GetLocalizedDescriptionAsync((TResource) resource, culture, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
ValueTask<string?> IOpenIddictResourceManager.GetLocalizedDisplayNameAsync(object resource, CancellationToken cancellationToken) |
||||
|
=> GetLocalizedDisplayNameAsync((TResource) resource, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
ValueTask<string?> IOpenIddictResourceManager.GetLocalizedDisplayNameAsync(object resource, CultureInfo culture, CancellationToken cancellationToken) |
||||
|
=> GetLocalizedDisplayNameAsync((TResource) resource, culture, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
ValueTask<string?> IOpenIddictResourceManager.GetNameAsync(object resource, CancellationToken cancellationToken) |
||||
|
=> GetNameAsync((TResource) resource, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
ValueTask<ImmutableDictionary<string, JsonElement>> IOpenIddictResourceManager.GetPropertiesAsync(object resource, CancellationToken cancellationToken) |
||||
|
=> GetPropertiesAsync((TResource) resource, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
IAsyncEnumerable<object> IOpenIddictResourceManager.ListAsync(int? count, int? offset, CancellationToken cancellationToken) |
||||
|
=> ListAsync(count, offset, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
IAsyncEnumerable<TResult> IOpenIddictResourceManager.ListAsync<TResult>(Func<IQueryable<object>, IQueryable<TResult>> query, CancellationToken cancellationToken) |
||||
|
=> ListAsync(query, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
IAsyncEnumerable<TResult> IOpenIddictResourceManager.ListAsync<TState, TResult>(Func<IQueryable<object>, TState, IQueryable<TResult>> query, TState state, CancellationToken cancellationToken) |
||||
|
=> ListAsync(query, state, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
ValueTask IOpenIddictResourceManager.PopulateAsync(OpenIddictResourceDescriptor descriptor, object resource, CancellationToken cancellationToken) |
||||
|
=> PopulateAsync(descriptor, (TResource) resource, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
ValueTask IOpenIddictResourceManager.PopulateAsync(object resource, OpenIddictResourceDescriptor descriptor, CancellationToken cancellationToken) |
||||
|
=> PopulateAsync((TResource) resource, descriptor, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
ValueTask IOpenIddictResourceManager.UpdateAsync(object resource, CancellationToken cancellationToken) |
||||
|
=> UpdateAsync((TResource) resource, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
ValueTask IOpenIddictResourceManager.UpdateAsync(object resource, OpenIddictResourceDescriptor descriptor, CancellationToken cancellationToken) |
||||
|
=> UpdateAsync((TResource) resource, descriptor, cancellationToken); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
IAsyncEnumerable<ValidationResult> IOpenIddictResourceManager.ValidateAsync(object resource, CancellationToken cancellationToken) |
||||
|
=> ValidateAsync((TResource) resource, cancellationToken); |
||||
|
} |
||||
@ -0,0 +1,72 @@ |
|||||
|
/* |
||||
|
* 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.Diagnostics; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
|
||||
|
namespace OpenIddict.EntityFramework.Models; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents an OpenIddict resource.
|
||||
|
/// </summary>
|
||||
|
public class OpenIddictEntityFrameworkResource : OpenIddictEntityFrameworkResource<string> |
||||
|
{ |
||||
|
public OpenIddictEntityFrameworkResource() => Id = Guid.NewGuid().ToString(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents an OpenIddict resource.
|
||||
|
/// </summary>
|
||||
|
[DebuggerDisplay("Id = {Id.ToString(),nq} ; Name = {Name,nq}")] |
||||
|
public class OpenIddictEntityFrameworkResource<TKey> where TKey : notnull, IEquatable<TKey> |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets or sets the concurrency token.
|
||||
|
/// </summary>
|
||||
|
public virtual string? ConcurrencyToken { get; set; } = Guid.NewGuid().ToString(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the public description associated with the current resource.
|
||||
|
/// </summary>
|
||||
|
public virtual string? Description { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the localized public descriptions associated
|
||||
|
/// with the current resource, serialized as a JSON object.
|
||||
|
/// </summary>
|
||||
|
[StringSyntax(StringSyntaxAttribute.Json)] |
||||
|
public virtual string? Descriptions { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the display name associated with the current resource.
|
||||
|
/// </summary>
|
||||
|
public virtual string? DisplayName { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the localized display names
|
||||
|
/// associated with the current application,
|
||||
|
/// serialized as a JSON object.
|
||||
|
/// </summary>
|
||||
|
[StringSyntax(StringSyntaxAttribute.Json)] |
||||
|
public virtual string? DisplayNames { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the unique identifier associated with the current resource.
|
||||
|
/// </summary>
|
||||
|
public virtual TKey? Id { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the unique name associated with the current resource.
|
||||
|
/// </summary>
|
||||
|
public virtual string? Name { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the additional properties serialized as a JSON object,
|
||||
|
/// or <see langword="null"/> if no bag was associated with the current resource.
|
||||
|
/// </summary>
|
||||
|
[StringSyntax(StringSyntaxAttribute.Json)] |
||||
|
public virtual string? Properties { get; set; } |
||||
|
} |
||||
@ -0,0 +1,60 @@ |
|||||
|
/* |
||||
|
* 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.ComponentModel; |
||||
|
using System.ComponentModel.DataAnnotations.Schema; |
||||
|
using System.Data.Entity.Infrastructure.Annotations; |
||||
|
using System.Data.Entity.ModelConfiguration; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using System.Linq.Expressions; |
||||
|
using OpenIddict.EntityFramework.Models; |
||||
|
|
||||
|
namespace OpenIddict.EntityFramework; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines a relational mapping for the resource entity.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TResource">The type of the resource entity.</typeparam>
|
||||
|
/// <typeparam name="TKey">The type of the primary key.</typeparam>
|
||||
|
[EditorBrowsable(EditorBrowsableState.Never)] |
||||
|
public sealed class OpenIddictEntityFrameworkResourceConfiguration< |
||||
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResource, |
||||
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : EntityTypeConfiguration<TResource> |
||||
|
where TResource : OpenIddictEntityFrameworkResource<TKey> |
||||
|
where TKey : notnull, IEquatable<TKey> |
||||
|
{ |
||||
|
public OpenIddictEntityFrameworkResourceConfiguration() |
||||
|
{ |
||||
|
// Warning: optional foreign keys MUST NOT be added as CLR properties because
|
||||
|
// Entity Framework would throw an exception due to the TKey generic parameter
|
||||
|
// being non-nullable when using value types like short, int, long or Guid.
|
||||
|
|
||||
|
HasKey(static resource => resource.Id); |
||||
|
|
||||
|
Property(static resource => resource.ConcurrencyToken) |
||||
|
.HasMaxLength(50) |
||||
|
.IsConcurrencyToken(); |
||||
|
|
||||
|
if (typeof(TKey) == typeof(string)) |
||||
|
{ |
||||
|
var parameter = Expression.Parameter(typeof(TResource), "resource"); |
||||
|
var property = Expression.Property(parameter, |
||||
|
typeof(TResource).GetProperty(nameof(OpenIddictEntityFrameworkResource.Id))!); |
||||
|
var lambda = Expression.Lambda<Func<TResource, string>>(property, parameter); |
||||
|
|
||||
|
Property(lambda).HasMaxLength(100); |
||||
|
} |
||||
|
|
||||
|
Property(static resource => resource.Name) |
||||
|
.HasMaxLength(200) |
||||
|
.HasColumnAnnotation(IndexAnnotation.AnnotationName, new IndexAnnotation(new IndexAttribute |
||||
|
{ |
||||
|
IsUnique = true |
||||
|
})); |
||||
|
|
||||
|
ToTable("OpenIddictResources"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,635 @@ |
|||||
|
/* |
||||
|
* 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.ComponentModel; |
||||
|
using System.Data.Entity.Infrastructure; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using System.Globalization; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using System.Text; |
||||
|
using System.Text.Encodings.Web; |
||||
|
using System.Text.Json; |
||||
|
using Microsoft.Extensions.Caching.Memory; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using OpenIddict.EntityFramework.Models; |
||||
|
using static OpenIddict.Abstractions.OpenIddictExceptions; |
||||
|
|
||||
|
namespace OpenIddict.EntityFramework; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Provides methods allowing to manage the resources stored in a database.
|
||||
|
/// </summary>
|
||||
|
public class OpenIddictEntityFrameworkResourceStore : |
||||
|
OpenIddictEntityFrameworkResourceStore<OpenIddictEntityFrameworkResource, string> |
||||
|
{ |
||||
|
public OpenIddictEntityFrameworkResourceStore( |
||||
|
IMemoryCache cache, |
||||
|
IOpenIddictEntityFrameworkContext context, |
||||
|
IOptionsMonitor<OpenIddictEntityFrameworkOptions> options) |
||||
|
: base(cache, context, options) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Provides methods allowing to manage the resources stored in a database.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TResource">The type of the resource entity.</typeparam>
|
||||
|
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
|
||||
|
public class OpenIddictEntityFrameworkResourceStore< |
||||
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResource, |
||||
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : IOpenIddictResourceStore<TResource> |
||||
|
where TResource : OpenIddictEntityFrameworkResource<TKey> |
||||
|
where TKey : notnull, IEquatable<TKey> |
||||
|
{ |
||||
|
public OpenIddictEntityFrameworkResourceStore( |
||||
|
IMemoryCache cache, |
||||
|
IOpenIddictEntityFrameworkContext context, |
||||
|
IOptionsMonitor<OpenIddictEntityFrameworkOptions> options) |
||||
|
{ |
||||
|
Cache = cache ?? throw new ArgumentNullException(nameof(cache)); |
||||
|
Context = context ?? throw new ArgumentNullException(nameof(context)); |
||||
|
Options = options ?? throw new ArgumentNullException(nameof(options)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the memory cache associated with the current store.
|
||||
|
/// </summary>
|
||||
|
protected IMemoryCache Cache { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the database context associated with the current store.
|
||||
|
/// </summary>
|
||||
|
protected IOpenIddictEntityFrameworkContext Context { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the options associated with the current store.
|
||||
|
/// </summary>
|
||||
|
protected IOptionsMonitor<OpenIddictEntityFrameworkOptions> Options { get; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async ValueTask<long> CountAsync(CancellationToken cancellationToken) |
||||
|
{ |
||||
|
var context = await Context.GetDbContextAsync(cancellationToken); |
||||
|
|
||||
|
return await context.Set<TResource>().LongCountAsync(cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async ValueTask<long> CountAsync<TState, TResult>( |
||||
|
Func<IQueryable<TResource>, TState, IQueryable<TResult>> query, |
||||
|
TState state, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(query); |
||||
|
|
||||
|
var context = await Context.GetDbContextAsync(cancellationToken); |
||||
|
|
||||
|
return await query(context.Set<TResource>(), state).LongCountAsync(cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async ValueTask CreateAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
var context = await Context.GetDbContextAsync(cancellationToken); |
||||
|
|
||||
|
context.Set<TResource>().Add(resource); |
||||
|
|
||||
|
await context.SaveChangesAsync(cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async ValueTask DeleteAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
var context = await Context.GetDbContextAsync(cancellationToken); |
||||
|
|
||||
|
context.Set<TResource>().Remove(resource); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
await context.SaveChangesAsync(cancellationToken); |
||||
|
} |
||||
|
|
||||
|
catch (DbUpdateConcurrencyException exception) |
||||
|
{ |
||||
|
// Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing.
|
||||
|
context.Entry(resource).State = EntityState.Unchanged; |
||||
|
|
||||
|
throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async ValueTask<TResource?> FindByIdAsync(string identifier, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentException.ThrowIfNullOrEmpty(identifier); |
||||
|
|
||||
|
var context = await Context.GetDbContextAsync(cancellationToken); |
||||
|
var key = ConvertIdentifierFromString(identifier); |
||||
|
|
||||
|
return GetTrackedEntity() is TResource resource ? resource : await QueryAsync(); |
||||
|
|
||||
|
TResource? GetTrackedEntity() => |
||||
|
(from entry in context.ChangeTracker.Entries<TResource>() |
||||
|
where entry.Entity.Id is TKey identifier && identifier.Equals(key) |
||||
|
select entry.Entity).FirstOrDefault(); |
||||
|
|
||||
|
Task<TResource?> QueryAsync() => |
||||
|
(from resource in context.Set<TResource>() |
||||
|
where resource.Id!.Equals(key) |
||||
|
select resource).FirstOrDefaultAsync(cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async ValueTask<TResource?> FindByNameAsync(string name, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentException.ThrowIfNullOrEmpty(name); |
||||
|
|
||||
|
var context = await Context.GetDbContextAsync(cancellationToken); |
||||
|
|
||||
|
return GetTrackedEntity() is TResource resource ? resource : await QueryAsync(); |
||||
|
|
||||
|
TResource? GetTrackedEntity() => |
||||
|
(from entry in context.ChangeTracker.Entries<TResource>() |
||||
|
where string.Equals(entry.Entity.Name, name, StringComparison.Ordinal) |
||||
|
select entry.Entity).FirstOrDefault(); |
||||
|
|
||||
|
Task<TResource?> QueryAsync() => |
||||
|
(from resource in context.Set<TResource>() |
||||
|
where resource.Name == name |
||||
|
select resource).FirstOrDefaultAsync(cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual IAsyncEnumerable<TResource> FindByNamesAsync(ImmutableArray<string> names, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
if (names.Any(string.IsNullOrEmpty)) |
||||
|
{ |
||||
|
throw new ArgumentException(SR.GetResourceString(SR.ID0203), nameof(names)); |
||||
|
} |
||||
|
|
||||
|
return ExecuteAsync(cancellationToken); |
||||
|
|
||||
|
async IAsyncEnumerable<TResource> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) |
||||
|
{ |
||||
|
var context = await Context.GetDbContextAsync(cancellationToken); |
||||
|
|
||||
|
// Note: Enumerable.Contains() is deliberately used without the extension method syntax to ensure
|
||||
|
// ImmutableArray.Contains() (which is not fully supported by Entity Framework 6.x) is not used instead.
|
||||
|
await foreach (var resource in |
||||
|
(from resource in context.Set<TResource>() |
||||
|
where Enumerable.Contains(names, resource.Name) |
||||
|
select resource).AsAsyncEnumerable(cancellationToken)) |
||||
|
{ |
||||
|
yield return resource; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async ValueTask<TResult?> GetAsync<TState, TResult>( |
||||
|
Func<IQueryable<TResource>, TState, IQueryable<TResult>> query, |
||||
|
TState state, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(query); |
||||
|
|
||||
|
var context = await Context.GetDbContextAsync(cancellationToken); |
||||
|
|
||||
|
return await query(context.Set<TResource>(), state).FirstOrDefaultAsync(cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask<string?> GetDescriptionAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
return new(resource.Description); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask<ImmutableDictionary<CultureInfo, string>> GetDescriptionsAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
if (string.IsNullOrEmpty(resource.Descriptions)) |
||||
|
{ |
||||
|
return new(ImmutableDictionary.Create<CultureInfo, string>()); |
||||
|
} |
||||
|
|
||||
|
// Note: parsing the stringified descriptions is an expensive operation.
|
||||
|
// To mitigate that, the resulting object is stored in the memory cache.
|
||||
|
var key = string.Concat("20e1ab51-b505-40b0-9a10-d0596b9f2143", "\x1e", resource.Descriptions); |
||||
|
var descriptions = Cache.GetOrCreate(key, entry => |
||||
|
{ |
||||
|
entry.SetPriority(CacheItemPriority.High) |
||||
|
.SetSlidingExpiration(TimeSpan.FromMinutes(1)); |
||||
|
|
||||
|
using var document = JsonDocument.Parse(resource.Descriptions); |
||||
|
var builder = ImmutableDictionary.CreateBuilder<CultureInfo, string>(); |
||||
|
|
||||
|
foreach (var property in document.RootElement.EnumerateObject()) |
||||
|
{ |
||||
|
var value = property.Value.GetString(); |
||||
|
if (string.IsNullOrEmpty(value)) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
builder[CultureInfo.GetCultureInfo(property.Name)] = value; |
||||
|
} |
||||
|
|
||||
|
return builder.ToImmutable(); |
||||
|
})!; |
||||
|
|
||||
|
return new(descriptions); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask<string?> GetDisplayNameAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
return new(resource.DisplayName); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask<ImmutableDictionary<CultureInfo, string>> GetDisplayNamesAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
if (string.IsNullOrEmpty(resource.DisplayNames)) |
||||
|
{ |
||||
|
return new(ImmutableDictionary.Create<CultureInfo, string>()); |
||||
|
} |
||||
|
|
||||
|
// Note: parsing the stringified display names is an expensive operation.
|
||||
|
// To mitigate that, the resulting object is stored in the memory cache.
|
||||
|
var key = string.Concat("65c3ea08-ded7-488f-b001-5098de04172b", "\x1e", resource.DisplayNames); |
||||
|
var names = Cache.GetOrCreate(key, entry => |
||||
|
{ |
||||
|
entry.SetPriority(CacheItemPriority.High) |
||||
|
.SetSlidingExpiration(TimeSpan.FromMinutes(1)); |
||||
|
|
||||
|
using var document = JsonDocument.Parse(resource.DisplayNames); |
||||
|
var builder = ImmutableDictionary.CreateBuilder<CultureInfo, string>(); |
||||
|
|
||||
|
foreach (var property in document.RootElement.EnumerateObject()) |
||||
|
{ |
||||
|
var value = property.Value.GetString(); |
||||
|
if (string.IsNullOrEmpty(value)) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
builder[CultureInfo.GetCultureInfo(property.Name)] = value; |
||||
|
} |
||||
|
|
||||
|
return builder.ToImmutable(); |
||||
|
})!; |
||||
|
|
||||
|
return new(names); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask<string?> GetIdAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
return new(ConvertIdentifierToString(resource.Id)); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask<string?> GetNameAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
return new(resource.Name); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask<ImmutableDictionary<string, JsonElement>> GetPropertiesAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
if (string.IsNullOrEmpty(resource.Properties)) |
||||
|
{ |
||||
|
return new(ImmutableDictionary.Create<string, JsonElement>()); |
||||
|
} |
||||
|
|
||||
|
// Note: parsing the stringified properties is an expensive operation.
|
||||
|
// To mitigate that, the resulting object is stored in the memory cache.
|
||||
|
var key = string.Concat("1f414494-e5aa-4cad-9c5f-4f98688e3623", "\x1e", resource.Properties); |
||||
|
var properties = Cache.GetOrCreate(key, entry => |
||||
|
{ |
||||
|
entry.SetPriority(CacheItemPriority.High) |
||||
|
.SetSlidingExpiration(TimeSpan.FromMinutes(1)); |
||||
|
|
||||
|
using var document = JsonDocument.Parse(resource.Properties); |
||||
|
var builder = ImmutableDictionary.CreateBuilder<string, JsonElement>(); |
||||
|
|
||||
|
foreach (var property in document.RootElement.EnumerateObject()) |
||||
|
{ |
||||
|
builder[property.Name] = property.Value.Clone(); |
||||
|
} |
||||
|
|
||||
|
return builder.ToImmutable(); |
||||
|
})!; |
||||
|
|
||||
|
return new(properties); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask<TResource> InstantiateAsync(CancellationToken cancellationToken) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
return new(Activator.CreateInstance<TResource>()); |
||||
|
} |
||||
|
|
||||
|
catch (MemberAccessException exception) |
||||
|
{ |
||||
|
return new(Task.FromException<TResource>( |
||||
|
new InvalidOperationException(SR.GetResourceString(SR.ID0240), exception))); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async IAsyncEnumerable<TResource> ListAsync(int? count, int? offset, |
||||
|
[EnumeratorCancellation] CancellationToken cancellationToken) |
||||
|
{ |
||||
|
var context = await Context.GetDbContextAsync(cancellationToken); |
||||
|
|
||||
|
IQueryable<TResource> query = context.Set<TResource>().OrderBy(resource => resource.Id!); |
||||
|
|
||||
|
if (offset.HasValue) |
||||
|
{ |
||||
|
query = query.Skip(offset.Value); |
||||
|
} |
||||
|
|
||||
|
if (count.HasValue) |
||||
|
{ |
||||
|
query = query.Take(count.Value); |
||||
|
} |
||||
|
|
||||
|
await foreach (var resource in query.AsAsyncEnumerable(cancellationToken)) |
||||
|
{ |
||||
|
yield return resource; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual IAsyncEnumerable<TResult> ListAsync<TState, TResult>( |
||||
|
Func<IQueryable<TResource>, TState, IQueryable<TResult>> query, |
||||
|
TState state, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(query); |
||||
|
|
||||
|
return ExecuteAsync(cancellationToken); |
||||
|
|
||||
|
async IAsyncEnumerable<TResult> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) |
||||
|
{ |
||||
|
var context = await Context.GetDbContextAsync(cancellationToken); |
||||
|
|
||||
|
await foreach (var resource in query(context.Set<TResource>(), state).AsAsyncEnumerable(cancellationToken)) |
||||
|
{ |
||||
|
yield return resource; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask SetDescriptionAsync(TResource resource, string? description, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
resource.Description = description; |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask SetDescriptionsAsync(TResource resource, |
||||
|
ImmutableDictionary<CultureInfo, string> descriptions, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
if (descriptions is not { Count: > 0 }) |
||||
|
{ |
||||
|
resource.Descriptions = null; |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
using var stream = new MemoryStream(); |
||||
|
using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions |
||||
|
{ |
||||
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, |
||||
|
Indented = false |
||||
|
}); |
||||
|
|
||||
|
writer.WriteStartObject(); |
||||
|
|
||||
|
foreach (var description in descriptions) |
||||
|
{ |
||||
|
writer.WritePropertyName(description.Key.Name); |
||||
|
writer.WriteStringValue(description.Value); |
||||
|
} |
||||
|
|
||||
|
writer.WriteEndObject(); |
||||
|
writer.Flush(); |
||||
|
|
||||
|
resource.Descriptions = Encoding.UTF8.GetString(stream.ToArray()); |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask SetDisplayNameAsync(TResource resource, string? name, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
resource.DisplayName = name; |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask SetDisplayNamesAsync(TResource resource, |
||||
|
ImmutableDictionary<CultureInfo, string> names, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
if (names is not { Count: > 0 }) |
||||
|
{ |
||||
|
resource.DisplayNames = null; |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
using var stream = new MemoryStream(); |
||||
|
using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions |
||||
|
{ |
||||
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, |
||||
|
Indented = false |
||||
|
}); |
||||
|
|
||||
|
writer.WriteStartObject(); |
||||
|
|
||||
|
foreach (var name in names) |
||||
|
{ |
||||
|
writer.WritePropertyName(name.Key.Name); |
||||
|
writer.WriteStringValue(name.Value); |
||||
|
} |
||||
|
|
||||
|
writer.WriteEndObject(); |
||||
|
writer.Flush(); |
||||
|
|
||||
|
resource.DisplayNames = Encoding.UTF8.GetString(stream.ToArray()); |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask SetNameAsync(TResource resource, string? name, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
resource.Name = name; |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask SetPropertiesAsync(TResource resource, |
||||
|
ImmutableDictionary<string, JsonElement> properties, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
if (properties is not { Count: > 0 }) |
||||
|
{ |
||||
|
resource.Properties = null; |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
using var stream = new MemoryStream(); |
||||
|
using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions |
||||
|
{ |
||||
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, |
||||
|
Indented = false |
||||
|
}); |
||||
|
|
||||
|
writer.WriteStartObject(); |
||||
|
|
||||
|
foreach (var property in properties) |
||||
|
{ |
||||
|
writer.WritePropertyName(property.Key); |
||||
|
property.Value.WriteTo(writer); |
||||
|
} |
||||
|
|
||||
|
writer.WriteEndObject(); |
||||
|
writer.Flush(); |
||||
|
|
||||
|
resource.Properties = Encoding.UTF8.GetString(stream.ToArray()); |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async ValueTask UpdateAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
var context = await Context.GetDbContextAsync(cancellationToken); |
||||
|
|
||||
|
context.Set<TResource>().Attach(resource); |
||||
|
|
||||
|
// Generate a new concurrency token and attach it
|
||||
|
// to the resource before persisting the changes.
|
||||
|
resource.ConcurrencyToken = Guid.NewGuid().ToString(); |
||||
|
|
||||
|
context.Entry(resource).State = EntityState.Modified; |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
await context.SaveChangesAsync(cancellationToken); |
||||
|
} |
||||
|
|
||||
|
catch (DbUpdateConcurrencyException exception) |
||||
|
{ |
||||
|
// Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing.
|
||||
|
context.Entry(resource).State = EntityState.Unchanged; |
||||
|
|
||||
|
throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Converts the provided identifier to a strongly typed key object.
|
||||
|
/// </summary>
|
||||
|
/// <param name="identifier">The identifier to convert.</param>
|
||||
|
/// <returns>An instance of <typeparamref name="TKey"/> representing the provided identifier.</returns>
|
||||
|
public virtual TKey? ConvertIdentifierFromString(string? identifier) |
||||
|
{ |
||||
|
if (string.IsNullOrEmpty(identifier)) |
||||
|
{ |
||||
|
return default; |
||||
|
} |
||||
|
|
||||
|
// Optimization: if the key is a string, directly return it as-is.
|
||||
|
if (typeof(TKey) == typeof(string)) |
||||
|
{ |
||||
|
return (TKey?) (object?) identifier; |
||||
|
} |
||||
|
|
||||
|
else |
||||
|
{ |
||||
|
var converter = |
||||
|
#if NET
|
||||
|
TypeDescriptor.GetConverterFromRegisteredType(typeof(TKey)); |
||||
|
#else
|
||||
|
TypeDescriptor.GetConverter(typeof(TKey)); |
||||
|
#endif
|
||||
|
|
||||
|
return (TKey?) converter.ConvertFromInvariantString(identifier); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Converts the provided identifier to its string representation.
|
||||
|
/// </summary>
|
||||
|
/// <param name="identifier">The identifier to convert.</param>
|
||||
|
/// <returns>A <see cref="string"/> representation of the provided identifier.</returns>
|
||||
|
public virtual string? ConvertIdentifierToString(TKey? identifier) |
||||
|
{ |
||||
|
if (Equals(identifier, default(TKey))) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
// Optimization: if the key is a string, directly return it as-is.
|
||||
|
if (identifier is string value) |
||||
|
{ |
||||
|
return value; |
||||
|
} |
||||
|
|
||||
|
else |
||||
|
{ |
||||
|
var converter = |
||||
|
#if NET
|
||||
|
TypeDescriptor.GetConverterFromRegisteredType(typeof(TKey)); |
||||
|
#else
|
||||
|
TypeDescriptor.GetConverter(typeof(TKey)); |
||||
|
#endif
|
||||
|
|
||||
|
return converter.ConvertToInvariantString(identifier); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,72 @@ |
|||||
|
/* |
||||
|
* 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.Diagnostics; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
|
||||
|
namespace OpenIddict.EntityFrameworkCore.Models; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents an OpenIddict resource.
|
||||
|
/// </summary>
|
||||
|
public class OpenIddictEntityFrameworkCoreResource : OpenIddictEntityFrameworkCoreResource<string> |
||||
|
{ |
||||
|
public OpenIddictEntityFrameworkCoreResource() => Id = Guid.NewGuid().ToString(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents an OpenIddict resource.
|
||||
|
/// </summary>
|
||||
|
[DebuggerDisplay("Id = {Id.ToString(),nq} ; Name = {Name,nq}")] |
||||
|
public class OpenIddictEntityFrameworkCoreResource<TKey> where TKey : notnull, IEquatable<TKey> |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets or sets the concurrency token.
|
||||
|
/// </summary>
|
||||
|
public virtual string? ConcurrencyToken { get; set; } = Guid.NewGuid().ToString(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the public description associated with the current resource.
|
||||
|
/// </summary>
|
||||
|
public virtual string? Description { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the localized public descriptions associated
|
||||
|
/// with the current resource, serialized as a JSON object.
|
||||
|
/// </summary>
|
||||
|
[StringSyntax(StringSyntaxAttribute.Json)] |
||||
|
public virtual string? Descriptions { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the display name associated with the current resource.
|
||||
|
/// </summary>
|
||||
|
public virtual string? DisplayName { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the localized display names
|
||||
|
/// associated with the current application,
|
||||
|
/// serialized as a JSON object.
|
||||
|
/// </summary>
|
||||
|
[StringSyntax(StringSyntaxAttribute.Json)] |
||||
|
public virtual string? DisplayNames { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the unique identifier associated with the current resource.
|
||||
|
/// </summary>
|
||||
|
public virtual TKey? Id { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the unique name associated with the current resource.
|
||||
|
/// </summary>
|
||||
|
public virtual string? Name { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the additional properties serialized as a JSON object,
|
||||
|
/// or <see langword="null"/> if no bag was associated with the current resource.
|
||||
|
/// </summary>
|
||||
|
[StringSyntax(StringSyntaxAttribute.Json)] |
||||
|
public virtual string? Properties { get; set; } |
||||
|
} |
||||
@ -0,0 +1,60 @@ |
|||||
|
/* |
||||
|
* 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.ComponentModel; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders; |
||||
|
using OpenIddict.EntityFrameworkCore.Models; |
||||
|
|
||||
|
namespace OpenIddict.EntityFrameworkCore; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines a relational mapping for the resource entity.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TResource">The type of the resource entity.</typeparam>
|
||||
|
/// <typeparam name="TKey">The type of the primary key.</typeparam>
|
||||
|
[EditorBrowsable(EditorBrowsableState.Never)] |
||||
|
public sealed class OpenIddictEntityFrameworkCoreResourceConfiguration< |
||||
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResource, |
||||
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : IEntityTypeConfiguration<TResource> |
||||
|
where TResource : OpenIddictEntityFrameworkCoreResource<TKey> |
||||
|
where TKey : notnull, IEquatable<TKey> |
||||
|
{ |
||||
|
public void Configure(EntityTypeBuilder<TResource> builder) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(builder); |
||||
|
|
||||
|
// Warning: optional foreign keys MUST NOT be added as CLR properties because
|
||||
|
// Entity Framework would throw an exception due to the TKey generic parameter
|
||||
|
// being non-nullable when using value types like short, int, long or Guid.
|
||||
|
|
||||
|
builder.HasKey(static resource => resource.Id); |
||||
|
|
||||
|
// Warning: the non-generic overlord is deliberately used to work around
|
||||
|
// a breaking change introduced in Entity Framework Core 3.x (where a
|
||||
|
// generic entity type builder is now returned by the HasIndex() method).
|
||||
|
builder.HasIndex(nameof(OpenIddictEntityFrameworkCoreResource.Name)) |
||||
|
.IsUnique(); |
||||
|
|
||||
|
builder.Property(static resource => resource.ConcurrencyToken) |
||||
|
.HasMaxLength(50) |
||||
|
.IsConcurrencyToken(); |
||||
|
|
||||
|
builder.Property(static resource => resource.Id) |
||||
|
.ValueGeneratedOnAdd(); |
||||
|
|
||||
|
if (typeof(TKey) == typeof(string)) |
||||
|
{ |
||||
|
builder.Property(static resource => resource.Id) |
||||
|
.HasMaxLength(100); |
||||
|
} |
||||
|
|
||||
|
builder.Property(static resource => resource.Name) |
||||
|
.HasMaxLength(200); |
||||
|
|
||||
|
builder.ToTable("OpenIddictResources"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,649 @@ |
|||||
|
/* |
||||
|
* 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.ComponentModel; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using System.Globalization; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using System.Text; |
||||
|
using System.Text.Encodings.Web; |
||||
|
using System.Text.Json; |
||||
|
using Microsoft.Extensions.Caching.Memory; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using OpenIddict.EntityFrameworkCore.Models; |
||||
|
using static OpenIddict.Abstractions.OpenIddictExceptions; |
||||
|
|
||||
|
namespace OpenIddict.EntityFrameworkCore; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Provides methods allowing to manage the resources stored in a database.
|
||||
|
/// </summary>
|
||||
|
public class OpenIddictEntityFrameworkCoreResourceStore : OpenIddictEntityFrameworkCoreResourceStore<OpenIddictEntityFrameworkCoreResource, string> |
||||
|
{ |
||||
|
public OpenIddictEntityFrameworkCoreResourceStore( |
||||
|
IMemoryCache cache, |
||||
|
IOpenIddictEntityFrameworkCoreContext context, |
||||
|
IOptionsMonitor<OpenIddictEntityFrameworkCoreOptions> options) |
||||
|
: base(cache, context, options) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Provides methods allowing to manage the resources stored in a database.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
|
||||
|
public class OpenIddictEntityFrameworkCoreResourceStore< |
||||
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : OpenIddictEntityFrameworkCoreResourceStore<OpenIddictEntityFrameworkCoreResource<TKey>, TKey> |
||||
|
where TKey : notnull, IEquatable<TKey> |
||||
|
{ |
||||
|
public OpenIddictEntityFrameworkCoreResourceStore( |
||||
|
IMemoryCache cache, |
||||
|
IOpenIddictEntityFrameworkCoreContext context, |
||||
|
IOptionsMonitor<OpenIddictEntityFrameworkCoreOptions> options) |
||||
|
: base(cache, context, options) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Provides methods allowing to manage the resources stored in a database.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TResource">The type of the resource entity.</typeparam>
|
||||
|
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
|
||||
|
public class OpenIddictEntityFrameworkCoreResourceStore< |
||||
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResource, |
||||
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : IOpenIddictResourceStore<TResource> |
||||
|
where TResource : OpenIddictEntityFrameworkCoreResource<TKey> |
||||
|
where TKey : notnull, IEquatable<TKey> |
||||
|
{ |
||||
|
public OpenIddictEntityFrameworkCoreResourceStore( |
||||
|
IMemoryCache cache, |
||||
|
IOpenIddictEntityFrameworkCoreContext context, |
||||
|
IOptionsMonitor<OpenIddictEntityFrameworkCoreOptions> options) |
||||
|
{ |
||||
|
Cache = cache ?? throw new ArgumentNullException(nameof(cache)); |
||||
|
Context = context ?? throw new ArgumentNullException(nameof(context)); |
||||
|
Options = options ?? throw new ArgumentNullException(nameof(options)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the memory cache associated with the current store.
|
||||
|
/// </summary>
|
||||
|
protected IMemoryCache Cache { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the database context associated with the current store.
|
||||
|
/// </summary>
|
||||
|
protected IOpenIddictEntityFrameworkCoreContext Context { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the options associated with the current store.
|
||||
|
/// </summary>
|
||||
|
protected IOptionsMonitor<OpenIddictEntityFrameworkCoreOptions> Options { get; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async ValueTask<long> CountAsync(CancellationToken cancellationToken) |
||||
|
{ |
||||
|
var context = await Context.GetDbContextAsync(cancellationToken); |
||||
|
|
||||
|
return await context.Set<TResource>().LongCountAsync(cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async ValueTask<long> CountAsync<TState, TResult>( |
||||
|
Func<IQueryable<TResource>, TState, IQueryable<TResult>> query, |
||||
|
TState state, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(query); |
||||
|
|
||||
|
var context = await Context.GetDbContextAsync(cancellationToken); |
||||
|
|
||||
|
return await query(context.Set<TResource>(), state).LongCountAsync(cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async ValueTask CreateAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
var context = await Context.GetDbContextAsync(cancellationToken); |
||||
|
|
||||
|
context.Add(resource); |
||||
|
|
||||
|
await context.SaveChangesAsync(cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async ValueTask DeleteAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
var context = await Context.GetDbContextAsync(cancellationToken); |
||||
|
|
||||
|
context.Remove(resource); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
await context.SaveChangesAsync(cancellationToken); |
||||
|
} |
||||
|
|
||||
|
catch (DbUpdateConcurrencyException exception) |
||||
|
{ |
||||
|
// Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing.
|
||||
|
context.Entry(resource).State = EntityState.Unchanged; |
||||
|
|
||||
|
throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async ValueTask<TResource?> FindByIdAsync(string identifier, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentException.ThrowIfNullOrEmpty(identifier); |
||||
|
|
||||
|
var context = await Context.GetDbContextAsync(cancellationToken); |
||||
|
var key = ConvertIdentifierFromString(identifier); |
||||
|
|
||||
|
return GetTrackedEntity() is TResource resource ? resource : await QueryAsync(); |
||||
|
|
||||
|
TResource? GetTrackedEntity() => |
||||
|
(from entry in context.ChangeTracker.Entries<TResource>() |
||||
|
where entry.Entity.Id is TKey identifier && identifier.Equals(key) |
||||
|
select entry.Entity).FirstOrDefault(); |
||||
|
|
||||
|
Task<TResource?> QueryAsync() => |
||||
|
(from resource in context.Set<TResource>().AsTracking() |
||||
|
where resource.Id!.Equals(key) |
||||
|
select resource).FirstOrDefaultAsync(cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async ValueTask<TResource?> FindByNameAsync(string name, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentException.ThrowIfNullOrEmpty(name); |
||||
|
|
||||
|
var context = await Context.GetDbContextAsync(cancellationToken); |
||||
|
|
||||
|
return GetTrackedEntity() is TResource resource ? resource : await QueryAsync(); |
||||
|
|
||||
|
TResource? GetTrackedEntity() => |
||||
|
(from entry in context.ChangeTracker.Entries<TResource>() |
||||
|
where string.Equals(entry.Entity.Name, name, StringComparison.Ordinal) |
||||
|
select entry.Entity).FirstOrDefault(); |
||||
|
|
||||
|
Task<TResource?> QueryAsync() => |
||||
|
(from resource in context.Set<TResource>().AsTracking() |
||||
|
where resource.Name == name |
||||
|
select resource).FirstOrDefaultAsync(cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual IAsyncEnumerable<TResource> FindByNamesAsync(ImmutableArray<string> names, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
if (names.Any(string.IsNullOrEmpty)) |
||||
|
{ |
||||
|
throw new ArgumentException(SR.GetResourceString(SR.ID0203), nameof(names)); |
||||
|
} |
||||
|
|
||||
|
return ExecuteAsync(cancellationToken); |
||||
|
|
||||
|
async IAsyncEnumerable<TResource> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) |
||||
|
{ |
||||
|
var context = await Context.GetDbContextAsync(cancellationToken); |
||||
|
|
||||
|
// Note: Enumerable.Contains() is deliberately used without the extension method syntax to ensure
|
||||
|
// ImmutableArray.Contains() (which is not fully supported by Entity Framework Core) is not used instead.
|
||||
|
await foreach (var resource in (from resource in context.Set<TResource>().AsTracking() |
||||
|
where Enumerable.Contains(names, resource.Name) |
||||
|
select resource).AsAsyncEnumerable().WithCancellation(cancellationToken)) |
||||
|
{ |
||||
|
yield return resource; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async ValueTask<TResult?> GetAsync<TState, TResult>( |
||||
|
Func<IQueryable<TResource>, TState, IQueryable<TResult>> query, |
||||
|
TState state, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(query); |
||||
|
|
||||
|
var context = await Context.GetDbContextAsync(cancellationToken); |
||||
|
|
||||
|
return await query(context.Set<TResource>().AsTracking(), state).FirstOrDefaultAsync(cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask<string?> GetDescriptionAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
return new(resource.Description); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask<ImmutableDictionary<CultureInfo, string>> GetDescriptionsAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
if (string.IsNullOrEmpty(resource.Descriptions)) |
||||
|
{ |
||||
|
return new(ImmutableDictionary.Create<CultureInfo, string>()); |
||||
|
} |
||||
|
|
||||
|
// Note: parsing the stringified descriptions is an expensive operation.
|
||||
|
// To mitigate that, the resulting object is stored in the memory cache.
|
||||
|
var key = string.Concat("20e1ab51-b505-40b0-9a10-d0596b9f2143", "\x1e", resource.Descriptions); |
||||
|
var descriptions = Cache.GetOrCreate(key, entry => |
||||
|
{ |
||||
|
entry.SetPriority(CacheItemPriority.High) |
||||
|
.SetSlidingExpiration(TimeSpan.FromMinutes(1)); |
||||
|
|
||||
|
using var document = JsonDocument.Parse(resource.Descriptions); |
||||
|
var builder = ImmutableDictionary.CreateBuilder<CultureInfo, string>(); |
||||
|
|
||||
|
foreach (var property in document.RootElement.EnumerateObject()) |
||||
|
{ |
||||
|
var value = property.Value.GetString(); |
||||
|
if (string.IsNullOrEmpty(value)) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
builder[CultureInfo.GetCultureInfo(property.Name)] = value; |
||||
|
} |
||||
|
|
||||
|
return builder.ToImmutable(); |
||||
|
})!; |
||||
|
|
||||
|
return new(descriptions); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask<string?> GetDisplayNameAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
return new(resource.DisplayName); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask<ImmutableDictionary<CultureInfo, string>> GetDisplayNamesAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
if (string.IsNullOrEmpty(resource.DisplayNames)) |
||||
|
{ |
||||
|
return new(ImmutableDictionary.Create<CultureInfo, string>()); |
||||
|
} |
||||
|
|
||||
|
// Note: parsing the stringified display names is an expensive operation.
|
||||
|
// To mitigate that, the resulting object is stored in the memory cache.
|
||||
|
var key = string.Concat("65c3ea08-ded7-488f-b001-5098de04172b", "\x1e", resource.DisplayNames); |
||||
|
var names = Cache.GetOrCreate(key, entry => |
||||
|
{ |
||||
|
entry.SetPriority(CacheItemPriority.High) |
||||
|
.SetSlidingExpiration(TimeSpan.FromMinutes(1)); |
||||
|
|
||||
|
using var document = JsonDocument.Parse(resource.DisplayNames); |
||||
|
var builder = ImmutableDictionary.CreateBuilder<CultureInfo, string>(); |
||||
|
|
||||
|
foreach (var property in document.RootElement.EnumerateObject()) |
||||
|
{ |
||||
|
var value = property.Value.GetString(); |
||||
|
if (string.IsNullOrEmpty(value)) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
builder[CultureInfo.GetCultureInfo(property.Name)] = value; |
||||
|
} |
||||
|
|
||||
|
return builder.ToImmutable(); |
||||
|
})!; |
||||
|
|
||||
|
return new(names); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask<string?> GetIdAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
return new(ConvertIdentifierToString(resource.Id)); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask<string?> GetNameAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
return new(resource.Name); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask<ImmutableDictionary<string, JsonElement>> GetPropertiesAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
if (string.IsNullOrEmpty(resource.Properties)) |
||||
|
{ |
||||
|
return new(ImmutableDictionary.Create<string, JsonElement>()); |
||||
|
} |
||||
|
|
||||
|
// Note: parsing the stringified properties is an expensive operation.
|
||||
|
// To mitigate that, the resulting object is stored in the memory cache.
|
||||
|
var key = string.Concat("1f414494-e5aa-4cad-9c5f-4f98688e3623", "\x1e", resource.Properties); |
||||
|
var properties = Cache.GetOrCreate(key, entry => |
||||
|
{ |
||||
|
entry.SetPriority(CacheItemPriority.High) |
||||
|
.SetSlidingExpiration(TimeSpan.FromMinutes(1)); |
||||
|
|
||||
|
using var document = JsonDocument.Parse(resource.Properties); |
||||
|
var builder = ImmutableDictionary.CreateBuilder<string, JsonElement>(); |
||||
|
|
||||
|
foreach (var property in document.RootElement.EnumerateObject()) |
||||
|
{ |
||||
|
builder[property.Name] = property.Value.Clone(); |
||||
|
} |
||||
|
|
||||
|
return builder.ToImmutable(); |
||||
|
})!; |
||||
|
|
||||
|
return new(properties); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask<TResource> InstantiateAsync(CancellationToken cancellationToken) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
return new(Activator.CreateInstance<TResource>()); |
||||
|
} |
||||
|
|
||||
|
catch (MemberAccessException exception) |
||||
|
{ |
||||
|
return new(Task.FromException<TResource>( |
||||
|
new InvalidOperationException(SR.GetResourceString(SR.ID0240), exception))); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async IAsyncEnumerable<TResource> ListAsync(int? count, int? offset, |
||||
|
[EnumeratorCancellation] CancellationToken cancellationToken) |
||||
|
{ |
||||
|
var context = await Context.GetDbContextAsync(cancellationToken); |
||||
|
|
||||
|
var query = context.Set<TResource>().OrderBy(resource => resource.Id!).AsTracking(); |
||||
|
|
||||
|
if (offset.HasValue) |
||||
|
{ |
||||
|
query = query.Skip(offset.Value); |
||||
|
} |
||||
|
|
||||
|
if (count.HasValue) |
||||
|
{ |
||||
|
query = query.Take(count.Value); |
||||
|
} |
||||
|
|
||||
|
await foreach (var resource in query.AsAsyncEnumerable().WithCancellation(cancellationToken)) |
||||
|
{ |
||||
|
yield return resource; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual IAsyncEnumerable<TResult> ListAsync<TState, TResult>( |
||||
|
Func<IQueryable<TResource>, TState, IQueryable<TResult>> query, |
||||
|
TState state, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(query); |
||||
|
|
||||
|
return ExecuteAsync(cancellationToken); |
||||
|
|
||||
|
async IAsyncEnumerable<TResult> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) |
||||
|
{ |
||||
|
var context = await Context.GetDbContextAsync(cancellationToken); |
||||
|
|
||||
|
await foreach (var resource in query(context.Set<TResource>().AsTracking(), state).AsAsyncEnumerable().WithCancellation(cancellationToken)) |
||||
|
{ |
||||
|
yield return resource; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask SetDescriptionAsync(TResource resource, string? description, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
resource.Description = description; |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask SetDescriptionsAsync(TResource resource, |
||||
|
ImmutableDictionary<CultureInfo, string> descriptions, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
if (descriptions is not { Count: > 0 }) |
||||
|
{ |
||||
|
resource.Descriptions = null; |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
using var stream = new MemoryStream(); |
||||
|
using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions |
||||
|
{ |
||||
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, |
||||
|
Indented = false |
||||
|
}); |
||||
|
|
||||
|
writer.WriteStartObject(); |
||||
|
|
||||
|
foreach (var description in descriptions) |
||||
|
{ |
||||
|
writer.WritePropertyName(description.Key.Name); |
||||
|
writer.WriteStringValue(description.Value); |
||||
|
} |
||||
|
|
||||
|
writer.WriteEndObject(); |
||||
|
writer.Flush(); |
||||
|
|
||||
|
resource.Descriptions = Encoding.UTF8.GetString(stream.ToArray()); |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask SetDisplayNameAsync(TResource resource, string? name, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
resource.DisplayName = name; |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask SetDisplayNamesAsync(TResource resource, |
||||
|
ImmutableDictionary<CultureInfo, string> names, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
if (names is not { Count: > 0 }) |
||||
|
{ |
||||
|
resource.DisplayNames = null; |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
using var stream = new MemoryStream(); |
||||
|
using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions |
||||
|
{ |
||||
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, |
||||
|
Indented = false |
||||
|
}); |
||||
|
|
||||
|
writer.WriteStartObject(); |
||||
|
|
||||
|
foreach (var name in names) |
||||
|
{ |
||||
|
writer.WritePropertyName(name.Key.Name); |
||||
|
writer.WriteStringValue(name.Value); |
||||
|
} |
||||
|
|
||||
|
writer.WriteEndObject(); |
||||
|
writer.Flush(); |
||||
|
|
||||
|
resource.DisplayNames = Encoding.UTF8.GetString(stream.ToArray()); |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask SetNameAsync(TResource resource, string? name, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
resource.Name = name; |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask SetPropertiesAsync(TResource resource, |
||||
|
ImmutableDictionary<string, JsonElement> properties, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
if (properties is not { Count: > 0 }) |
||||
|
{ |
||||
|
resource.Properties = null; |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
using var stream = new MemoryStream(); |
||||
|
using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions |
||||
|
{ |
||||
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, |
||||
|
Indented = false |
||||
|
}); |
||||
|
|
||||
|
writer.WriteStartObject(); |
||||
|
|
||||
|
foreach (var property in properties) |
||||
|
{ |
||||
|
writer.WritePropertyName(property.Key); |
||||
|
property.Value.WriteTo(writer); |
||||
|
} |
||||
|
|
||||
|
writer.WriteEndObject(); |
||||
|
writer.Flush(); |
||||
|
|
||||
|
resource.Properties = Encoding.UTF8.GetString(stream.ToArray()); |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async ValueTask UpdateAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
var context = await Context.GetDbContextAsync(cancellationToken); |
||||
|
|
||||
|
context.Attach(resource); |
||||
|
|
||||
|
// Generate a new concurrency token and attach it
|
||||
|
// to the resource before persisting the changes.
|
||||
|
resource.ConcurrencyToken = Guid.NewGuid().ToString(); |
||||
|
|
||||
|
context.Update(resource); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
await context.SaveChangesAsync(cancellationToken); |
||||
|
} |
||||
|
|
||||
|
catch (DbUpdateConcurrencyException exception) |
||||
|
{ |
||||
|
// Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing.
|
||||
|
context.Entry(resource).State = EntityState.Unchanged; |
||||
|
|
||||
|
throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Converts the provided identifier to a strongly typed key object.
|
||||
|
/// </summary>
|
||||
|
/// <param name="identifier">The identifier to convert.</param>
|
||||
|
/// <returns>An instance of <typeparamref name="TKey"/> representing the provided identifier.</returns>
|
||||
|
public virtual TKey? ConvertIdentifierFromString(string? identifier) |
||||
|
{ |
||||
|
if (string.IsNullOrEmpty(identifier)) |
||||
|
{ |
||||
|
return default; |
||||
|
} |
||||
|
|
||||
|
// Optimization: if the key is a string, directly return it as-is.
|
||||
|
if (typeof(TKey) == typeof(string)) |
||||
|
{ |
||||
|
return (TKey?) (object?) identifier; |
||||
|
} |
||||
|
|
||||
|
else |
||||
|
{ |
||||
|
var converter = |
||||
|
#if NET
|
||||
|
TypeDescriptor.GetConverterFromRegisteredType(typeof(TKey)); |
||||
|
#else
|
||||
|
TypeDescriptor.GetConverter(typeof(TKey)); |
||||
|
#endif
|
||||
|
|
||||
|
return (TKey?) converter.ConvertFromInvariantString(identifier); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Converts the provided identifier to its string representation.
|
||||
|
/// </summary>
|
||||
|
/// <param name="identifier">The identifier to convert.</param>
|
||||
|
/// <returns>A <see cref="string"/> representation of the provided identifier.</returns>
|
||||
|
public virtual string? ConvertIdentifierToString(TKey? identifier) |
||||
|
{ |
||||
|
if (Equals(identifier, default(TKey))) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
// Optimization: if the key is a string, directly return it as-is.
|
||||
|
if (identifier is string value) |
||||
|
{ |
||||
|
return value; |
||||
|
} |
||||
|
|
||||
|
else |
||||
|
{ |
||||
|
var converter = |
||||
|
#if NET
|
||||
|
TypeDescriptor.GetConverterFromRegisteredType(typeof(TKey)); |
||||
|
#else
|
||||
|
TypeDescriptor.GetConverter(typeof(TKey)); |
||||
|
#endif
|
||||
|
|
||||
|
return converter.ConvertToInvariantString(identifier); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,67 @@ |
|||||
|
/* |
||||
|
* 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.Diagnostics; |
||||
|
|
||||
|
namespace OpenIddict.MongoDb.Models; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents an OpenIddict resource.
|
||||
|
/// </summary>
|
||||
|
[DebuggerDisplay("Id = {Id.ToString(),nq} ; Name = {Name,nq}")] |
||||
|
public class OpenIddictMongoDbResource |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets or sets the concurrency token.
|
||||
|
/// </summary>
|
||||
|
[BsonElement("concurrency_token"), BsonIgnoreIfNull] |
||||
|
public virtual string? ConcurrencyToken { get; set; } = Guid.NewGuid().ToString(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the public description associated with the current resource.
|
||||
|
/// </summary>
|
||||
|
[BsonElement("description"), BsonIgnoreIfNull] |
||||
|
public virtual string? Description { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the localized public descriptions associated with the current resource.
|
||||
|
/// </summary>
|
||||
|
[BsonElement("descriptions"), BsonIgnoreIfNull] |
||||
|
public virtual IReadOnlyDictionary<string, string>? Descriptions { get; set; } |
||||
|
= ImmutableDictionary.Create<string, string>(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the display name associated with the current resource.
|
||||
|
/// </summary>
|
||||
|
[BsonElement("display_name"), BsonIgnoreIfNull] |
||||
|
public virtual string? DisplayName { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the localized display names associated with the current resource.
|
||||
|
/// </summary>
|
||||
|
[BsonElement("display_names"), BsonIgnoreIfNull] |
||||
|
public virtual IReadOnlyDictionary<string, string>? DisplayNames { get; set; } |
||||
|
= ImmutableDictionary.Create<string, string>(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the unique identifier associated with the current resource.
|
||||
|
/// </summary>
|
||||
|
[BsonId, BsonRequired] |
||||
|
public virtual ObjectId Id { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the unique name associated with the current resource.
|
||||
|
/// </summary>
|
||||
|
[BsonElement("name"), BsonIgnoreIfNull] |
||||
|
public virtual string? Name { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the additional properties associated with the current resource.
|
||||
|
/// </summary>
|
||||
|
[BsonElement("properties"), BsonIgnoreIfNull] |
||||
|
public virtual BsonDocument? Properties { get; set; } |
||||
|
} |
||||
@ -0,0 +1,437 @@ |
|||||
|
/* |
||||
|
* 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.Diagnostics.CodeAnalysis; |
||||
|
using System.Globalization; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using System.Text; |
||||
|
using System.Text.Encodings.Web; |
||||
|
using System.Text.Json; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using OpenIddict.MongoDb.Models; |
||||
|
using static OpenIddict.Abstractions.OpenIddictExceptions; |
||||
|
|
||||
|
namespace OpenIddict.MongoDb; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Provides methods allowing to manage the resources stored in a database.
|
||||
|
/// </summary>
|
||||
|
public class OpenIddictMongoDbResourceStore : OpenIddictMongoDbResourceStore<OpenIddictMongoDbResource> |
||||
|
{ |
||||
|
public OpenIddictMongoDbResourceStore( |
||||
|
IOpenIddictMongoDbContext context, |
||||
|
IOptionsMonitor<OpenIddictMongoDbOptions> options) |
||||
|
: base(context, options) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Provides methods allowing to manage the resources stored in a database.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TResource">The type of the resource entity.</typeparam>
|
||||
|
public class OpenIddictMongoDbResourceStore< |
||||
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResource> : IOpenIddictResourceStore<TResource> |
||||
|
where TResource : OpenIddictMongoDbResource |
||||
|
{ |
||||
|
public OpenIddictMongoDbResourceStore( |
||||
|
IOpenIddictMongoDbContext context, |
||||
|
IOptionsMonitor<OpenIddictMongoDbOptions> options) |
||||
|
{ |
||||
|
Context = context ?? throw new ArgumentNullException(nameof(context)); |
||||
|
Options = options ?? throw new ArgumentNullException(nameof(options)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the database context associated with the current store.
|
||||
|
/// </summary>
|
||||
|
protected IOpenIddictMongoDbContext Context { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the options associated with the current store.
|
||||
|
/// </summary>
|
||||
|
protected IOptionsMonitor<OpenIddictMongoDbOptions> Options { get; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async ValueTask<long> CountAsync(CancellationToken cancellationToken) |
||||
|
{ |
||||
|
var database = await Context.GetDatabaseAsync(cancellationToken); |
||||
|
var collection = database.GetCollection<TResource>(Options.CurrentValue.ResourcesCollectionName); |
||||
|
|
||||
|
return await collection.CountDocumentsAsync(FilterDefinition<TResource>.Empty, null, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async ValueTask<long> CountAsync<TState, TResult>( |
||||
|
Func<IQueryable<TResource>, TState, IQueryable<TResult>> query, |
||||
|
TState state, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(query); |
||||
|
|
||||
|
var database = await Context.GetDatabaseAsync(cancellationToken); |
||||
|
var collection = database.GetCollection<TResource>(Options.CurrentValue.ResourcesCollectionName); |
||||
|
|
||||
|
return await query(collection.AsQueryable(), state).LongCountAsync(cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async ValueTask CreateAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
var database = await Context.GetDatabaseAsync(cancellationToken); |
||||
|
var collection = database.GetCollection<TResource>(Options.CurrentValue.ResourcesCollectionName); |
||||
|
|
||||
|
await collection.InsertOneAsync(resource, null, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async ValueTask DeleteAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
var database = await Context.GetDatabaseAsync(cancellationToken); |
||||
|
var collection = database.GetCollection<TResource>(Options.CurrentValue.ResourcesCollectionName); |
||||
|
|
||||
|
if ((await collection.DeleteOneAsync(entity => |
||||
|
entity.Id == resource.Id && |
||||
|
entity.ConcurrencyToken == resource.ConcurrencyToken, cancellationToken)).DeletedCount is 0) |
||||
|
{ |
||||
|
throw new ConcurrencyException(SR.GetResourceString(SR.ID0239)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async ValueTask<TResource?> FindByIdAsync(string identifier, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentException.ThrowIfNullOrEmpty(identifier); |
||||
|
|
||||
|
var database = await Context.GetDatabaseAsync(cancellationToken); |
||||
|
var collection = database.GetCollection<TResource>(Options.CurrentValue.ResourcesCollectionName); |
||||
|
|
||||
|
return await collection.Find(resource => resource.Id == ObjectId.Parse(identifier)).FirstOrDefaultAsync(cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async ValueTask<TResource?> FindByNameAsync(string name, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentException.ThrowIfNullOrEmpty(name); |
||||
|
|
||||
|
var database = await Context.GetDatabaseAsync(cancellationToken); |
||||
|
var collection = database.GetCollection<TResource>(Options.CurrentValue.ResourcesCollectionName); |
||||
|
|
||||
|
return await collection.Find(resource => resource.Name == name).FirstOrDefaultAsync(cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual IAsyncEnumerable<TResource> FindByNamesAsync(ImmutableArray<string> names, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
if (names.Any(string.IsNullOrEmpty)) |
||||
|
{ |
||||
|
throw new ArgumentException(SR.GetResourceString(SR.ID0203), nameof(names)); |
||||
|
} |
||||
|
|
||||
|
return ExecuteAsync(cancellationToken); |
||||
|
|
||||
|
async IAsyncEnumerable<TResource> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) |
||||
|
{ |
||||
|
var database = await Context.GetDatabaseAsync(cancellationToken); |
||||
|
var collection = database.GetCollection<TResource>(Options.CurrentValue.ResourcesCollectionName); |
||||
|
|
||||
|
// Note: Enumerable.Contains() is deliberately used without the extension method syntax to ensure
|
||||
|
// ImmutableArray.Contains() (which is not fully supported by MongoDB) is not used instead.
|
||||
|
await foreach (var resource in collection.Find(resource => Enumerable.Contains(names, resource.Name)).ToAsyncEnumerable(cancellationToken)) |
||||
|
{ |
||||
|
yield return resource; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async ValueTask<TResult?> GetAsync<TState, TResult>( |
||||
|
Func<IQueryable<TResource>, TState, IQueryable<TResult>> query, |
||||
|
TState state, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(query); |
||||
|
|
||||
|
var database = await Context.GetDatabaseAsync(cancellationToken); |
||||
|
var collection = database.GetCollection<TResource>(Options.CurrentValue.ResourcesCollectionName); |
||||
|
|
||||
|
return await query(collection.AsQueryable(), state).FirstOrDefaultAsync(cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask<string?> GetDescriptionAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
return new(resource.Description); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask<ImmutableDictionary<CultureInfo, string>> GetDescriptionsAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
if (resource.Descriptions is not { Count: > 0 }) |
||||
|
{ |
||||
|
return new(ImmutableDictionary.Create<CultureInfo, string>()); |
||||
|
} |
||||
|
|
||||
|
return new(resource.Descriptions.ToImmutableDictionary( |
||||
|
static pair => CultureInfo.GetCultureInfo(pair.Key), |
||||
|
static pair => pair.Value)); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask<string?> GetDisplayNameAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
return new(resource.DisplayName); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask<ImmutableDictionary<CultureInfo, string>> GetDisplayNamesAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
if (resource.DisplayNames is not { Count: > 0 }) |
||||
|
{ |
||||
|
return new(ImmutableDictionary.Create<CultureInfo, string>()); |
||||
|
} |
||||
|
|
||||
|
return new(resource.DisplayNames.ToImmutableDictionary( |
||||
|
static pair => CultureInfo.GetCultureInfo(pair.Key), |
||||
|
static pair => pair.Value)); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask<string?> GetIdAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
return new(resource.Id.ToString()); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask<string?> GetNameAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
return new(resource.Name); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask<ImmutableDictionary<string, JsonElement>> GetPropertiesAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
if (resource.Properties is null) |
||||
|
{ |
||||
|
return new(ImmutableDictionary.Create<string, JsonElement>()); |
||||
|
} |
||||
|
|
||||
|
using var document = JsonDocument.Parse(resource.Properties.ToJson()); |
||||
|
var builder = ImmutableDictionary.CreateBuilder<string, JsonElement>(); |
||||
|
|
||||
|
foreach (var property in document.RootElement.EnumerateObject()) |
||||
|
{ |
||||
|
builder[property.Name] = property.Value.Clone(); |
||||
|
} |
||||
|
|
||||
|
return new(builder.ToImmutable()); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask<TResource> InstantiateAsync(CancellationToken cancellationToken) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
return new(Activator.CreateInstance<TResource>()); |
||||
|
} |
||||
|
|
||||
|
catch (MemberAccessException exception) |
||||
|
{ |
||||
|
return new(Task.FromException<TResource>( |
||||
|
new InvalidOperationException(SR.GetResourceString(SR.ID0240), exception))); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async IAsyncEnumerable<TResource> ListAsync( |
||||
|
int? count, int? offset, [EnumeratorCancellation] CancellationToken cancellationToken) |
||||
|
{ |
||||
|
var database = await Context.GetDatabaseAsync(cancellationToken); |
||||
|
var collection = database.GetCollection<TResource>(Options.CurrentValue.ResourcesCollectionName); |
||||
|
|
||||
|
var query = (IQueryable<TResource>) collection.AsQueryable().OrderBy(resource => resource.Id); |
||||
|
|
||||
|
if (offset.HasValue) |
||||
|
{ |
||||
|
query = query.Skip(offset.Value); |
||||
|
} |
||||
|
|
||||
|
if (count.HasValue) |
||||
|
{ |
||||
|
query = query.Take(count.Value); |
||||
|
} |
||||
|
|
||||
|
await foreach (var resource in ((IAsyncCursorSource<TResource>) query).ToAsyncEnumerable(cancellationToken)) |
||||
|
{ |
||||
|
yield return resource; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual IAsyncEnumerable<TResult> ListAsync<TState, TResult>( |
||||
|
Func<IQueryable<TResource>, TState, IQueryable<TResult>> query, |
||||
|
TState state, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(query); |
||||
|
|
||||
|
return ExecuteAsync(cancellationToken); |
||||
|
|
||||
|
async IAsyncEnumerable<TResult> ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) |
||||
|
{ |
||||
|
var database = await Context.GetDatabaseAsync(cancellationToken); |
||||
|
var collection = database.GetCollection<TResource>(Options.CurrentValue.ResourcesCollectionName); |
||||
|
|
||||
|
await foreach (var element in query(collection.AsQueryable(), state).ToAsyncEnumerable(cancellationToken)) |
||||
|
{ |
||||
|
yield return element; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask SetDescriptionAsync(TResource resource, string? description, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
resource.Description = description; |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask SetDescriptionsAsync(TResource resource, |
||||
|
ImmutableDictionary<CultureInfo, string> descriptions, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
if (descriptions is not { Count: > 0 }) |
||||
|
{ |
||||
|
resource.Descriptions = null; |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
resource.Descriptions = descriptions.ToImmutableDictionary( |
||||
|
static pair => pair.Key.Name, |
||||
|
static pair => pair.Value); |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask SetDisplayNamesAsync(TResource resource, |
||||
|
ImmutableDictionary<CultureInfo, string> names, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
if (names is not { Count: > 0 }) |
||||
|
{ |
||||
|
resource.DisplayNames = null; |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
resource.DisplayNames = names.ToImmutableDictionary( |
||||
|
static pair => pair.Key.Name, |
||||
|
static pair => pair.Value); |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask SetDisplayNameAsync(TResource resource, string? name, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
resource.DisplayName = name; |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask SetNameAsync(TResource resource, string? name, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
resource.Name = name; |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual ValueTask SetPropertiesAsync(TResource resource, |
||||
|
ImmutableDictionary<string, JsonElement> properties, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
if (properties is not { Count: > 0 }) |
||||
|
{ |
||||
|
resource.Properties = null; |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
using var stream = new MemoryStream(); |
||||
|
using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions |
||||
|
{ |
||||
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, |
||||
|
Indented = false |
||||
|
}); |
||||
|
|
||||
|
writer.WriteStartObject(); |
||||
|
|
||||
|
foreach (var property in properties) |
||||
|
{ |
||||
|
writer.WritePropertyName(property.Key); |
||||
|
property.Value.WriteTo(writer); |
||||
|
} |
||||
|
|
||||
|
writer.WriteEndObject(); |
||||
|
writer.Flush(); |
||||
|
|
||||
|
resource.Properties = BsonDocument.Parse(Encoding.UTF8.GetString(stream.ToArray())); |
||||
|
|
||||
|
return ValueTask.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual async ValueTask UpdateAsync(TResource resource, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
ArgumentNullException.ThrowIfNull(resource); |
||||
|
|
||||
|
// Generate a new concurrency token and attach it
|
||||
|
// to the resource before persisting the changes.
|
||||
|
var timestamp = resource.ConcurrencyToken; |
||||
|
resource.ConcurrencyToken = Guid.NewGuid().ToString(); |
||||
|
|
||||
|
var database = await Context.GetDatabaseAsync(cancellationToken); |
||||
|
var collection = database.GetCollection<TResource>(Options.CurrentValue.ResourcesCollectionName); |
||||
|
|
||||
|
if ((await collection.ReplaceOneAsync(entity => |
||||
|
entity.Id == resource.Id && |
||||
|
entity.ConcurrencyToken == timestamp, resource, null as ReplaceOptions, cancellationToken)).MatchedCount is 0) |
||||
|
{ |
||||
|
throw new ConcurrencyException(SR.GetResourceString(SR.ID0239)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,361 @@ |
|||||
|
/* |
||||
|
* 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 Microsoft.Extensions.Options; |
||||
|
using Moq; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace OpenIddict.Core.Tests; |
||||
|
|
||||
|
public class OpenIddictResourceCacheTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Constructor_ThrowsAnExceptionForNullOptions() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var options = (IOptionsMonitor<OpenIddictCoreOptions>) null!; |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<object>>(); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = Assert.Throws<ArgumentNullException>(() => new OpenIddictResourceCache<object>(options, store)); |
||||
|
|
||||
|
Assert.Equal("options", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Constructor_ThrowsAnExceptionForNullStore() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = (IOpenIddictResourceStore<object>) null!; |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = Assert.Throws<ArgumentNullException>(() => new OpenIddictResourceCache<object>(options, store)); |
||||
|
|
||||
|
Assert.Equal("store", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task AddAsync_ThrowsAnExceptionForNullResource() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<OpenIddictResource>>(); |
||||
|
var cache = new OpenIddictResourceCache<OpenIddictResource>(options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentNullException>( |
||||
|
() => cache.AddAsync(resource: null!, CancellationToken.None).AsTask()); |
||||
|
|
||||
|
Assert.Equal("resource", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Dispose_CanBeCalledMultipleTimes() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<OpenIddictResource>>(); |
||||
|
var cache = new OpenIddictResourceCache<OpenIddictResource>(options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
cache.Dispose(); |
||||
|
cache.Dispose(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task FindByIdAsync_ThrowsAnExceptionForNullIdentifier() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<OpenIddictResource>>(); |
||||
|
var cache = new OpenIddictResourceCache<OpenIddictResource>(options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentNullException>( |
||||
|
() => cache.FindByIdAsync(identifier: null!, CancellationToken.None).AsTask()); |
||||
|
|
||||
|
Assert.Equal("identifier", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task FindByIdAsync_ThrowsAnExceptionForEmptyIdentifier() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<OpenIddictResource>>(); |
||||
|
var cache = new OpenIddictResourceCache<OpenIddictResource>(options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentException>( |
||||
|
() => cache.FindByIdAsync(identifier: string.Empty, CancellationToken.None).AsTask()); |
||||
|
|
||||
|
Assert.Equal("identifier", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task FindByIdAsync_ReturnsCachedResourceOnCacheHit() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var resource = new OpenIddictResource(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = new Mock<IOpenIddictResourceStore<OpenIddictResource>>(); |
||||
|
|
||||
|
store.Setup(store => store.GetIdAsync(resource, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync("resource-id"); |
||||
|
store.Setup(store => store.GetNameAsync(resource, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync("resource-name"); |
||||
|
|
||||
|
var cache = new OpenIddictResourceCache<OpenIddictResource>(options, store.Object); |
||||
|
|
||||
|
await cache.AddAsync(resource, CancellationToken.None); |
||||
|
|
||||
|
// Act
|
||||
|
var result = await cache.FindByIdAsync("resource-id", CancellationToken.None); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.Same(resource, result); |
||||
|
store.Verify(store => store.FindByIdAsync("resource-id", It.IsAny<CancellationToken>()), Times.Never()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task FindByIdAsync_QueriesStoreOnCacheMiss() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var resource = new OpenIddictResource(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = new Mock<IOpenIddictResourceStore<OpenIddictResource>>(); |
||||
|
|
||||
|
store.Setup(store => store.FindByIdAsync("resource-id", It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(resource); |
||||
|
store.Setup(store => store.GetIdAsync(resource, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync("resource-id"); |
||||
|
store.Setup(store => store.GetNameAsync(resource, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync("resource-name"); |
||||
|
|
||||
|
var cache = new OpenIddictResourceCache<OpenIddictResource>(options, store.Object); |
||||
|
|
||||
|
// Act
|
||||
|
var result = await cache.FindByIdAsync("resource-id", CancellationToken.None); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.Same(resource, result); |
||||
|
store.Verify(store => store.FindByIdAsync("resource-id", It.IsAny<CancellationToken>()), Times.Once()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task FindByIdAsync_ReturnsNullWhenResourceNotFound() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = new Mock<IOpenIddictResourceStore<OpenIddictResource>>(); |
||||
|
|
||||
|
store.Setup(store => store.FindByIdAsync("resource-id", It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync((OpenIddictResource?) null); |
||||
|
|
||||
|
var cache = new OpenIddictResourceCache<OpenIddictResource>(options, store.Object); |
||||
|
|
||||
|
// Act
|
||||
|
var result = await cache.FindByIdAsync("resource-id", CancellationToken.None); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.Null(result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task FindByNameAsync_ThrowsAnExceptionForNullName() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<OpenIddictResource>>(); |
||||
|
var cache = new OpenIddictResourceCache<OpenIddictResource>(options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentNullException>( |
||||
|
() => cache.FindByNameAsync(name: null!, CancellationToken.None).AsTask()); |
||||
|
|
||||
|
Assert.Equal("name", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task FindByNameAsync_ThrowsAnExceptionForEmptyName() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<OpenIddictResource>>(); |
||||
|
var cache = new OpenIddictResourceCache<OpenIddictResource>(options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentException>( |
||||
|
() => cache.FindByNameAsync(name: string.Empty, CancellationToken.None).AsTask()); |
||||
|
|
||||
|
Assert.Equal("name", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task FindByNameAsync_ReturnsCachedResourceOnCacheHit() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var resource = new OpenIddictResource(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = new Mock<IOpenIddictResourceStore<OpenIddictResource>>(); |
||||
|
|
||||
|
store.Setup(store => store.GetIdAsync(resource, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync("resource-id"); |
||||
|
store.Setup(store => store.GetNameAsync(resource, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync("resource-name"); |
||||
|
|
||||
|
var cache = new OpenIddictResourceCache<OpenIddictResource>(options, store.Object); |
||||
|
|
||||
|
await cache.AddAsync(resource, CancellationToken.None); |
||||
|
|
||||
|
// Act
|
||||
|
var result = await cache.FindByNameAsync("resource-name", CancellationToken.None); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.Same(resource, result); |
||||
|
store.Verify(store => store.FindByNameAsync("resource-name", It.IsAny<CancellationToken>()), Times.Never()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task FindByNameAsync_QueriesStoreOnCacheMiss() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var resource = new OpenIddictResource(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = new Mock<IOpenIddictResourceStore<OpenIddictResource>>(); |
||||
|
|
||||
|
store.Setup(store => store.FindByNameAsync("resource-name", It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(resource); |
||||
|
store.Setup(store => store.GetIdAsync(resource, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync("resource-id"); |
||||
|
store.Setup(store => store.GetNameAsync(resource, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync("resource-name"); |
||||
|
|
||||
|
var cache = new OpenIddictResourceCache<OpenIddictResource>(options, store.Object); |
||||
|
|
||||
|
// Act
|
||||
|
var result = await cache.FindByNameAsync("resource-name", CancellationToken.None); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.Same(resource, result); |
||||
|
store.Verify(store => store.FindByNameAsync("resource-name", It.IsAny<CancellationToken>()), Times.Once()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task FindByNamesAsync_QueriesStoreOnCacheMiss() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var resources = new[] |
||||
|
{ |
||||
|
new OpenIddictResource(), |
||||
|
new OpenIddictResource() |
||||
|
}; |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = new Mock<IOpenIddictResourceStore<OpenIddictResource>>(); |
||||
|
|
||||
|
store.Setup(store => store.GetIdAsync(resources[0], It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync("resource-id-1"); |
||||
|
store.Setup(store => store.GetNameAsync(resources[0], It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync("resource-name-1"); |
||||
|
|
||||
|
store.Setup(store => store.GetIdAsync(resources[1], It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync("resource-id-2"); |
||||
|
store.Setup(store => store.GetNameAsync(resources[1], It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync("resource-name-2"); |
||||
|
|
||||
|
store.Setup(store => store.FindByNamesAsync(It.IsAny<ImmutableArray<string>>(), It.IsAny<CancellationToken>())) |
||||
|
.Returns(resources.ToAsyncEnumerable()); |
||||
|
|
||||
|
var cache = new OpenIddictResourceCache<OpenIddictResource>(options, store.Object); |
||||
|
|
||||
|
// Act
|
||||
|
var results = await cache.FindByNamesAsync(["resource-name-1", "resource-name-2"], CancellationToken.None).ToListAsync(); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.Equal(2, results.Count); |
||||
|
Assert.Contains(resources[0], results); |
||||
|
Assert.Contains(resources[1], results); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task RemoveAsync_ThrowsAnExceptionForNullResource() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<OpenIddictResource>>(); |
||||
|
var cache = new OpenIddictResourceCache<OpenIddictResource>(options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentNullException>( |
||||
|
() => cache.RemoveAsync(resource: null!, CancellationToken.None).AsTask()); |
||||
|
|
||||
|
Assert.Equal("resource", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task RemoveAsync_InvalidatesCachedEntries() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var resource = new OpenIddictResource(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = new Mock<IOpenIddictResourceStore<OpenIddictResource>>(); |
||||
|
|
||||
|
store.Setup(store => store.GetIdAsync(resource, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync("resource-id"); |
||||
|
store.Setup(store => store.GetNameAsync(resource, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync("resource-name"); |
||||
|
|
||||
|
var cache = new OpenIddictResourceCache<OpenIddictResource>(options, store.Object); |
||||
|
|
||||
|
await cache.AddAsync(resource, CancellationToken.None); |
||||
|
|
||||
|
// Act
|
||||
|
await cache.RemoveAsync(resource, CancellationToken.None); |
||||
|
|
||||
|
var result = await cache.FindByIdAsync("resource-id", CancellationToken.None); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.Null(result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task RemoveAsync_ThrowsForResourceWithoutId() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var resource = new OpenIddictResource(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = new Mock<IOpenIddictResourceStore<OpenIddictResource>>(); |
||||
|
|
||||
|
store.Setup(store => store.GetIdAsync(resource, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync((string?) null); |
||||
|
|
||||
|
var cache = new OpenIddictResourceCache<OpenIddictResource>(options, store.Object); |
||||
|
|
||||
|
// Act and assert
|
||||
|
await Assert.ThrowsAsync<InvalidOperationException>( |
||||
|
() => cache.RemoveAsync(resource, CancellationToken.None).AsTask()); |
||||
|
} |
||||
|
|
||||
|
public sealed class OpenIddictResource; |
||||
|
} |
||||
@ -0,0 +1,888 @@ |
|||||
|
/* |
||||
|
* 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.ComponentModel.DataAnnotations; |
||||
|
using System.Globalization; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Moq; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace OpenIddict.Core.Tests; |
||||
|
|
||||
|
public class OpenIddictResourceManagerTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Constructor_ThrowsAnExceptionForNullCache() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = (IOpenIddictResourceCache<CustomResource>) null!; |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = Assert.Throws<ArgumentNullException>( |
||||
|
() => new OpenIddictResourceManager<CustomResource>(cache, logger, options, store)); |
||||
|
|
||||
|
Assert.Equal("cache", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Constructor_ThrowsAnExceptionForNullLogger() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = (ILogger<OpenIddictResourceManager<CustomResource>>) null!; |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = Assert.Throws<ArgumentNullException>( |
||||
|
() => new OpenIddictResourceManager<CustomResource>(cache, logger, options, store)); |
||||
|
|
||||
|
Assert.Equal("logger", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Constructor_ThrowsAnExceptionForNullOptions() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = (IOptionsMonitor<OpenIddictCoreOptions>) null!; |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = Assert.Throws<ArgumentNullException>( |
||||
|
() => new OpenIddictResourceManager<CustomResource>(cache, logger, options, store)); |
||||
|
|
||||
|
Assert.Equal("options", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Constructor_ThrowsAnExceptionForNullStore() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = (IOpenIddictResourceStore<CustomResource>) null!; |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = Assert.Throws<ArgumentNullException>( |
||||
|
() => new OpenIddictResourceManager<CustomResource>(cache, logger, options, store)); |
||||
|
|
||||
|
Assert.Equal("store", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task CountAsync_CallsStoreMethod() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = new Mock<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
store.Setup(store => store.CountAsync(It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(42); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store.Object); |
||||
|
|
||||
|
// Act
|
||||
|
var count = await manager.CountAsync(); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.Equal(42, count); |
||||
|
store.Verify(store => store.CountAsync(It.IsAny<CancellationToken>()), Times.Once()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task CountAsync_WithQuery_ThrowsAnExceptionForNullQuery() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentNullException>( |
||||
|
() => manager.CountAsync<CustomResource>(query: null!).AsTask()); |
||||
|
|
||||
|
Assert.Equal("query", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task CreateAsync_ThrowsAnExceptionForNullResource() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentNullException>( |
||||
|
() => manager.CreateAsync(resource: null!).AsTask()); |
||||
|
|
||||
|
Assert.Equal("resource", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task DeleteAsync_ThrowsAnExceptionForNullResource() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentNullException>( |
||||
|
() => manager.DeleteAsync(resource: null!).AsTask()); |
||||
|
|
||||
|
Assert.Equal("resource", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task DeleteAsync_RemovesResourceFromCache_WhenCachingIsEnabled() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = new Mock<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions { DisableEntityCaching = false }); |
||||
|
var store = new Mock<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
var resource = new CustomResource(); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache.Object, logger, options, store.Object); |
||||
|
|
||||
|
// Act
|
||||
|
await manager.DeleteAsync(resource); |
||||
|
|
||||
|
// Assert
|
||||
|
cache.Verify(cache => cache.RemoveAsync(resource, It.IsAny<CancellationToken>()), Times.Once()); |
||||
|
store.Verify(store => store.DeleteAsync(resource, It.IsAny<CancellationToken>()), Times.Once()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task DeleteAsync_DoesNotRemoveFromCache_WhenCachingIsDisabled() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = new Mock<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions { DisableEntityCaching = true }); |
||||
|
var store = new Mock<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
var resource = new CustomResource(); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache.Object, logger, options, store.Object); |
||||
|
|
||||
|
// Act
|
||||
|
await manager.DeleteAsync(resource); |
||||
|
|
||||
|
// Assert
|
||||
|
cache.Verify(cache => cache.RemoveAsync(It.IsAny<CustomResource>(), It.IsAny<CancellationToken>()), Times.Never()); |
||||
|
store.Verify(store => store.DeleteAsync(resource, It.IsAny<CancellationToken>()), Times.Once()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task FindByIdAsync_ThrowsAnExceptionForNullIdentifier() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentNullException>( |
||||
|
() => manager.FindByIdAsync(identifier: null!).AsTask()); |
||||
|
|
||||
|
Assert.Equal("identifier", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task FindByIdAsync_ThrowsAnExceptionForEmptyIdentifier() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentException>( |
||||
|
() => manager.FindByIdAsync(identifier: string.Empty).AsTask()); |
||||
|
|
||||
|
Assert.Equal("identifier", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task FindByIdAsync_UsesCache_WhenCachingIsEnabled() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var resource = new CustomResource(); |
||||
|
var cache = new Mock<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions { DisableEntityCaching = false }); |
||||
|
var store = new Mock<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
cache.Setup(cache => cache.FindByIdAsync("id", It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(resource); |
||||
|
|
||||
|
store.Setup(store => store.GetIdAsync(resource, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync("id"); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache.Object, logger, options, store.Object); |
||||
|
|
||||
|
// Act
|
||||
|
var result = await manager.FindByIdAsync("id"); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.Same(resource, result); |
||||
|
cache.Verify(cache => cache.FindByIdAsync("id", It.IsAny<CancellationToken>()), Times.Once()); |
||||
|
store.Verify(store => store.FindByIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task FindByIdAsync_UsesStore_WhenCachingIsDisabled() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var resource = new CustomResource(); |
||||
|
var cache = new Mock<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions { DisableEntityCaching = true }); |
||||
|
var store = new Mock<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
store.Setup(store => store.FindByIdAsync("id", It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(resource); |
||||
|
|
||||
|
store.Setup(store => store.GetIdAsync(resource, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync("id"); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache.Object, logger, options, store.Object); |
||||
|
|
||||
|
// Act
|
||||
|
var result = await manager.FindByIdAsync("id"); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.Same(resource, result); |
||||
|
cache.Verify(cache => cache.FindByIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never()); |
||||
|
store.Verify(store => store.FindByIdAsync("id", It.IsAny<CancellationToken>()), Times.Once()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task FindByNameAsync_ThrowsAnExceptionForNullName() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentNullException>( |
||||
|
() => manager.FindByNameAsync(name: null!).AsTask()); |
||||
|
|
||||
|
Assert.Equal("name", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task FindByNameAsync_ThrowsAnExceptionForEmptyName() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentException>( |
||||
|
() => manager.FindByNameAsync(name: string.Empty).AsTask()); |
||||
|
|
||||
|
Assert.Equal("name", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task FindByNamesAsync_ReturnsEmptyWhenNoResourcesMatch() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = new Mock<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
cache.Setup(cache => cache.FindByNamesAsync(It.IsAny<ImmutableArray<string>>(), It.IsAny<CancellationToken>())) |
||||
|
.Returns((ImmutableArray<string> names, CancellationToken cancellationToken) => |
||||
|
{ |
||||
|
return Enumerable.Empty<CustomResource>().ToAsyncEnumerable(); |
||||
|
}); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache.Object, logger, options, store); |
||||
|
|
||||
|
// Act
|
||||
|
var results = new List<CustomResource>(); |
||||
|
await foreach (var resource in manager.FindByNamesAsync(["resource1", "resource2"])) |
||||
|
{ |
||||
|
results.Add(resource); |
||||
|
} |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.Empty(results); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetAsync_WithQuery_ThrowsAnExceptionForNullQuery() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentNullException>( |
||||
|
() => manager.GetAsync<CustomResource>(query: null!).AsTask()); |
||||
|
|
||||
|
Assert.Equal("query", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetAsync_WithQueryAndState_ThrowsAnExceptionForNullQuery() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentNullException>( |
||||
|
() => manager.GetAsync<object, CustomResource>(query: null!, state: null!).AsTask()); |
||||
|
|
||||
|
Assert.Equal("query", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetDescriptionAsync_ThrowsAnExceptionForNullResource() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentNullException>( |
||||
|
() => manager.GetDescriptionAsync(resource: null!).AsTask()); |
||||
|
|
||||
|
Assert.Equal("resource", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetDescriptionsAsync_ThrowsAnExceptionForNullResource() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentNullException>( |
||||
|
() => manager.GetDescriptionsAsync(resource: null!).AsTask()); |
||||
|
|
||||
|
Assert.Equal("resource", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetDisplayNameAsync_ThrowsAnExceptionForNullResource() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentNullException>( |
||||
|
() => manager.GetDisplayNameAsync(resource: null!).AsTask()); |
||||
|
|
||||
|
Assert.Equal("resource", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetDisplayNamesAsync_ThrowsAnExceptionForNullResource() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentNullException>( |
||||
|
() => manager.GetDisplayNamesAsync(resource: null!).AsTask()); |
||||
|
|
||||
|
Assert.Equal("resource", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetIdAsync_ThrowsAnExceptionForNullResource() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentNullException>( |
||||
|
() => manager.GetIdAsync(resource: null!).AsTask()); |
||||
|
|
||||
|
Assert.Equal("resource", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetIdAsync_ReturnsIdentifierFromStore() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var resource = new CustomResource(); |
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = new Mock<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
store.Setup(store => store.GetIdAsync(resource, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync("unique-resource-id"); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store.Object); |
||||
|
|
||||
|
// Act
|
||||
|
var id = await manager.GetIdAsync(resource); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.Equal("unique-resource-id", id); |
||||
|
store.Verify(store => store.GetIdAsync(resource, It.IsAny<CancellationToken>()), Times.Once()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetLocalizedDescriptionAsync_ThrowsAnExceptionForNullResource() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentNullException>( |
||||
|
() => manager.GetLocalizedDescriptionAsync(resource: null!, CultureInfo.InvariantCulture).AsTask()); |
||||
|
|
||||
|
Assert.Equal("resource", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetLocalizedDescriptionAsync_ReturnsDescriptionForMatchingCulture() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var resource = new CustomResource(); |
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = new Mock<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
store.Setup(store => store.GetDescriptionsAsync(resource, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(ImmutableDictionary.Create<CultureInfo, string>() |
||||
|
.Add(CultureInfo.GetCultureInfo("en-US"), "English description") |
||||
|
.Add(CultureInfo.GetCultureInfo("fr-FR"), "Description française")); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store.Object); |
||||
|
|
||||
|
// Act
|
||||
|
var description = await manager.GetLocalizedDescriptionAsync(resource, CultureInfo.GetCultureInfo("fr-FR")); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.Equal("Description française", description); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetLocalizedDescriptionAsync_FallsBackToNonLocalizedDescriptionWhenNoCultureMatches() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var resource = new CustomResource(); |
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = new Mock<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
store.Setup(store => store.GetDescriptionsAsync(resource, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(ImmutableDictionary.Create<CultureInfo, string>() |
||||
|
.Add(CultureInfo.GetCultureInfo("en-US"), "English description")); |
||||
|
|
||||
|
store.Setup(store => store.GetDescriptionAsync(resource, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync("Default description"); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store.Object); |
||||
|
|
||||
|
// Act
|
||||
|
var description = await manager.GetLocalizedDescriptionAsync(resource, CultureInfo.GetCultureInfo("ja-JP")); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.Equal("Default description", description); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetLocalizedDisplayNameAsync_ThrowsAnExceptionForNullResource() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentNullException>( |
||||
|
() => manager.GetLocalizedDisplayNameAsync(resource: null!, CultureInfo.InvariantCulture).AsTask()); |
||||
|
|
||||
|
Assert.Equal("resource", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetLocalizedDisplayNameAsync_ReturnsDisplayNameForMatchingCulture() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var resource = new CustomResource(); |
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = new Mock<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
store.Setup(store => store.GetDisplayNamesAsync(resource, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(ImmutableDictionary.Create<CultureInfo, string>() |
||||
|
.Add(CultureInfo.GetCultureInfo("en-US"), "English name") |
||||
|
.Add(CultureInfo.GetCultureInfo("fr-FR"), "Nom fran�ais")); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store.Object); |
||||
|
|
||||
|
// Act
|
||||
|
var name = await manager.GetLocalizedDisplayNameAsync(resource, CultureInfo.GetCultureInfo("fr-FR")); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.Equal("Nom fran�ais", name); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetNameAsync_ThrowsAnExceptionForNullResource() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentNullException>( |
||||
|
() => manager.GetNameAsync(resource: null!).AsTask()); |
||||
|
|
||||
|
Assert.Equal("resource", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetPropertiesAsync_ThrowsAnExceptionForNullResource() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentNullException>( |
||||
|
() => manager.GetPropertiesAsync(resource: null!).AsTask()); |
||||
|
|
||||
|
Assert.Equal("resource", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task ListAsync_ReturnsAllResources() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var resources = new[] { new CustomResource(), new CustomResource() }; |
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = new Mock<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
store.Setup(store => store.ListAsync(It.IsAny<int?>(), It.IsAny<int?>(), It.IsAny<CancellationToken>())) |
||||
|
.Returns(resources.ToAsyncEnumerable()); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store.Object); |
||||
|
|
||||
|
// Act
|
||||
|
var results = new List<CustomResource>(); |
||||
|
await foreach (var scp in manager.ListAsync()) |
||||
|
{ |
||||
|
results.Add(scp); |
||||
|
} |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.Equal(2, results.Count); |
||||
|
store.Verify(store => store.ListAsync(It.IsAny<int?>(), It.IsAny<int?>(), It.IsAny<CancellationToken>()), Times.Once()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task PopulateAsync_ThrowsAnExceptionForNullResource() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store); |
||||
|
var descriptor = new OpenIddictResourceDescriptor(); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentNullException>( |
||||
|
() => manager.PopulateAsync(resource: null!, descriptor).AsTask()); |
||||
|
|
||||
|
Assert.Equal("resource", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task PopulateAsync_ThrowsAnExceptionForNullDescriptor() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var resource = new CustomResource(); |
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentNullException>( |
||||
|
() => manager.PopulateAsync(resource, descriptor: null!).AsTask()); |
||||
|
|
||||
|
Assert.Equal("descriptor", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task UpdateAsync_ThrowsAnExceptionForNullResource() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentNullException>( |
||||
|
() => manager.UpdateAsync(resource: null!).AsTask()); |
||||
|
|
||||
|
Assert.Equal("resource", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task UpdateAsync_WithDescriptor_ThrowsAnExceptionForNullResource() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store); |
||||
|
var descriptor = new OpenIddictResourceDescriptor(); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = await Assert.ThrowsAsync<ArgumentNullException>( |
||||
|
() => manager.UpdateAsync(resource: null!, descriptor).AsTask()); |
||||
|
|
||||
|
Assert.Equal("resource", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void ValidateAsync_ThrowsAnExceptionForNullResource() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(); |
||||
|
var store = Mock.Of<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store); |
||||
|
|
||||
|
// Act and assert
|
||||
|
var exception = Assert.Throws<ArgumentNullException>( |
||||
|
() => manager.ValidateAsync(resource: null!)); |
||||
|
|
||||
|
Assert.Equal("resource", exception.ParamName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task ValidateAsync_ReturnsErrorWhenNameIsEmpty() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var resource = new CustomResource(); |
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = new Mock<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
store.Setup(store => store.GetNameAsync(resource, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(string.Empty); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store.Object); |
||||
|
|
||||
|
// Act
|
||||
|
var results = await manager.ValidateAsync(resource).ToListAsync(); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.Contains(results, result => result.ErrorMessage == SR.GetResourceString(SR.ID2206)); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData("resource")] |
||||
|
[InlineData("/resource")] |
||||
|
[InlineData("urn:resource#fragment")] |
||||
|
public async Task ValidateAsync_ReturnsErrorWhenNameIsNotValidUri(string name) |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var resource = new CustomResource(); |
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = new Mock<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
store.Setup(store => store.GetNameAsync(resource, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(name); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store.Object); |
||||
|
|
||||
|
// Act
|
||||
|
var results = await manager.ValidateAsync(resource).ToListAsync(); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.Contains(results, result => result.ErrorMessage == SR.GetResourceString(SR.ID2207)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task ValidateAsync_ReturnsErrorWhenNameIsAlreadyUsed() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var resource = new CustomResource(); |
||||
|
var other = new CustomResource(); |
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = new Mock<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
store.Setup(store => store.GetNameAsync(resource, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync("urn:resource"); |
||||
|
|
||||
|
store.Setup(store => store.FindByNameAsync("urn:resource", It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(other); |
||||
|
|
||||
|
store.Setup(store => store.GetIdAsync(resource, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync("urn:resource-id"); |
||||
|
|
||||
|
store.Setup(store => store.GetIdAsync(other, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync("urn:other-resource-id"); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store.Object); |
||||
|
|
||||
|
// Act
|
||||
|
var results = await manager.ValidateAsync(resource).ToListAsync(); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.Contains(results, result => result.ErrorMessage == SR.GetResourceString(SR.ID2208)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task ValidateAsync_ReturnsNoErrorsForValidResource() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var resource = new CustomResource(); |
||||
|
var cache = Mock.Of<IOpenIddictResourceCache<CustomResource>>(); |
||||
|
var logger = Mock.Of<ILogger<OpenIddictResourceManager<CustomResource>>>(); |
||||
|
var options = Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>( |
||||
|
mock => mock.CurrentValue == new OpenIddictCoreOptions()); |
||||
|
var store = new Mock<IOpenIddictResourceStore<CustomResource>>(); |
||||
|
|
||||
|
store.Setup(store => store.GetNameAsync(resource, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync("urn:resource"); |
||||
|
|
||||
|
store.Setup(store => store.FindByNameAsync("urn:resource", It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync((CustomResource?) null); |
||||
|
|
||||
|
var manager = new OpenIddictResourceManager<CustomResource>(cache, logger, options, store.Object); |
||||
|
|
||||
|
// Act
|
||||
|
var results = await manager.ValidateAsync(resource).ToListAsync(); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.DoesNotContain(results, static result => result != ValidationResult.Success); |
||||
|
} |
||||
|
|
||||
|
public class CustomResource; |
||||
|
} |
||||
Loading…
Reference in new issue