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