Browse Source

Introduce a new resource entity and support dynamic resources

pull/2498/head
Kévin Chalet 3 weeks ago
parent
commit
eeb8deddbf
  1. 2
      sandbox/OpenIddict.Sandbox.AspNet.Server/Properties/launchSettings.json
  2. 2
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Program.cs
  3. 4
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Properties/launchSettings.json
  4. 2
      src/OpenIddict.Abstractions/Caches/IOpenIddictApplicationCache.cs
  5. 2
      src/OpenIddict.Abstractions/Caches/IOpenIddictAuthorizationCache.cs
  6. 62
      src/OpenIddict.Abstractions/Caches/IOpenIddictResourceCache.cs
  7. 2
      src/OpenIddict.Abstractions/Caches/IOpenIddictScopeCache.cs
  8. 2
      src/OpenIddict.Abstractions/Caches/IOpenIddictTokenCache.cs
  9. 40
      src/OpenIddict.Abstractions/Descriptors/OpenIddictResourceDescriptor.cs
  10. 367
      src/OpenIddict.Abstractions/Managers/IOpenIddictResourceManager.cs
  11. 74
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  12. 2
      src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs
  13. 2
      src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs
  14. 280
      src/OpenIddict.Abstractions/Stores/IOpenIddictResourceStore.cs
  15. 2
      src/OpenIddict.Abstractions/Stores/IOpenIddictScopeStore.cs
  16. 2
      src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs
  17. 2
      src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs
  18. 2
      src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs
  19. 258
      src/OpenIddict.Core/Caches/OpenIddictResourceCache.cs
  20. 2
      src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs
  21. 2
      src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs
  22. 2
      src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
  23. 8
      src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
  24. 936
      src/OpenIddict.Core/Managers/OpenIddictResourceManager.cs
  25. 8
      src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs
  26. 8
      src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
  27. 93
      src/OpenIddict.Core/OpenIddictCoreBuilder.cs
  28. 4
      src/OpenIddict.Core/OpenIddictCoreExtensions.cs
  29. 6
      src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs
  30. 6
      src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkAuthorization.cs
  31. 72
      src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkResource.cs
  32. 6
      src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkScope.cs
  33. 6
      src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkToken.cs
  34. 10
      src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkApplicationConfiguration.cs
  35. 10
      src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkAuthorizationConfiguration.cs
  36. 60
      src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkResourceConfiguration.cs
  37. 6
      src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkScopeConfiguration.cs
  38. 10
      src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkTokenConfiguration.cs
  39. 8
      src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs
  40. 2
      src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkExtensions.cs
  41. 6
      src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkHelpers.cs
  42. 6
      src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkApplicationStore.cs
  43. 12
      src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs
  44. 635
      src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkResourceStore.cs
  45. 11
      src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkScopeStore.cs
  46. 12
      src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs
  47. 6
      src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreApplication.cs
  48. 6
      src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreAuthorization.cs
  49. 72
      src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreResource.cs
  50. 6
      src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreScope.cs
  51. 6
      src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreToken.cs
  52. 10
      src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreApplicationConfiguration.cs
  53. 10
      src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreAuthorizationConfiguration.cs
  54. 60
      src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreResourceConfiguration.cs
  55. 6
      src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreScopeConfiguration.cs
  56. 10
      src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreTokenConfiguration.cs
  57. 7
      src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs
  58. 4
      src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreCustomizer.cs
  59. 2
      src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs
  60. 21
      src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs
  61. 6
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs
  62. 14
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs
  63. 649
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreResourceStore.cs
  64. 11
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreScopeStore.cs
  65. 12
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs
  66. 67
      src/OpenIddict.MongoDb.Models/OpenIddictMongoDbResource.cs
  67. 29
      src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs
  68. 2
      src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs
  69. 5
      src/OpenIddict.MongoDb/OpenIddictMongoDbOptions.cs
  70. 10
      src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbApplicationStore.cs
  71. 8
      src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs
  72. 437
      src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbResourceStore.cs
  73. 24
      src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbScopeStore.cs
  74. 8
      src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs
  75. 92
      src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs
  76. 46
      src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
  77. 2
      test/OpenIddict.Core.Tests/Caches/OpenIddictApplicationCacheTests.cs
  78. 2
      test/OpenIddict.Core.Tests/Caches/OpenIddictAuthorizationCacheTests.cs
  79. 361
      test/OpenIddict.Core.Tests/Caches/OpenIddictResourceCacheTests.cs
  80. 2
      test/OpenIddict.Core.Tests/Caches/OpenIddictScopeCacheTests.cs
  81. 2
      test/OpenIddict.Core.Tests/Caches/OpenIddictTokenCacheTests.cs
  82. 2
      test/OpenIddict.Core.Tests/Managers/OpenIddictApplicationManagerTests.cs
  83. 2
      test/OpenIddict.Core.Tests/Managers/OpenIddictAuthorizationManagerTests.cs
  84. 888
      test/OpenIddict.Core.Tests/Managers/OpenIddictResourceManagerTests.cs
  85. 6
      test/OpenIddict.Core.Tests/Managers/OpenIddictScopeManagerTests.cs
  86. 2
      test/OpenIddict.Core.Tests/Managers/OpenIddictTokenManagerTests.cs
  87. 96
      test/OpenIddict.Core.Tests/OpenIddictCoreBuilderTests.cs
  88. 20
      test/OpenIddict.Core.Tests/OpenIddictCoreExtensionsTests.cs
  89. 15
      test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkBuilderTests.cs
  90. 8
      test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkExtensionsTests.cs
  91. 15
      test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreBuilderTests.cs
  92. 8
      test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreExtensionsTests.cs
  93. 23
      test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreHelpersTests.cs
  94. 58
      test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbBuilderTests.cs
  95. 8
      test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbExtensionsTests.cs
  96. 153
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs
  97. 68
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs
  98. 25
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
  99. 4
      test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTests.cs

2
sandbox/OpenIddict.Sandbox.AspNet.Server/Properties/launchSettings.json

@ -10,7 +10,7 @@
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchBrowser": false,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}

2
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Program.cs

@ -234,7 +234,7 @@ builder.Services.AddOpenIddict()
options.EnableSelfSignedTlsClientAuthentication();
// Note: setting a static issuer is mandatory when using mTLS aliases to ensure it not
// Note: setting a static issuer is mandatory when using mTLS aliases to ensure it is not
// dynamically computed based on the request URI, as this would result in two different
// issuers being used (one pointing to the mTLS domain and one pointing to the regular one).
options.SetIssuer("https://localhost:44395/");

4
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Properties/launchSettings.json

@ -11,7 +11,7 @@
"profiles": {
"Kestrel": {
"commandName": "Project",
"launchBrowser": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:44395/",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
@ -20,7 +20,7 @@
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchBrowser": false,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}

2
src/OpenIddict.Abstractions/Caches/IOpenIddictApplicationCache.cs

@ -11,7 +11,7 @@ namespace OpenIddict.Abstractions;
/// <summary>
/// Provides methods allowing to cache applications after retrieving them from the store.
/// </summary>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TApplication">The type of the application entity.</typeparam>
public interface IOpenIddictApplicationCache<TApplication> where TApplication : class
{
/// <summary>

2
src/OpenIddict.Abstractions/Caches/IOpenIddictAuthorizationCache.cs

@ -11,7 +11,7 @@ namespace OpenIddict.Abstractions;
/// <summary>
/// Provides methods allowing to cache authorizations after retrieving them from the store.
/// </summary>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the authorization entity.</typeparam>
public interface IOpenIddictAuthorizationCache<TAuthorization> where TAuthorization : class
{
/// <summary>

62
src/OpenIddict.Abstractions/Caches/IOpenIddictResourceCache.cs

@ -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);
}

2
src/OpenIddict.Abstractions/Caches/IOpenIddictScopeCache.cs

@ -11,7 +11,7 @@ namespace OpenIddict.Abstractions;
/// <summary>
/// Provides methods allowing to cache scopes after retrieving them from the store.
/// </summary>
/// <typeparam name="TScope">The type of the Scope entity.</typeparam>
/// <typeparam name="TScope">The type of the scope entity.</typeparam>
public interface IOpenIddictScopeCache<TScope> where TScope : class
{
/// <summary>

2
src/OpenIddict.Abstractions/Caches/IOpenIddictTokenCache.cs

@ -9,7 +9,7 @@ namespace OpenIddict.Abstractions;
/// <summary>
/// Provides methods allowing to cache tokens after retrieving them from the store.
/// </summary>
/// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TToken">The type of the token entity.</typeparam>
public interface IOpenIddictTokenCache<TToken> where TToken : class
{
/// <summary>

40
src/OpenIddict.Abstractions/Descriptors/OpenIddictResourceDescriptor.cs

@ -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);
}

367
src/OpenIddict.Abstractions/Managers/IOpenIddictResourceManager.cs

@ -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);
}

74
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -810,10 +810,10 @@ To register the OWIN integration, use 'services.AddOpenIddict().AddValidation().
<value>The client secret hash cannot be set on the application entity.</value>
</data>
<data name="ID0207" xml:space="preserve">
<value>One or more validation error(s) occurred while trying to create a new application:</value>
<value>One or more validation error(s) occurred while trying to create a new entity:</value>
</data>
<data name="ID0208" xml:space="preserve">
<value>An error occurred while trying to create a new application.</value>
<value>An error occurred while trying to create a new entity.</value>
</data>
<data name="ID0209" xml:space="preserve">
<value>The client type cannot be null or empty.</value>
@ -834,7 +834,7 @@ To register the OWIN integration, use 'services.AddOpenIddict().AddValidation().
<value>Callback URIs must be valid absolute URIs.</value>
</data>
<data name="ID0215" xml:space="preserve">
<value>One or more validation error(s) occurred while trying to update an existing application:</value>
<value>One or more validation error(s) occurred while trying to update an existing entity:</value>
</data>
<data name="ID0216" xml:space="preserve">
<value>The secret cannot be null or empty.</value>
@ -845,33 +845,6 @@ To register the OWIN integration, use 'services.AddOpenIddict().AddValidation().
<data name="ID0218" xml:space="preserve">
<value>The client secret payload stored in the database is malformed, uses an unsupported version or doesn't meet the minimum security requirements and may have been tampered with.</value>
</data>
<data name="ID0219" xml:space="preserve">
<value>One or more validation error(s) occurred while trying to create a new authorization:</value>
</data>
<data name="ID0220" xml:space="preserve">
<value>An error occurred while trying to create a new authorization.</value>
</data>
<data name="ID0221" xml:space="preserve">
<value>One or more validation error(s) occurred while trying to update an existing authorization:</value>
</data>
<data name="ID0222" xml:space="preserve">
<value>One or more validation error(s) occurred while trying to create a new scope:</value>
</data>
<data name="ID0223" xml:space="preserve">
<value>An error occurred while trying to create a new scope.</value>
</data>
<data name="ID0224" xml:space="preserve">
<value>One or more validation error(s) occurred while trying to update an existing scope:</value>
</data>
<data name="ID0225" xml:space="preserve">
<value>One or more validation error(s) occurred while trying to create a new token:</value>
</data>
<data name="ID0226" xml:space="preserve">
<value>An error occurred while trying to create a new token.</value>
</data>
<data name="ID0227" xml:space="preserve">
<value>One or more validation error(s) occurred while trying to update an existing token:</value>
</data>
<data name="ID0232" xml:space="preserve">
<value>The specified type is invalid.</value>
</data>
@ -883,20 +856,12 @@ To register the OWIN integration, use 'services.AddOpenIddict().AddValidation().
To configure the OpenIddict Entity Framework 6.x stores to use a specific 'DbContext', use 'options.UseEntityFramework().UseDbContext&lt;TContext&gt;()'.</value>
</data>
<data name="ID0239" xml:space="preserve">
<value>The application was concurrently updated and cannot be persisted in its current state.
Reload the application from the database and retry the operation.</value>
<value>The entity was concurrently updated and cannot be persisted in its current state.
Reload the entity from the database and retry the operation.</value>
</data>
<data name="ID0240" xml:space="preserve">
<value>An error occurred while trying to create a new application instance.
Make sure that the application entity is not abstract and has a public parameterless constructor or create a custom application store that overrides 'InstantiateAsync()' to use a custom factory.</value>
</data>
<data name="ID0241" xml:space="preserve">
<value>The authorization was concurrently updated and cannot be persisted in its current state.
Reload the authorization from the database and retry the operation.</value>
</data>
<data name="ID0242" xml:space="preserve">
<value>An error occurred while trying to create a new authorization instance.
Make sure that the authorization entity is not abstract and has a public parameterless constructor or create a custom authorization store that overrides 'InstantiateAsync()' to use a custom factory.</value>
<value>An error occurred while trying to create a new entity instance.
Make sure that the entity is not abstract and has a public parameterless constructor or create a custom store that overrides 'InstantiateAsync()' to use a custom factory.</value>
</data>
<data name="ID0243" xml:space="preserve">
<value>An error occurred while pruning authorizations.</value>
@ -904,22 +869,6 @@ Make sure that the authorization entity is not abstract and has a public paramet
<data name="ID0244" xml:space="preserve">
<value>The application associated with the authorization cannot be found.</value>
</data>
<data name="ID0245" xml:space="preserve">
<value>The scope was concurrently updated and cannot be persisted in its current state.
Reload the scope from the database and retry the operation.</value>
</data>
<data name="ID0246" xml:space="preserve">
<value>An error occurred while trying to create a new scope instance.
Make sure that the scope entity is not abstract and has a public parameterless constructor or create a custom scope store that overrides 'InstantiateAsync()' to use a custom factory.</value>
</data>
<data name="ID0247" xml:space="preserve">
<value>The token was concurrently updated and cannot be persisted in its current state.
Reload the token from the database and retry the operation.</value>
</data>
<data name="ID0248" xml:space="preserve">
<value>An error occurred while trying to create a new token instance.
Make sure that the token entity is not abstract and has a public parameterless constructor or create a custom token store that overrides 'InstantiateAsync()' to use a custom factory.</value>
</data>
<data name="ID0249" xml:space="preserve">
<value>An error occurred while pruning tokens.</value>
</data>
@ -2490,6 +2439,15 @@ To use a custom policy relying on the system store, set 'OpenIddictServerOptions
<data name="ID2205" xml:space="preserve">
<value>The specified TLS client certificate is not allowed or valid for this operation.</value>
</data>
<data name="ID2206" xml:space="preserve">
<value>The resource name cannot be null or empty.</value>
</data>
<data name="ID2207" xml:space="preserve">
<value>The resource name must be a valid absolute URI.</value>
</data>
<data name="ID2208" xml:space="preserve">
<value>A resource with the same name already exists.</value>
</data>
<data name="ID4000" xml:space="preserve">
<value>The '{0}' parameter shouldn't be null or empty at this point.</value>
</data>

2
src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs

@ -15,7 +15,7 @@ namespace OpenIddict.Abstractions;
/// <summary>
/// Provides methods allowing to manage the applications stored in a database.
/// </summary>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TApplication">The type of the application entity.</typeparam>
public interface IOpenIddictApplicationStore<TApplication> where TApplication : class
{
/// <summary>

2
src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs

@ -12,7 +12,7 @@ namespace OpenIddict.Abstractions;
/// <summary>
/// Provides methods allowing to manage the authorizations stored in a database.
/// </summary>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the authorization entity.</typeparam>
public interface IOpenIddictAuthorizationStore<TAuthorization> where TAuthorization : class
{
/// <summary>

280
src/OpenIddict.Abstractions/Stores/IOpenIddictResourceStore.cs

@ -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);
}

2
src/OpenIddict.Abstractions/Stores/IOpenIddictScopeStore.cs

@ -13,7 +13,7 @@ namespace OpenIddict.Abstractions;
/// <summary>
/// Provides methods allowing to manage the scopes stored in a database.
/// </summary>
/// <typeparam name="TScope">The type of the Scope entity.</typeparam>
/// <typeparam name="TScope">The type of the scope entity.</typeparam>
public interface IOpenIddictScopeStore<TScope> where TScope : class
{
/// <summary>

2
src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs

@ -12,7 +12,7 @@ namespace OpenIddict.Abstractions;
/// <summary>
/// Provides methods allowing to manage the tokens stored in a database.
/// </summary>
/// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TToken">The type of the token entity.</typeparam>
public interface IOpenIddictTokenStore<TToken> where TToken : class
{
/// <summary>

2
src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs

@ -17,7 +17,7 @@ namespace OpenIddict.Core;
/// <summary>
/// Provides methods allowing to cache applications after retrieving them from the store.
/// </summary>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TApplication">The type of the application entity.</typeparam>
public sealed class OpenIddictApplicationCache<TApplication> : IOpenIddictApplicationCache<TApplication>, IDisposable where TApplication : class
{
private readonly MemoryCache _cache;

2
src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs

@ -16,7 +16,7 @@ namespace OpenIddict.Core;
/// <summary>
/// Provides methods allowing to cache authorizations after retrieving them from the store.
/// </summary>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the authorization entity.</typeparam>
public sealed class OpenIddictAuthorizationCache<TAuthorization> : IOpenIddictAuthorizationCache<TAuthorization>, IDisposable where TAuthorization : class
{
private readonly MemoryCache _cache;

258
src/OpenIddict.Core/Caches/OpenIddictResourceCache.cs

@ -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);
}
}

2
src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs

@ -16,7 +16,7 @@ namespace OpenIddict.Core;
/// <summary>
/// Provides methods allowing to cache scopes after retrieving them from the store.
/// </summary>
/// <typeparam name="TScope">The type of the Scope entity.</typeparam>
/// <typeparam name="TScope">The type of the scope entity.</typeparam>
public sealed class OpenIddictScopeCache<TScope> : IOpenIddictScopeCache<TScope>, IDisposable where TScope : class
{
private readonly MemoryCache _cache;

2
src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs

@ -16,7 +16,7 @@ namespace OpenIddict.Core;
/// <summary>
/// Provides methods allowing to cache tokens after retrieving them from the store.
/// </summary>
/// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TToken">The type of the token entity.</typeparam>
public sealed class OpenIddictTokenCache<TToken> : IOpenIddictTokenCache<TToken>, IDisposable where TToken : class
{
private readonly MemoryCache _cache;

2
src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs

@ -30,7 +30,7 @@ namespace OpenIddict.Core;
/// Applications that do not want to depend on a specific entity type can use the non-generic
/// <see cref="IOpenIddictApplicationManager"/> instead, for which the actual entity type is resolved at runtime.
/// </remarks>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TApplication">The type of the application entity.</typeparam>
public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplicationManager where TApplication : class
{
/// <summary>

8
src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs

@ -24,7 +24,7 @@ namespace OpenIddict.Core;
/// Applications that do not want to depend on a specific entity type can use the non-generic
/// <see cref="IOpenIddictAuthorizationManager"/> instead, for which the actual entity type is resolved at runtime.
/// </remarks>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the authorization entity.</typeparam>
public class OpenIddictAuthorizationManager<TAuthorization> : IOpenIddictAuthorizationManager where TAuthorization : class
{
/// <summary>
@ -138,7 +138,7 @@ public class OpenIddictAuthorizationManager<TAuthorization> : IOpenIddictAuthori
if (results.Any(result => result != ValidationResult.Success))
{
var builder = new StringBuilder();
builder.AppendLine(SR.GetResourceString(SR.ID0219));
builder.AppendLine(SR.GetResourceString(SR.ID0207));
builder.AppendLine();
foreach (var result in results)
@ -184,7 +184,7 @@ public class OpenIddictAuthorizationManager<TAuthorization> : IOpenIddictAuthori
ArgumentNullException.ThrowIfNull(descriptor);
var authorization = await Store.InstantiateAsync(cancellationToken) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0220));
throw new InvalidOperationException(SR.GetResourceString(SR.ID0208));
await PopulateAsync(authorization, descriptor, cancellationToken);
await CreateAsync(authorization, cancellationToken);
@ -858,7 +858,7 @@ public class OpenIddictAuthorizationManager<TAuthorization> : IOpenIddictAuthori
if (results.Any(result => result != ValidationResult.Success))
{
var builder = new StringBuilder();
builder.AppendLine(SR.GetResourceString(SR.ID0221));
builder.AppendLine(SR.GetResourceString(SR.ID0215));
builder.AppendLine();
foreach (var result in results)

936
src/OpenIddict.Core/Managers/OpenIddictResourceManager.cs

@ -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);
}

8
src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs

@ -23,7 +23,7 @@ namespace OpenIddict.Core;
/// Applications that do not want to depend on a specific entity type can use the non-generic
/// <see cref="IOpenIddictScopeManager"/> instead, for which the actual entity type is resolved at runtime.
/// </remarks>
/// <typeparam name="TScope">The type of the Scope entity.</typeparam>
/// <typeparam name="TScope">The type of the scope entity.</typeparam>
public class OpenIddictScopeManager<TScope> : IOpenIddictScopeManager where TScope : class
{
/// <summary>
@ -131,7 +131,7 @@ public class OpenIddictScopeManager<TScope> : IOpenIddictScopeManager where TSco
if (results.Any(result => result != ValidationResult.Success))
{
var builder = new StringBuilder();
builder.AppendLine(SR.GetResourceString(SR.ID0222));
builder.AppendLine(SR.GetResourceString(SR.ID0207));
builder.AppendLine();
foreach (var result in results)
@ -177,7 +177,7 @@ public class OpenIddictScopeManager<TScope> : IOpenIddictScopeManager where TSco
ArgumentNullException.ThrowIfNull(descriptor);
var scope = await Store.InstantiateAsync(cancellationToken) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0223));
throw new InvalidOperationException(SR.GetResourceString(SR.ID0208));
await PopulateAsync(scope, descriptor, cancellationToken);
await CreateAsync(scope, cancellationToken);
@ -792,7 +792,7 @@ public class OpenIddictScopeManager<TScope> : IOpenIddictScopeManager where TSco
if (results.Any(result => result != ValidationResult.Success))
{
var builder = new StringBuilder();
builder.AppendLine(SR.GetResourceString(SR.ID0224));
builder.AppendLine(SR.GetResourceString(SR.ID0215));
builder.AppendLine();
foreach (var result in results)

8
src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs

@ -24,7 +24,7 @@ namespace OpenIddict.Core;
/// Applications that do not want to depend on a specific entity type can use the non-generic
/// <see cref="IOpenIddictTokenManager"/> instead, for which the actual entity type is resolved at runtime.
/// </remarks>
/// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TToken">The type of the token entity.</typeparam>
public class OpenIddictTokenManager<TToken> : IOpenIddictTokenManager where TToken : class
{
/// <summary>
@ -146,7 +146,7 @@ public class OpenIddictTokenManager<TToken> : IOpenIddictTokenManager where TTok
if (results.Any(result => result != ValidationResult.Success))
{
var builder = new StringBuilder();
builder.AppendLine(SR.GetResourceString(SR.ID0225));
builder.AppendLine(SR.GetResourceString(SR.ID0207));
builder.AppendLine();
foreach (var result in results)
@ -192,7 +192,7 @@ public class OpenIddictTokenManager<TToken> : IOpenIddictTokenManager where TTok
ArgumentNullException.ThrowIfNull(descriptor);
var token = await Store.InstantiateAsync(cancellationToken) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0226));
throw new InvalidOperationException(SR.GetResourceString(SR.ID0208));
await PopulateAsync(token, descriptor, cancellationToken);
await CreateAsync(token, cancellationToken);
@ -1052,7 +1052,7 @@ public class OpenIddictTokenManager<TToken> : IOpenIddictTokenManager where TTok
if (results.Any(result => result != ValidationResult.Success))
{
var builder = new StringBuilder();
builder.AppendLine(SR.GetResourceString(SR.ID0227));
builder.AppendLine(SR.GetResourceString(SR.ID0215));
builder.AppendLine();
foreach (var result in results)

93
src/OpenIddict.Core/OpenIddictCoreBuilder.cs

@ -216,6 +216,86 @@ public sealed class OpenIddictCoreBuilder
return this;
}
/// <summary>
/// Replaces the resource manager by the specified type.
/// </summary>
/// <typeparam name="TResource">The type of the entity.</typeparam>
/// <typeparam name="TManager">The type of the manager.</typeparam>
/// <returns>The <see cref="OpenIddictCoreBuilder"/> instance.</returns>
public OpenIddictCoreBuilder ReplaceResourceManager<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResource,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TManager>()
where TResource : class
where TManager : OpenIddictResourceManager<TResource>
{
Services.Replace(ServiceDescriptor.Scoped<OpenIddictResourceManager<TResource>, TManager>());
return this;
}
/// <summary>
/// Replaces the resource manager by the specified type.
/// </summary>
/// <remarks>
/// Note: the specified type MUST be an open generic type definition containing exactly one generic argument.
/// </remarks>
/// <param name="type">The type of the manager.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/> instance.</returns>
public OpenIddictCoreBuilder ReplaceResourceManager(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type)
{
if (!type.IsGenericTypeDefinition || type.GetGenericArguments() is not { Length: 1 })
{
throw new ArgumentException(SR.GetResourceString(SR.ID0232), nameof(type));
}
Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictResourceManager<>), type));
return this;
}
/// <summary>
/// Replaces the resource store by the specified type.
/// </summary>
/// <typeparam name="TResource">The type of the entity.</typeparam>
/// <typeparam name="TStore">The type of the store.</typeparam>
/// <param name="lifetime">The lifetime of the store.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/> instance.</returns>
public OpenIddictCoreBuilder ReplaceResourceStore<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResource,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TStore>(
ServiceLifetime lifetime = ServiceLifetime.Scoped)
where TResource : class
where TStore : IOpenIddictResourceStore<TResource>
{
Services.Replace(ServiceDescriptor.Describe(typeof(IOpenIddictResourceStore<TResource>), typeof(TStore), lifetime));
return this;
}
/// <summary>
/// Replaces the resource store by the specified type.
/// </summary>
/// <remarks>
/// Note: the specified type MUST be an open generic type definition containing exactly one generic argument.
/// </remarks>
/// <param name="type">The type of the store.</param>
/// <param name="lifetime">The lifetime of the store.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/> instance.</returns>
public OpenIddictCoreBuilder ReplaceResourceStore(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type,
ServiceLifetime lifetime = ServiceLifetime.Scoped)
{
if (!type.IsGenericTypeDefinition || type.GetGenericArguments() is not { Length: 1 })
{
throw new ArgumentException(SR.GetResourceString(SR.ID0232), nameof(type));
}
Services.Replace(ServiceDescriptor.Describe(typeof(IOpenIddictResourceStore<>), type, lifetime));
return this;
}
/// <summary>
/// Replaces the scope manager by the specified type.
/// </summary>
@ -493,6 +573,19 @@ public sealed class OpenIddictCoreBuilder
return this;
}
/// <summary>
/// Configures OpenIddict to use the specified entity as the default resource entity.
/// </summary>
/// <returns>The <see cref="OpenIddictCoreBuilder"/> instance.</returns>
public OpenIddictCoreBuilder SetDefaultResourceEntity<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResource>() where TResource : class
{
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictResourceManager>(static provider =>
provider.GetRequiredService<OpenIddictResourceManager<TResource>>()));
return this;
}
/// <summary>
/// Configures OpenIddict to use the specified entity as the default scope entity.
/// </summary>

4
src/OpenIddict.Core/OpenIddictCoreExtensions.cs

@ -31,11 +31,13 @@ public static class OpenIddictCoreExtensions
builder.Services.TryAddScoped(typeof(IOpenIddictApplicationCache<>), typeof(OpenIddictApplicationCache<>));
builder.Services.TryAddScoped(typeof(IOpenIddictAuthorizationCache<>), typeof(OpenIddictAuthorizationCache<>));
builder.Services.TryAddScoped(typeof(IOpenIddictResourceCache<>), typeof(OpenIddictResourceCache<>));
builder.Services.TryAddScoped(typeof(IOpenIddictScopeCache<>), typeof(OpenIddictScopeCache<>));
builder.Services.TryAddScoped(typeof(IOpenIddictTokenCache<>), typeof(OpenIddictTokenCache<>));
builder.Services.TryAddScoped(typeof(OpenIddictApplicationManager<>));
builder.Services.TryAddScoped(typeof(OpenIddictAuthorizationManager<>));
builder.Services.TryAddScoped(typeof(OpenIddictResourceManager<>));
builder.Services.TryAddScoped(typeof(OpenIddictScopeManager<>));
builder.Services.TryAddScoped(typeof(OpenIddictTokenManager<>));
@ -46,6 +48,8 @@ public static class OpenIddictCoreExtensions
throw new InvalidOperationException(SR.GetResourceString(SR.ID0472)));
builder.Services.TryAddScoped<IOpenIddictAuthorizationManager>(static provider =>
throw new InvalidOperationException(SR.GetResourceString(SR.ID0472)));
builder.Services.TryAddScoped<IOpenIddictResourceManager>(static provider =>
throw new InvalidOperationException(SR.GetResourceString(SR.ID0472)));
builder.Services.TryAddScoped<IOpenIddictScopeManager>(static provider =>
throw new InvalidOperationException(SR.GetResourceString(SR.ID0472)));
builder.Services.TryAddScoped<IOpenIddictTokenManager>(static provider =>

6
src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs

@ -14,11 +14,7 @@ namespace OpenIddict.EntityFramework.Models;
/// </summary>
public class OpenIddictEntityFrameworkApplication : OpenIddictEntityFrameworkApplication<string, OpenIddictEntityFrameworkAuthorization, OpenIddictEntityFrameworkToken>
{
public OpenIddictEntityFrameworkApplication()
{
// Generate a new string identifier.
Id = Guid.NewGuid().ToString();
}
public OpenIddictEntityFrameworkApplication() => Id = Guid.NewGuid().ToString();
}
/// <summary>

6
src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkAuthorization.cs

@ -14,11 +14,7 @@ namespace OpenIddict.EntityFramework.Models;
/// </summary>
public class OpenIddictEntityFrameworkAuthorization : OpenIddictEntityFrameworkAuthorization<string, OpenIddictEntityFrameworkApplication, OpenIddictEntityFrameworkToken>
{
public OpenIddictEntityFrameworkAuthorization()
{
// Generate a new string identifier.
Id = Guid.NewGuid().ToString();
}
public OpenIddictEntityFrameworkAuthorization() => Id = Guid.NewGuid().ToString();
}
/// <summary>

72
src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkResource.cs

@ -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; }
}

6
src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkScope.cs

@ -14,11 +14,7 @@ namespace OpenIddict.EntityFramework.Models;
/// </summary>
public class OpenIddictEntityFrameworkScope : OpenIddictEntityFrameworkScope<string>
{
public OpenIddictEntityFrameworkScope()
{
// Generate a new string identifier.
Id = Guid.NewGuid().ToString();
}
public OpenIddictEntityFrameworkScope() => Id = Guid.NewGuid().ToString();
}
/// <summary>

6
src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkToken.cs

@ -14,11 +14,7 @@ namespace OpenIddict.EntityFramework.Models;
/// </summary>
public class OpenIddictEntityFrameworkToken : OpenIddictEntityFrameworkToken<string, OpenIddictEntityFrameworkApplication, OpenIddictEntityFrameworkAuthorization>
{
public OpenIddictEntityFrameworkToken()
{
// Generate a new string identifier.
Id = Guid.NewGuid().ToString();
}
public OpenIddictEntityFrameworkToken() => Id = Guid.NewGuid().ToString();
}
/// <summary>

10
src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkApplicationConfiguration.cs

@ -15,12 +15,12 @@ using OpenIddict.EntityFramework.Models;
namespace OpenIddict.EntityFramework;
/// <summary>
/// Defines a relational mapping for the Application entity.
/// Defines a relational mapping for the application entity.
/// </summary>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TKey">The type of the Key entity.</typeparam>
/// <typeparam name="TApplication">The type of the application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the authorization entity.</typeparam>
/// <typeparam name="TToken">The type of the token entity.</typeparam>
/// <typeparam name="TKey">The type of the primary key.</typeparam>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class OpenIddictEntityFrameworkApplicationConfiguration<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication,

10
src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkAuthorizationConfiguration.cs

@ -13,12 +13,12 @@ using OpenIddict.EntityFramework.Models;
namespace OpenIddict.EntityFramework;
/// <summary>
/// Defines a relational mapping for the Authorization entity.
/// Defines a relational mapping for the authorization entity.
/// </summary>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TKey">The type of the Key entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the authorization entity.</typeparam>
/// <typeparam name="TApplication">The type of the application entity.</typeparam>
/// <typeparam name="TToken">The type of the token entity.</typeparam>
/// <typeparam name="TKey">The type of the primary key.</typeparam>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class OpenIddictEntityFrameworkAuthorizationConfiguration<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization,

60
src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkResourceConfiguration.cs

@ -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");
}
}

6
src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkScopeConfiguration.cs

@ -15,10 +15,10 @@ using OpenIddict.EntityFramework.Models;
namespace OpenIddict.EntityFramework;
/// <summary>
/// Defines a relational mapping for the Scope entity.
/// Defines a relational mapping for the scope entity.
/// </summary>
/// <typeparam name="TScope">The type of the Scope entity.</typeparam>
/// <typeparam name="TKey">The type of the Key entity.</typeparam>
/// <typeparam name="TScope">The type of the scope entity.</typeparam>
/// <typeparam name="TKey">The type of the primary key.</typeparam>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class OpenIddictEntityFrameworkScopeConfiguration<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TScope,

10
src/OpenIddict.EntityFramework/Configurations/OpenIddictEntityFrameworkTokenConfiguration.cs

@ -15,12 +15,12 @@ using OpenIddict.EntityFramework.Models;
namespace OpenIddict.EntityFramework;
/// <summary>
/// Defines a relational mapping for the Token entity.
/// Defines a relational mapping for the token entity.
/// </summary>
/// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TKey">The type of the Key entity.</typeparam>
/// <typeparam name="TToken">The type of the token entity.</typeparam>
/// <typeparam name="TApplication">The type of the application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the authorization entity.</typeparam>
/// <typeparam name="TKey">The type of the primary key.</typeparam>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class OpenIddictEntityFrameworkTokenConfiguration<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken,

8
src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs

@ -65,11 +65,13 @@ public sealed class OpenIddictEntityFrameworkBuilder
public OpenIddictEntityFrameworkBuilder ReplaceDefaultEntities<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResource,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TScope,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey>()
where TApplication : OpenIddictEntityFrameworkApplication<TKey, TAuthorization, TToken>
where TAuthorization : OpenIddictEntityFrameworkAuthorization<TKey, TApplication, TToken>
where TResource : OpenIddictEntityFrameworkResource<TKey>
where TScope : OpenIddictEntityFrameworkScope<TKey>
where TToken : OpenIddictEntityFrameworkToken<TKey, TApplication, TAuthorization>
where TKey : notnull, IEquatable<TKey>
@ -79,7 +81,7 @@ public sealed class OpenIddictEntityFrameworkBuilder
//
// To ensure a better exception is thrown, a manual check is made here.
if (typeof(TApplication).IsGenericType || typeof(TAuthorization).IsGenericType ||
typeof(TScope).IsGenericType || typeof(TToken).IsGenericType)
typeof(TResource).IsGenericType || typeof(TScope).IsGenericType || typeof(TToken).IsGenericType)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0277));
}
@ -97,6 +99,8 @@ public sealed class OpenIddictEntityFrameworkBuilder
provider.GetRequiredService<OpenIddictApplicationManager<TApplication>>()));
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictAuthorizationManager>(static provider =>
provider.GetRequiredService<OpenIddictAuthorizationManager<TAuthorization>>()));
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictResourceManager>(static provider =>
provider.GetRequiredService<OpenIddictResourceManager<TResource>>()));
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictScopeManager>(static provider =>
provider.GetRequiredService<OpenIddictScopeManager<TScope>>()));
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictTokenManager>(static provider =>
@ -106,6 +110,8 @@ public sealed class OpenIddictEntityFrameworkBuilder
OpenIddictEntityFrameworkApplicationStore<TApplication, TAuthorization, TToken, TKey>>());
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictAuthorizationStore<TAuthorization>,
OpenIddictEntityFrameworkAuthorizationStore<TAuthorization, TApplication, TToken, TKey>>());
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictResourceStore<TResource>,
OpenIddictEntityFrameworkResourceStore<TResource, TKey>>());
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictScopeStore<TScope>,
OpenIddictEntityFrameworkScopeStore<TScope, TKey>>());
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictTokenStore<TToken>,

2
src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkExtensions.cs

@ -33,11 +33,13 @@ public static class OpenIddictEntityFrameworkExtensions
builder.SetDefaultApplicationEntity<OpenIddictEntityFrameworkApplication>()
.SetDefaultAuthorizationEntity<OpenIddictEntityFrameworkAuthorization>()
.SetDefaultResourceEntity<OpenIddictEntityFrameworkResource>()
.SetDefaultScopeEntity<OpenIddictEntityFrameworkScope>()
.SetDefaultTokenEntity<OpenIddictEntityFrameworkToken>();
builder.ReplaceApplicationStore<OpenIddictEntityFrameworkApplication, OpenIddictEntityFrameworkApplicationStore>()
.ReplaceAuthorizationStore<OpenIddictEntityFrameworkAuthorization, OpenIddictEntityFrameworkAuthorizationStore>()
.ReplaceResourceStore<OpenIddictEntityFrameworkResource, OpenIddictEntityFrameworkResourceStore>()
.ReplaceScopeStore<OpenIddictEntityFrameworkScope, OpenIddictEntityFrameworkScopeStore>()
.ReplaceTokenStore<OpenIddictEntityFrameworkToken, OpenIddictEntityFrameworkTokenStore>();

6
src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkHelpers.cs

@ -27,6 +27,7 @@ public static class OpenIddictEntityFrameworkHelpers
public static DbModelBuilder UseOpenIddict(this DbModelBuilder builder)
=> builder.UseOpenIddict<OpenIddictEntityFrameworkApplication,
OpenIddictEntityFrameworkAuthorization,
OpenIddictEntityFrameworkResource,
OpenIddictEntityFrameworkScope,
OpenIddictEntityFrameworkToken, string>();
@ -36,18 +37,20 @@ public static class OpenIddictEntityFrameworkHelpers
/// </summary>
/// <remarks>
/// Note: when using custom entities, the new entities MUST be registered by calling
/// <see cref="OpenIddictEntityFrameworkBuilder.ReplaceDefaultEntities{TApplication, TAuthorization, TScope, TToken, TKey}"/>.
/// <see cref="OpenIddictEntityFrameworkBuilder.ReplaceDefaultEntities{TApplication, TAuthorization, TResource, TScope, TToken, TKey}"/>.
/// </remarks>
/// <param name="builder">The builder used to configure the Entity Framework context.</param>
/// <returns>The Entity Framework context builder.</returns>
public static DbModelBuilder UseOpenIddict<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResource,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TScope,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey>(this DbModelBuilder builder)
where TApplication : OpenIddictEntityFrameworkApplication<TKey, TAuthorization, TToken>
where TAuthorization : OpenIddictEntityFrameworkAuthorization<TKey, TApplication, TToken>
where TResource : OpenIddictEntityFrameworkResource<TKey>
where TScope : OpenIddictEntityFrameworkScope<TKey>
where TToken : OpenIddictEntityFrameworkToken<TKey, TApplication, TAuthorization>
where TKey : notnull, IEquatable<TKey>
@ -57,6 +60,7 @@ public static class OpenIddictEntityFrameworkHelpers
builder.Configurations
.Add(new OpenIddictEntityFrameworkApplicationConfiguration<TApplication, TAuthorization, TToken, TKey>())
.Add(new OpenIddictEntityFrameworkAuthorizationConfiguration<TAuthorization, TApplication, TToken, TKey>())
.Add(new OpenIddictEntityFrameworkResourceConfiguration<TResource, TKey>())
.Add(new OpenIddictEntityFrameworkScopeConfiguration<TScope, TKey>())
.Add(new OpenIddictEntityFrameworkTokenConfiguration<TToken, TApplication, TAuthorization, TKey>());

6
src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkApplicationStore.cs

@ -42,9 +42,9 @@ public class OpenIddictEntityFrameworkApplicationStore :
/// <summary>
/// Provides methods allowing to manage the applications stored in a database.
/// </summary>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TApplication">The type of the application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the authorization entity.</typeparam>
/// <typeparam name="TToken">The type of the token entity.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public class OpenIddictEntityFrameworkApplicationStore<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication,

12
src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs

@ -40,9 +40,9 @@ public class OpenIddictEntityFrameworkAuthorizationStore :
/// <summary>
/// Provides methods allowing to manage the authorizations stored in a database.
/// </summary>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the authorization entity.</typeparam>
/// <typeparam name="TApplication">The type of the application entity.</typeparam>
/// <typeparam name="TToken">The type of the token entity.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public class OpenIddictEntityFrameworkAuthorizationStore<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization,
@ -153,7 +153,7 @@ public class OpenIddictEntityFrameworkAuthorizationStore<
context.Entry(token).State = EntityState.Unchanged;
}
throw new ConcurrencyException(SR.GetResourceString(SR.ID0241), exception);
throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception);
}
}
@ -430,7 +430,7 @@ public class OpenIddictEntityFrameworkAuthorizationStore<
catch (MemberAccessException exception)
{
return new(Task.FromException<TAuthorization>(
new InvalidOperationException(SR.GetResourceString(SR.ID0242), exception)));
new InvalidOperationException(SR.GetResourceString(SR.ID0240), exception)));
}
}
@ -882,7 +882,7 @@ public class OpenIddictEntityFrameworkAuthorizationStore<
// Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing.
context.Entry(authorization).State = EntityState.Unchanged;
throw new ConcurrencyException(SR.GetResourceString(SR.ID0241), exception);
throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception);
}
}

635
src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkResourceStore.cs

@ -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);
}
}
}

11
src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkScopeStore.cs

@ -38,7 +38,7 @@ public class OpenIddictEntityFrameworkScopeStore :
/// <summary>
/// Provides methods allowing to manage the scopes stored in a database.
/// </summary>
/// <typeparam name="TScope">The type of the Scope entity.</typeparam>
/// <typeparam name="TScope">The type of the scope entity.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public class OpenIddictEntityFrameworkScopeStore<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TScope,
@ -122,7 +122,7 @@ public class OpenIddictEntityFrameworkScopeStore<
// Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing.
context.Entry(scope).State = EntityState.Unchanged;
throw new ConcurrencyException(SR.GetResourceString(SR.ID0245), exception);
throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception);
}
}
@ -194,8 +194,7 @@ public class OpenIddictEntityFrameworkScopeStore<
}
/// <inheritdoc/>
public virtual IAsyncEnumerable<TScope> FindByResourceAsync(
string resource, CancellationToken cancellationToken)
public virtual IAsyncEnumerable<TScope> FindByResourceAsync(string resource, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(resource);
@ -427,7 +426,7 @@ public class OpenIddictEntityFrameworkScopeStore<
catch (MemberAccessException exception)
{
return new(Task.FromException<TScope>(
new InvalidOperationException(SR.GetResourceString(SR.ID0246), exception)));
new InvalidOperationException(SR.GetResourceString(SR.ID0240), exception)));
}
}
@ -672,7 +671,7 @@ public class OpenIddictEntityFrameworkScopeStore<
// Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing.
context.Entry(scope).State = EntityState.Unchanged;
throw new ConcurrencyException(SR.GetResourceString(SR.ID0245), exception);
throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception);
}
}

12
src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs

@ -40,9 +40,9 @@ public class OpenIddictEntityFrameworkTokenStore :
/// <summary>
/// Provides methods allowing to manage the tokens stored in a database.
/// </summary>
/// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TToken">The type of the token entity.</typeparam>
/// <typeparam name="TApplication">The type of the application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the authorization entity.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public class OpenIddictEntityFrameworkTokenStore<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken,
@ -130,7 +130,7 @@ public class OpenIddictEntityFrameworkTokenStore<
// Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing.
context.Entry(token).State = EntityState.Unchanged;
throw new ConcurrencyException(SR.GetResourceString(SR.ID0247), exception);
throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception);
}
}
@ -478,7 +478,7 @@ public class OpenIddictEntityFrameworkTokenStore<
catch (MemberAccessException exception)
{
return new(Task.FromException<TToken>(
new InvalidOperationException(SR.GetResourceString(SR.ID0248), exception)));
new InvalidOperationException(SR.GetResourceString(SR.ID0240), exception)));
}
}
@ -1016,7 +1016,7 @@ public class OpenIddictEntityFrameworkTokenStore<
// Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing.
context.Entry(token).State = EntityState.Unchanged;
throw new ConcurrencyException(SR.GetResourceString(SR.ID0247), exception);
throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception);
}
}

6
src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreApplication.cs

@ -14,11 +14,7 @@ namespace OpenIddict.EntityFrameworkCore.Models;
/// </summary>
public class OpenIddictEntityFrameworkCoreApplication : OpenIddictEntityFrameworkCoreApplication<string, OpenIddictEntityFrameworkCoreAuthorization, OpenIddictEntityFrameworkCoreToken>
{
public OpenIddictEntityFrameworkCoreApplication()
{
// Generate a new string identifier.
Id = Guid.NewGuid().ToString();
}
public OpenIddictEntityFrameworkCoreApplication() => Id = Guid.NewGuid().ToString();
}
/// <summary>

6
src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreAuthorization.cs

@ -14,11 +14,7 @@ namespace OpenIddict.EntityFrameworkCore.Models;
/// </summary>
public class OpenIddictEntityFrameworkCoreAuthorization : OpenIddictEntityFrameworkCoreAuthorization<string, OpenIddictEntityFrameworkCoreApplication, OpenIddictEntityFrameworkCoreToken>
{
public OpenIddictEntityFrameworkCoreAuthorization()
{
// Generate a new string identifier.
Id = Guid.NewGuid().ToString();
}
public OpenIddictEntityFrameworkCoreAuthorization() => Id = Guid.NewGuid().ToString();
}
/// <summary>

72
src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreResource.cs

@ -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; }
}

6
src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreScope.cs

@ -14,11 +14,7 @@ namespace OpenIddict.EntityFrameworkCore.Models;
/// </summary>
public class OpenIddictEntityFrameworkCoreScope : OpenIddictEntityFrameworkCoreScope<string>
{
public OpenIddictEntityFrameworkCoreScope()
{
// Generate a new string identifier.
Id = Guid.NewGuid().ToString();
}
public OpenIddictEntityFrameworkCoreScope() => Id = Guid.NewGuid().ToString();
}
/// <summary>

6
src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreToken.cs

@ -14,11 +14,7 @@ namespace OpenIddict.EntityFrameworkCore.Models;
/// </summary>
public class OpenIddictEntityFrameworkCoreToken : OpenIddictEntityFrameworkCoreToken<string, OpenIddictEntityFrameworkCoreApplication, OpenIddictEntityFrameworkCoreAuthorization>
{
public OpenIddictEntityFrameworkCoreToken()
{
// Generate a new string identifier.
Id = Guid.NewGuid().ToString();
}
public OpenIddictEntityFrameworkCoreToken() => Id = Guid.NewGuid().ToString();
}
/// <summary>

10
src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreApplicationConfiguration.cs

@ -12,12 +12,12 @@ using OpenIddict.EntityFrameworkCore.Models;
namespace OpenIddict.EntityFrameworkCore;
/// <summary>
/// Defines a relational mapping for the Application entity.
/// Defines a relational mapping for the application entity.
/// </summary>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TKey">The type of the Key entity.</typeparam>
/// <typeparam name="TApplication">The type of the application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the authorization entity.</typeparam>
/// <typeparam name="TToken">The type of the token entity.</typeparam>
/// <typeparam name="TKey">The type of the primary key.</typeparam>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class OpenIddictEntityFrameworkCoreApplicationConfiguration<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication,

10
src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreAuthorizationConfiguration.cs

@ -12,12 +12,12 @@ using OpenIddict.EntityFrameworkCore.Models;
namespace OpenIddict.EntityFrameworkCore;
/// <summary>
/// Defines a relational mapping for the Authorization entity.
/// Defines a relational mapping for the authorization entity.
/// </summary>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TKey">The type of the Key entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the authorization entity.</typeparam>
/// <typeparam name="TApplication">The type of the application entity.</typeparam>
/// <typeparam name="TToken">The type of the token entity.</typeparam>
/// <typeparam name="TKey">The type of the primary key.</typeparam>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class OpenIddictEntityFrameworkCoreAuthorizationConfiguration<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization,

60
src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreResourceConfiguration.cs

@ -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");
}
}

6
src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreScopeConfiguration.cs

@ -12,10 +12,10 @@ using OpenIddict.EntityFrameworkCore.Models;
namespace OpenIddict.EntityFrameworkCore;
/// <summary>
/// Defines a relational mapping for the Scope entity.
/// Defines a relational mapping for the scope entity.
/// </summary>
/// <typeparam name="TScope">The type of the Scope entity.</typeparam>
/// <typeparam name="TKey">The type of the Key entity.</typeparam>
/// <typeparam name="TScope">The type of the scope entity.</typeparam>
/// <typeparam name="TKey">The type of the primary key.</typeparam>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class OpenIddictEntityFrameworkCoreScopeConfiguration<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TScope,

10
src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictEntityFrameworkCoreTokenConfiguration.cs

@ -12,12 +12,12 @@ using OpenIddict.EntityFrameworkCore.Models;
namespace OpenIddict.EntityFrameworkCore;
/// <summary>
/// Defines a relational mapping for the Token entity.
/// Defines a relational mapping for the token entity.
/// </summary>
/// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TKey">The type of the Key entity.</typeparam>
/// <typeparam name="TToken">The type of the token entity.</typeparam>
/// <typeparam name="TApplication">The type of the application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the authorization entity.</typeparam>
/// <typeparam name="TKey">The type of the primary key.</typeparam>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class OpenIddictEntityFrameworkCoreTokenConfiguration<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken,

7
src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs

@ -77,6 +77,7 @@ public sealed class OpenIddictEntityFrameworkCoreBuilder
where TKey : notnull, IEquatable<TKey>
=> ReplaceDefaultEntities<OpenIddictEntityFrameworkCoreApplication<TKey>,
OpenIddictEntityFrameworkCoreAuthorization<TKey>,
OpenIddictEntityFrameworkCoreResource<TKey>,
OpenIddictEntityFrameworkCoreScope<TKey>,
OpenIddictEntityFrameworkCoreToken<TKey>, TKey>();
@ -88,11 +89,13 @@ public sealed class OpenIddictEntityFrameworkCoreBuilder
public OpenIddictEntityFrameworkCoreBuilder ReplaceDefaultEntities<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResource,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TScope,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey>()
where TApplication : OpenIddictEntityFrameworkCoreApplication<TKey, TAuthorization, TToken>
where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization<TKey, TApplication, TToken>
where TResource : OpenIddictEntityFrameworkCoreResource<TKey>
where TScope : OpenIddictEntityFrameworkCoreScope<TKey>
where TToken : OpenIddictEntityFrameworkCoreToken<TKey, TApplication, TAuthorization>
where TKey : notnull, IEquatable<TKey>
@ -110,6 +113,8 @@ public sealed class OpenIddictEntityFrameworkCoreBuilder
provider.GetRequiredService<OpenIddictApplicationManager<TApplication>>()));
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictAuthorizationManager>(static provider =>
provider.GetRequiredService<OpenIddictAuthorizationManager<TAuthorization>>()));
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictResourceManager>(static provider =>
provider.GetRequiredService<OpenIddictResourceManager<TResource>>()));
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictScopeManager>(static provider =>
provider.GetRequiredService<OpenIddictScopeManager<TScope>>()));
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictTokenManager>(static provider =>
@ -119,6 +124,8 @@ public sealed class OpenIddictEntityFrameworkCoreBuilder
OpenIddictEntityFrameworkCoreApplicationStore<TApplication, TAuthorization, TToken, TKey>>());
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictAuthorizationStore<TAuthorization>,
OpenIddictEntityFrameworkCoreAuthorizationStore<TAuthorization, TApplication, TToken, TKey>>());
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictResourceStore<TResource>,
OpenIddictEntityFrameworkCoreResourceStore<TResource, TKey>>());
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictScopeStore<TScope>,
OpenIddictEntityFrameworkCoreScopeStore<TScope, TKey>>());
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictTokenStore<TToken>,

4
src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreCustomizer.cs

@ -18,11 +18,13 @@ namespace OpenIddict.EntityFrameworkCore;
public sealed class OpenIddictEntityFrameworkCoreCustomizer<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResource,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TScope,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey> : RelationalModelCustomizer
where TApplication : OpenIddictEntityFrameworkCoreApplication<TKey, TAuthorization, TToken>
where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization<TKey, TApplication, TToken>
where TResource : OpenIddictEntityFrameworkCoreResource<TKey>
where TScope : OpenIddictEntityFrameworkCoreScope<TKey>
where TToken : OpenIddictEntityFrameworkCoreToken<TKey, TApplication, TAuthorization>
where TKey : notnull, IEquatable<TKey>
@ -39,7 +41,7 @@ public sealed class OpenIddictEntityFrameworkCoreCustomizer<
ArgumentNullException.ThrowIfNull(context);
// Register the OpenIddict entity sets.
modelBuilder.UseOpenIddict<TApplication, TAuthorization, TScope, TToken, TKey>();
modelBuilder.UseOpenIddict<TApplication, TAuthorization, TResource, TScope, TToken, TKey>();
base.Customize(modelBuilder, context);
}

2
src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs

@ -33,11 +33,13 @@ public static class OpenIddictEntityFrameworkCoreExtensions
builder.SetDefaultApplicationEntity<OpenIddictEntityFrameworkCoreApplication>()
.SetDefaultAuthorizationEntity<OpenIddictEntityFrameworkCoreAuthorization>()
.SetDefaultResourceEntity<OpenIddictEntityFrameworkCoreResource>()
.SetDefaultScopeEntity<OpenIddictEntityFrameworkCoreScope>()
.SetDefaultTokenEntity<OpenIddictEntityFrameworkCoreToken>();
builder.ReplaceApplicationStore<OpenIddictEntityFrameworkCoreApplication, OpenIddictEntityFrameworkCoreApplicationStore>()
.ReplaceAuthorizationStore<OpenIddictEntityFrameworkCoreAuthorization, OpenIddictEntityFrameworkCoreAuthorizationStore>()
.ReplaceResourceStore<OpenIddictEntityFrameworkCoreResource, OpenIddictEntityFrameworkCoreResourceStore>()
.ReplaceScopeStore<OpenIddictEntityFrameworkCoreScope, OpenIddictEntityFrameworkCoreScopeStore>()
.ReplaceTokenStore<OpenIddictEntityFrameworkCoreToken, OpenIddictEntityFrameworkCoreTokenStore>();

21
src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs

@ -26,6 +26,7 @@ public static class OpenIddictEntityFrameworkCoreHelpers
public static DbContextOptionsBuilder UseOpenIddict(this DbContextOptionsBuilder builder)
=> builder.UseOpenIddict<OpenIddictEntityFrameworkCoreApplication,
OpenIddictEntityFrameworkCoreAuthorization,
OpenIddictEntityFrameworkCoreResource,
OpenIddictEntityFrameworkCoreScope,
OpenIddictEntityFrameworkCoreToken, string>();
@ -57,6 +58,7 @@ public static class OpenIddictEntityFrameworkCoreHelpers
where TKey : notnull, IEquatable<TKey>
=> builder.UseOpenIddict<OpenIddictEntityFrameworkCoreApplication<TKey>,
OpenIddictEntityFrameworkCoreAuthorization<TKey>,
OpenIddictEntityFrameworkCoreResource<TKey>,
OpenIddictEntityFrameworkCoreScope<TKey>,
OpenIddictEntityFrameworkCoreToken<TKey>, TKey>();
@ -86,19 +88,21 @@ public static class OpenIddictEntityFrameworkCoreHelpers
/// </summary>
/// <remarks>
/// Note: when using custom entities, the new entities MUST be registered by calling
/// <see cref="OpenIddictEntityFrameworkCoreBuilder.ReplaceDefaultEntities{TApplication, TAuthorization, TScope, TToken, TKey}"/>.
/// <see cref="OpenIddictEntityFrameworkCoreBuilder.ReplaceDefaultEntities{TApplication, TAuthorization, TResource, TScope, TToken, TKey}"/>.
/// </remarks>
/// <param name="builder">The builder used to configure the Entity Framework Core context.</param>
/// <returns>The Entity Framework Core context builder.</returns>
public static DbContextOptionsBuilder UseOpenIddict<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResource,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TScope,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey>(
this DbContextOptionsBuilder builder)
where TApplication : OpenIddictEntityFrameworkCoreApplication<TKey, TAuthorization, TToken>
where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization<TKey, TApplication, TToken>
where TResource : OpenIddictEntityFrameworkCoreResource<TKey>
where TScope : OpenIddictEntityFrameworkCoreScope<TKey>
where TToken : OpenIddictEntityFrameworkCoreToken<TKey, TApplication, TAuthorization>
where TKey : notnull, IEquatable<TKey>
@ -106,7 +110,7 @@ public static class OpenIddictEntityFrameworkCoreHelpers
ArgumentNullException.ThrowIfNull(builder);
return ReplaceService<IModelCustomizer,
OpenIddictEntityFrameworkCoreCustomizer<TApplication, TAuthorization, TScope, TToken, TKey>>(builder);
OpenIddictEntityFrameworkCoreCustomizer<TApplication, TAuthorization, TResource, TScope, TToken, TKey>>(builder);
static DbContextOptionsBuilder ReplaceService<
TService,
@ -121,13 +125,14 @@ public static class OpenIddictEntityFrameworkCoreHelpers
/// </summary>
/// <remarks>
/// Note: when using custom entities, the new entities MUST be registered by calling
/// <see cref="OpenIddictEntityFrameworkCoreBuilder.ReplaceDefaultEntities{TApplication, TAuthorization, TScope, TToken, TKey}"/>.
/// <see cref="OpenIddictEntityFrameworkCoreBuilder.ReplaceDefaultEntities{TApplication, TAuthorization, TResource, TScope, TToken, TKey}"/>.
/// </remarks>
/// <param name="builder">The builder used to configure the Entity Framework Core context.</param>
/// <returns>The Entity Framework Core context builder.</returns>
public static DbContextOptionsBuilder<TContext> UseOpenIddict<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResource,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TScope,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey,
@ -135,12 +140,13 @@ public static class OpenIddictEntityFrameworkCoreHelpers
this DbContextOptionsBuilder<TContext> builder)
where TApplication : OpenIddictEntityFrameworkCoreApplication<TKey, TAuthorization, TToken>
where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization<TKey, TApplication, TToken>
where TResource : OpenIddictEntityFrameworkCoreResource<TKey>
where TScope : OpenIddictEntityFrameworkCoreScope<TKey>
where TToken : OpenIddictEntityFrameworkCoreToken<TKey, TApplication, TAuthorization>
where TKey : notnull, IEquatable<TKey>
where TContext : DbContext
{
builder.UseOpenIddict<TApplication, TAuthorization, TScope, TToken, TKey>();
builder.UseOpenIddict<TApplication, TAuthorization, TResource, TScope, TToken, TKey>();
return builder;
}
@ -153,6 +159,7 @@ public static class OpenIddictEntityFrameworkCoreHelpers
public static ModelBuilder UseOpenIddict(this ModelBuilder builder)
=> builder.UseOpenIddict<OpenIddictEntityFrameworkCoreApplication,
OpenIddictEntityFrameworkCoreAuthorization,
OpenIddictEntityFrameworkCoreResource,
OpenIddictEntityFrameworkCoreScope,
OpenIddictEntityFrameworkCoreToken, string>();
@ -170,6 +177,7 @@ public static class OpenIddictEntityFrameworkCoreHelpers
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey>(this ModelBuilder builder) where TKey : notnull, IEquatable<TKey>
=> builder.UseOpenIddict<OpenIddictEntityFrameworkCoreApplication<TKey>,
OpenIddictEntityFrameworkCoreAuthorization<TKey>,
OpenIddictEntityFrameworkCoreResource<TKey>,
OpenIddictEntityFrameworkCoreScope<TKey>,
OpenIddictEntityFrameworkCoreToken<TKey>, TKey>();
@ -179,18 +187,20 @@ public static class OpenIddictEntityFrameworkCoreHelpers
/// </summary>
/// <remarks>
/// Note: when using custom entities, the new entities MUST be registered by calling
/// <see cref="OpenIddictEntityFrameworkCoreBuilder.ReplaceDefaultEntities{TApplication, TAuthorization, TScope, TToken, TKey}"/>.
/// <see cref="OpenIddictEntityFrameworkCoreBuilder.ReplaceDefaultEntities{TApplication, TAuthorization, TResource, TScope, TToken, TKey}"/>.
/// </remarks>
/// <param name="builder">The builder used to configure the Entity Framework Core context.</param>
/// <returns>The Entity Framework Core context builder.</returns>
public static ModelBuilder UseOpenIddict<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResource,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TScope,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TKey>(this ModelBuilder builder)
where TApplication : OpenIddictEntityFrameworkCoreApplication<TKey, TAuthorization, TToken>
where TAuthorization : OpenIddictEntityFrameworkCoreAuthorization<TKey, TApplication, TToken>
where TResource : OpenIddictEntityFrameworkCoreResource<TKey>
where TScope : OpenIddictEntityFrameworkCoreScope<TKey>
where TToken : OpenIddictEntityFrameworkCoreToken<TKey, TApplication, TAuthorization>
where TKey : notnull, IEquatable<TKey>
@ -200,6 +210,7 @@ public static class OpenIddictEntityFrameworkCoreHelpers
return builder
.ApplyConfiguration(new OpenIddictEntityFrameworkCoreApplicationConfiguration<TApplication, TAuthorization, TToken, TKey>())
.ApplyConfiguration(new OpenIddictEntityFrameworkCoreAuthorizationConfiguration<TAuthorization, TApplication, TToken, TKey>())
.ApplyConfiguration(new OpenIddictEntityFrameworkCoreResourceConfiguration<TResource, TKey>())
.ApplyConfiguration(new OpenIddictEntityFrameworkCoreScopeConfiguration<TScope, TKey>())
.ApplyConfiguration(new OpenIddictEntityFrameworkCoreTokenConfiguration<TToken, TApplication, TAuthorization, TKey>());
}

6
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs

@ -61,9 +61,9 @@ public class OpenIddictEntityFrameworkCoreApplicationStore<
/// <summary>
/// Provides methods allowing to manage the applications stored in a database.
/// </summary>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TApplication">The type of the application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the authorization entity.</typeparam>
/// <typeparam name="TToken">The type of the token entity.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public class OpenIddictEntityFrameworkCoreApplicationStore<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication,

14
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs

@ -59,9 +59,9 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore<
/// <summary>
/// Provides methods allowing to manage the authorizations stored in a database.
/// </summary>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the authorization entity.</typeparam>
/// <typeparam name="TApplication">The type of the application entity.</typeparam>
/// <typeparam name="TToken">The type of the token entity.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public class OpenIddictEntityFrameworkCoreAuthorizationStore<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization,
@ -168,7 +168,7 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore<
// Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing.
context.Entry(authorization).State = EntityState.Unchanged;
throw new ConcurrencyException(SR.GetResourceString(SR.ID0241), exception);
throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception);
}
});
}
@ -219,7 +219,7 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore<
context.Entry(token).State = EntityState.Unchanged;
}
throw new ConcurrencyException(SR.GetResourceString(SR.ID0241), exception);
throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception);
}
});
}
@ -514,7 +514,7 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore<
catch (MemberAccessException exception)
{
return new(Task.FromException<TAuthorization>(
new InvalidOperationException(SR.GetResourceString(SR.ID0242), exception)));
new InvalidOperationException(SR.GetResourceString(SR.ID0240), exception)));
}
}
@ -1062,7 +1062,7 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore<
// Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing.
context.Entry(authorization).State = EntityState.Unchanged;
throw new ConcurrencyException(SR.GetResourceString(SR.ID0241), exception);
throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception);
}
}

649
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreResourceStore.cs

@ -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);
}
}
}

11
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreScopeStore.cs

@ -53,7 +53,7 @@ public class OpenIddictEntityFrameworkCoreScopeStore<
/// <summary>
/// Provides methods allowing to manage the scopes stored in a database.
/// </summary>
/// <typeparam name="TScope">The type of the Scope entity.</typeparam>
/// <typeparam name="TScope">The type of the scope entity.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public class OpenIddictEntityFrameworkCoreScopeStore<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TScope,
@ -137,7 +137,7 @@ public class OpenIddictEntityFrameworkCoreScopeStore<
// Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing.
context.Entry(scope).State = EntityState.Unchanged;
throw new ConcurrencyException(SR.GetResourceString(SR.ID0245), exception);
throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception);
}
}
@ -208,8 +208,7 @@ public class OpenIddictEntityFrameworkCoreScopeStore<
}
/// <inheritdoc/>
public virtual IAsyncEnumerable<TScope> FindByResourceAsync(
string resource, CancellationToken cancellationToken)
public virtual IAsyncEnumerable<TScope> FindByResourceAsync(string resource, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(resource);
@ -441,7 +440,7 @@ public class OpenIddictEntityFrameworkCoreScopeStore<
catch (MemberAccessException exception)
{
return new(Task.FromException<TScope>(
new InvalidOperationException(SR.GetResourceString(SR.ID0246), exception)));
new InvalidOperationException(SR.GetResourceString(SR.ID0240), exception)));
}
}
@ -686,7 +685,7 @@ public class OpenIddictEntityFrameworkCoreScopeStore<
// Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing.
context.Entry(scope).State = EntityState.Unchanged;
throw new ConcurrencyException(SR.GetResourceString(SR.ID0245), exception);
throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception);
}
}

12
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs

@ -59,9 +59,9 @@ public class OpenIddictEntityFrameworkCoreTokenStore<
/// <summary>
/// Provides methods allowing to manage the tokens stored in a database.
/// </summary>
/// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TToken">The type of the token entity.</typeparam>
/// <typeparam name="TApplication">The type of the application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the authorization entity.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public class OpenIddictEntityFrameworkCoreTokenStore<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken,
@ -149,7 +149,7 @@ public class OpenIddictEntityFrameworkCoreTokenStore<
// Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing.
context.Entry(token).State = EntityState.Unchanged;
throw new ConcurrencyException(SR.GetResourceString(SR.ID0247), exception);
throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception);
}
}
@ -525,7 +525,7 @@ public class OpenIddictEntityFrameworkCoreTokenStore<
catch (MemberAccessException exception)
{
return new(Task.FromException<TToken>(
new InvalidOperationException(SR.GetResourceString(SR.ID0248), exception)));
new InvalidOperationException(SR.GetResourceString(SR.ID0240), exception)));
}
}
@ -1175,7 +1175,7 @@ public class OpenIddictEntityFrameworkCoreTokenStore<
// Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing.
context.Entry(token).State = EntityState.Unchanged;
throw new ConcurrencyException(SR.GetResourceString(SR.ID0247), exception);
throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception);
}
}

67
src/OpenIddict.MongoDb.Models/OpenIddictMongoDbResource.cs

@ -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; }
}

29
src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs

@ -91,6 +91,23 @@ public sealed class OpenIddictMongoDbBuilder
return this;
}
/// <summary>
/// Configures OpenIddict to use the specified entity as the default resource entity.
/// </summary>
/// <returns>The <see cref="OpenIddictMongoDbBuilder"/> instance.</returns>
public OpenIddictMongoDbBuilder ReplaceDefaultResourceEntity<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResource>()
where TResource : OpenIddictMongoDbResource
{
Services.Replace(ServiceDescriptor.Scoped<IOpenIddictResourceManager>(static provider =>
provider.GetRequiredService<OpenIddictResourceManager<TResource>>()));
Services.Replace(ServiceDescriptor.Scoped<
IOpenIddictResourceStore<TResource>, OpenIddictMongoDbResourceStore<TResource>>());
return this;
}
/// <summary>
/// Configures OpenIddict to use the specified entity as the default scope entity.
/// </summary>
@ -149,6 +166,18 @@ public sealed class OpenIddictMongoDbBuilder
return Configure(options => options.AuthorizationsCollectionName = name);
}
/// <summary>
/// Replaces the default resources collection name (by default, openiddict.resources).
/// </summary>
/// <param name="name">The collection name</param>
/// <returns>The <see cref="OpenIddictMongoDbBuilder"/> instance.</returns>
public OpenIddictMongoDbBuilder SetResourcesCollectionName(string name)
{
ArgumentException.ThrowIfNullOrEmpty(name);
return Configure(options => options.ResourcesCollectionName = name);
}
/// <summary>
/// Replaces the default scopes collection name (by default, openiddict.scopes).
/// </summary>

2
src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs

@ -32,6 +32,7 @@ public static class OpenIddictMongoDbExtensions
builder.SetDefaultApplicationEntity<OpenIddictMongoDbApplication>()
.SetDefaultAuthorizationEntity<OpenIddictMongoDbAuthorization>()
.SetDefaultResourceEntity<OpenIddictMongoDbResource>()
.SetDefaultScopeEntity<OpenIddictMongoDbScope>()
.SetDefaultTokenEntity<OpenIddictMongoDbToken>();
@ -39,6 +40,7 @@ public static class OpenIddictMongoDbExtensions
// be safely registered as singleton services and shared/reused across requests.
builder.ReplaceApplicationStore<OpenIddictMongoDbApplication, OpenIddictMongoDbApplicationStore>(ServiceLifetime.Singleton)
.ReplaceAuthorizationStore<OpenIddictMongoDbAuthorization, OpenIddictMongoDbAuthorizationStore>(ServiceLifetime.Singleton)
.ReplaceResourceStore<OpenIddictMongoDbResource, OpenIddictMongoDbResourceStore>(ServiceLifetime.Singleton)
.ReplaceScopeStore<OpenIddictMongoDbScope, OpenIddictMongoDbScopeStore>(ServiceLifetime.Singleton)
.ReplaceTokenStore<OpenIddictMongoDbToken, OpenIddictMongoDbTokenStore>(ServiceLifetime.Singleton);

5
src/OpenIddict.MongoDb/OpenIddictMongoDbOptions.cs

@ -27,6 +27,11 @@ public sealed class OpenIddictMongoDbOptions
/// </summary>
public IMongoDatabase? Database { get; set; }
/// <summary>
/// Gets or sets the name of the resources collection (by default, openiddict.resources).
/// </summary>
public string ResourcesCollectionName { get; set; } = "openiddict.resources";
/// <summary>
/// Gets or sets the name of the scopes collection (by default, openiddict.scopes).
/// </summary>

10
src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbApplicationStore.cs

@ -34,7 +34,7 @@ public class OpenIddictMongoDbApplicationStore : OpenIddictMongoDbApplicationSto
/// <summary>
/// Provides methods allowing to manage the applications stored in a database.
/// </summary>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TApplication">The type of the application entity.</typeparam>
public class OpenIddictMongoDbApplicationStore<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApplication> : IOpenIddictApplicationStore<TApplication>
where TApplication : OpenIddictMongoDbApplication
@ -251,8 +251,8 @@ public class OpenIddictMongoDbApplicationStore<
}
return new(application.DisplayNames.ToImmutableDictionary(
pair => CultureInfo.GetCultureInfo(pair.Key),
pair => pair.Value));
static pair => CultureInfo.GetCultureInfo(pair.Key),
static pair => pair.Value));
}
/// <inheritdoc/>
@ -506,8 +506,8 @@ public class OpenIddictMongoDbApplicationStore<
}
application.DisplayNames = names.ToImmutableDictionary(
pair => pair.Key.Name,
pair => pair.Value);
static pair => pair.Key.Name,
static pair => pair.Value);
return ValueTask.CompletedTask;
}

8
src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs

@ -32,7 +32,7 @@ public class OpenIddictMongoDbAuthorizationStore : OpenIddictMongoDbAuthorizatio
/// <summary>
/// Provides methods allowing to manage the authorizations stored in a database.
/// </summary>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the authorization entity.</typeparam>
public class OpenIddictMongoDbAuthorizationStore<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TAuthorization> : IOpenIddictAuthorizationStore<TAuthorization>
where TAuthorization : OpenIddictMongoDbAuthorization
@ -100,7 +100,7 @@ public class OpenIddictMongoDbAuthorizationStore<
entity.Id == authorization.Id &&
entity.ConcurrencyToken == authorization.ConcurrencyToken, cancellationToken)).DeletedCount is 0)
{
throw new ConcurrencyException(SR.GetResourceString(SR.ID0241));
throw new ConcurrencyException(SR.GetResourceString(SR.ID0239));
}
// Delete the tokens associated with the authorization.
@ -322,7 +322,7 @@ public class OpenIddictMongoDbAuthorizationStore<
catch (MemberAccessException exception)
{
return new(Task.FromException<TAuthorization>(
new InvalidOperationException(SR.GetResourceString(SR.ID0242), exception)));
new InvalidOperationException(SR.GetResourceString(SR.ID0240), exception)));
}
}
@ -601,7 +601,7 @@ public class OpenIddictMongoDbAuthorizationStore<
entity.Id == authorization.Id &&
entity.ConcurrencyToken == timestamp, authorization, null as ReplaceOptions, cancellationToken)).MatchedCount is 0)
{
throw new ConcurrencyException(SR.GetResourceString(SR.ID0241));
throw new ConcurrencyException(SR.GetResourceString(SR.ID0239));
}
}
}

437
src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbResourceStore.cs

@ -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));
}
}
}

24
src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbScopeStore.cs

@ -33,7 +33,7 @@ public class OpenIddictMongoDbScopeStore : OpenIddictMongoDbScopeStore<OpenIddic
/// <summary>
/// Provides methods allowing to manage the scopes stored in a database.
/// </summary>
/// <typeparam name="TScope">The type of the Scope entity.</typeparam>
/// <typeparam name="TScope">The type of the scope entity.</typeparam>
public class OpenIddictMongoDbScopeStore<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TScope> : IOpenIddictScopeStore<TScope>
where TScope : OpenIddictMongoDbScope
@ -101,7 +101,7 @@ public class OpenIddictMongoDbScopeStore<
entity.Id == scope.Id &&
entity.ConcurrencyToken == scope.ConcurrencyToken, cancellationToken)).DeletedCount is 0)
{
throw new ConcurrencyException(SR.GetResourceString(SR.ID0245));
throw new ConcurrencyException(SR.GetResourceString(SR.ID0239));
}
}
@ -202,8 +202,8 @@ public class OpenIddictMongoDbScopeStore<
}
return new(scope.Descriptions.ToImmutableDictionary(
pair => CultureInfo.GetCultureInfo(pair.Key),
pair => pair.Value));
static pair => CultureInfo.GetCultureInfo(pair.Key),
static pair => pair.Value));
}
/// <inheritdoc/>
@ -225,8 +225,8 @@ public class OpenIddictMongoDbScopeStore<
}
return new(scope.DisplayNames.ToImmutableDictionary(
pair => CultureInfo.GetCultureInfo(pair.Key),
pair => pair.Value));
static pair => CultureInfo.GetCultureInfo(pair.Key),
static pair => pair.Value));
}
/// <inheritdoc/>
@ -290,7 +290,7 @@ public class OpenIddictMongoDbScopeStore<
catch (MemberAccessException exception)
{
return new(Task.FromException<TScope>(
new InvalidOperationException(SR.GetResourceString(SR.ID0246), exception)));
new InvalidOperationException(SR.GetResourceString(SR.ID0240), exception)));
}
}
@ -364,8 +364,8 @@ public class OpenIddictMongoDbScopeStore<
}
scope.Descriptions = descriptions.ToImmutableDictionary(
pair => pair.Key.Name,
pair => pair.Value);
static pair => pair.Key.Name,
static pair => pair.Value);
return ValueTask.CompletedTask;
}
@ -384,8 +384,8 @@ public class OpenIddictMongoDbScopeStore<
}
scope.DisplayNames = names.ToImmutableDictionary(
pair => pair.Key.Name,
pair => pair.Value);
static pair => pair.Key.Name,
static pair => pair.Value);
return ValueTask.CompletedTask;
}
@ -480,7 +480,7 @@ public class OpenIddictMongoDbScopeStore<
entity.Id == scope.Id &&
entity.ConcurrencyToken == timestamp, scope, null as ReplaceOptions, cancellationToken)).MatchedCount is 0)
{
throw new ConcurrencyException(SR.GetResourceString(SR.ID0245));
throw new ConcurrencyException(SR.GetResourceString(SR.ID0239));
}
}
}

8
src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs

@ -32,7 +32,7 @@ public class OpenIddictMongoDbTokenStore : OpenIddictMongoDbTokenStore<OpenIddic
/// <summary>
/// Provides methods allowing to manage the tokens stored in a database.
/// </summary>
/// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TToken">The type of the token entity.</typeparam>
public class OpenIddictMongoDbTokenStore<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TToken> : IOpenIddictTokenStore<TToken>
where TToken : OpenIddictMongoDbToken
@ -100,7 +100,7 @@ public class OpenIddictMongoDbTokenStore<
entity.Id == token.Id &&
entity.ConcurrencyToken == token.ConcurrencyToken, cancellationToken)).DeletedCount is 0)
{
throw new ConcurrencyException(SR.GetResourceString(SR.ID0247));
throw new ConcurrencyException(SR.GetResourceString(SR.ID0239));
}
}
@ -379,7 +379,7 @@ public class OpenIddictMongoDbTokenStore<
catch (MemberAccessException exception)
{
return new(Task.FromException<TToken>(
new InvalidOperationException(SR.GetResourceString(SR.ID0248), exception)));
new InvalidOperationException(SR.GetResourceString(SR.ID0240), exception)));
}
}
@ -712,7 +712,7 @@ public class OpenIddictMongoDbTokenStore<
entity.Id == token.Id &&
entity.ConcurrencyToken == timestamp, token, null as ReplaceOptions, cancellationToken)).MatchedCount is 0)
{
throw new ConcurrencyException(SR.GetResourceString(SR.ID0247));
throw new ConcurrencyException(SR.GetResourceString(SR.ID0239));
}
}
}

92
src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs

@ -1570,26 +1570,62 @@ public static partial class OpenIddictServerHandlers
/// </summary>
public sealed class ValidateResources : IOpenIddictServerHandler<ValidateAuthorizationRequestContext>
{
private readonly IOpenIddictResourceManager? _resourceManager;
public ValidateResources(IOpenIddictResourceManager? resourceManager = null)
=> _resourceManager = resourceManager;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateAuthorizationRequestContext>()
.AddFilter<RequireResourceValidationEnabled>()
.UseSingletonHandler<ValidateResources>()
.UseScopedHandler(static provider =>
{
// Note: the resource manager is only resolved if the degraded mode was not enabled to ensure
// invalid core configuration exceptions are not thrown even if the managers were registered.
var options = provider.GetRequiredService<IOptionsMonitor<OpenIddictServerOptions>>().CurrentValue;
return options.EnableDegradedMode ?
new ValidateResources() :
new ValidateResources(provider.GetService<IOpenIddictResourceManager>() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)));
})
.SetOrder(ValidateScopes.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidateAuthorizationRequestContext context)
public async ValueTask HandleAsync(ValidateAuthorizationRequestContext context)
{
ArgumentNullException.ThrowIfNull(context);
// If at least one resource was not recognized, return an error.
// If all the specified resources are registered in the options, avoid making a database lookup.
var resources = context.Request.GetResources().ToHashSet(StringComparer.Ordinal);
resources.ExceptWith(context.Options.Resources.Select(static resource => resource.AbsoluteUri));
// Note: the remaining resources are only checked if the degraded mode was not enabled,
// as this requires using the resource manager, which is never used with the degraded mode,
// even if the service was registered and resolved from the dependency injection container.
if (resources.Count is not 0 && !context.Options.EnableDegradedMode)
{
if (_resourceManager is null)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
}
await foreach (var resource in _resourceManager.FindByNamesAsync([.. resources]))
{
var name = await _resourceManager.GetNameAsync(resource);
if (!string.IsNullOrEmpty(name))
{
resources.Remove(name);
}
}
}
// If at least one resource was not recognized, return an error.
if (resources.Count is not 0)
{
context.Logger.LogInformation(6275, SR.GetResourceString(SR.ID6274), resources);
@ -1599,10 +1635,8 @@ public static partial class OpenIddictServerHandlers
description: SR.FormatID2190(Parameters.Resource),
uri: SR.FormatID8000(SR.ID2190));
return ValueTask.CompletedTask;
return;
}
return ValueTask.CompletedTask;
}
}
@ -3623,26 +3657,62 @@ public static partial class OpenIddictServerHandlers
/// </summary>
public sealed class ValidatePushedResources : IOpenIddictServerHandler<ValidatePushedAuthorizationRequestContext>
{
private readonly IOpenIddictResourceManager? _resourceManager;
public ValidatePushedResources(IOpenIddictResourceManager? resourceManager = null)
=> _resourceManager = resourceManager;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidatePushedAuthorizationRequestContext>()
.AddFilter<RequireResourceValidationEnabled>()
.UseSingletonHandler<ValidatePushedResources>()
.UseScopedHandler(static provider =>
{
// Note: the resource manager is only resolved if the degraded mode was not enabled to ensure
// invalid core configuration exceptions are not thrown even if the managers were registered.
var options = provider.GetRequiredService<IOptionsMonitor<OpenIddictServerOptions>>().CurrentValue;
return options.EnableDegradedMode ?
new ValidatePushedResources() :
new ValidatePushedResources(provider.GetService<IOpenIddictResourceManager>() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)));
})
.SetOrder(ValidatePushedScopes.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
public async ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
{
ArgumentNullException.ThrowIfNull(context);
// If at least one resource was not recognized, return an error.
// If all the specified resources are registered in the options, avoid making a database lookup.
var resources = context.Request.GetResources().ToHashSet(StringComparer.Ordinal);
resources.ExceptWith(context.Options.Resources.Select(static resource => resource.AbsoluteUri));
// Note: the remaining resources are only checked if the degraded mode was not enabled,
// as this requires using the resource manager, which is never used with the degraded mode,
// even if the service was registered and resolved from the dependency injection container.
if (resources.Count is not 0 && !context.Options.EnableDegradedMode)
{
if (_resourceManager is null)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
}
await foreach (var resource in _resourceManager.FindByNamesAsync([.. resources]))
{
var name = await _resourceManager.GetNameAsync(resource);
if (!string.IsNullOrEmpty(name))
{
resources.Remove(name);
}
}
}
// If at least one resource was not recognized, return an error.
if (resources.Count is not 0)
{
context.Logger.LogInformation(6275, SR.GetResourceString(SR.ID6275), resources);
@ -3652,10 +3722,8 @@ public static partial class OpenIddictServerHandlers
description: SR.FormatID2190(Parameters.Resource),
uri: SR.FormatID8000(SR.ID2190));
return ValueTask.CompletedTask;
return;
}
return ValueTask.CompletedTask;
}
}

46
src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs

@ -1118,26 +1118,62 @@ public static partial class OpenIddictServerHandlers
/// </summary>
public sealed class ValidateResources : IOpenIddictServerHandler<ValidateTokenRequestContext>
{
private readonly IOpenIddictResourceManager? _resourceManager;
public ValidateResources(IOpenIddictResourceManager? resourceManager = null)
=> _resourceManager = resourceManager;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>()
.AddFilter<RequireResourceValidationEnabled>()
.UseSingletonHandler<ValidateResources>()
.UseScopedHandler(static provider =>
{
// Note: the resource manager is only resolved if the degraded mode was not enabled to ensure
// invalid core configuration exceptions are not thrown even if the managers were registered.
var options = provider.GetRequiredService<IOptionsMonitor<OpenIddictServerOptions>>().CurrentValue;
return options.EnableDegradedMode ?
new ValidateResources() :
new ValidateResources(provider.GetService<IOpenIddictResourceManager>() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)));
})
.SetOrder(ValidateAudiences.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidateTokenRequestContext context)
public async ValueTask HandleAsync(ValidateTokenRequestContext context)
{
ArgumentNullException.ThrowIfNull(context);
// If at least one resource was not recognized, return an error.
// If all the specified resources are registered in the options, avoid making a database lookup.
var resources = context.Request.GetResources().ToHashSet(StringComparer.Ordinal);
resources.ExceptWith(context.Options.Resources.Select(static resource => resource.AbsoluteUri));
// Note: the remaining resources are only checked if the degraded mode was not enabled,
// as this requires using the resource manager, which is never used with the degraded mode,
// even if the service was registered and resolved from the dependency injection container.
if (resources.Count is not 0 && !context.Options.EnableDegradedMode)
{
if (_resourceManager is null)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
}
await foreach (var resource in _resourceManager.FindByNamesAsync([.. resources]))
{
var name = await _resourceManager.GetNameAsync(resource);
if (!string.IsNullOrEmpty(name))
{
resources.Remove(name);
}
}
}
// If at least one resource was not recognized, return an error.
if (resources.Count is not 0)
{
context.Logger.LogInformation(6273, SR.GetResourceString(SR.ID6273), resources);
@ -1147,10 +1183,8 @@ public static partial class OpenIddictServerHandlers
description: SR.FormatID2190(Parameters.Resource),
uri: SR.FormatID8000(SR.ID2190));
return ValueTask.CompletedTask;
return;
}
return ValueTask.CompletedTask;
}
}

2
test/OpenIddict.Core.Tests/Caches/OpenIddictApplicationCacheTests.cs

@ -433,5 +433,5 @@ public class OpenIddictApplicationCacheTests
() => cache.RemoveAsync(application, CancellationToken.None).AsTask());
}
public sealed class OpenIddictApplication { }
public sealed class OpenIddictApplication;
}

2
test/OpenIddict.Core.Tests/Caches/OpenIddictAuthorizationCacheTests.cs

@ -326,5 +326,5 @@ public class OpenIddictAuthorizationCacheTests
() => cache.RemoveAsync(authorization, CancellationToken.None).AsTask());
}
public sealed class OpenIddictAuthorization { }
public sealed class OpenIddictAuthorization;
}

361
test/OpenIddict.Core.Tests/Caches/OpenIddictResourceCacheTests.cs

@ -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;
}

2
test/OpenIddict.Core.Tests/Caches/OpenIddictScopeCacheTests.cs

@ -413,5 +413,5 @@ public class OpenIddictScopeCacheTests
() => cache.RemoveAsync(scope, CancellationToken.None).AsTask());
}
public sealed class OpenIddictScope { }
public sealed class OpenIddictScope;
}

2
test/OpenIddict.Core.Tests/Caches/OpenIddictTokenCacheTests.cs

@ -499,5 +499,5 @@ public class OpenIddictTokenCacheTests
() => cache.RemoveAsync(token, CancellationToken.None).AsTask());
}
public sealed class OpenIddictToken { }
public sealed class OpenIddictToken;
}

2
test/OpenIddict.Core.Tests/Managers/OpenIddictApplicationManagerTests.cs

@ -3537,7 +3537,7 @@ public class OpenIddictApplicationManagerTests
Assert.Equal("certificate", exception.ParamName);
}
public class CustomApplication { }
public class CustomApplication;
private class CustomApplicationManagerWithProtectedAccess : OpenIddictApplicationManager<CustomApplication>
{

2
test/OpenIddict.Core.Tests/Managers/OpenIddictAuthorizationManagerTests.cs

@ -1286,5 +1286,5 @@ public class OpenIddictAuthorizationManagerTests
Assert.DoesNotContain(results, static result => result != ValidationResult.Success);
}
public class CustomAuthorization { }
public class CustomAuthorization;
}

888
test/OpenIddict.Core.Tests/Managers/OpenIddictResourceManagerTests.cs

@ -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;
}

6
test/OpenIddict.Core.Tests/Managers/OpenIddictScopeManagerTests.cs

@ -574,7 +574,7 @@ public class OpenIddictScopeManagerTests
store.Setup(store => store.GetDescriptionsAsync(scope, It.IsAny<CancellationToken>()))
.ReturnsAsync(ImmutableDictionary.Create<CultureInfo, string>()
.Add(CultureInfo.GetCultureInfo("en-US"), "English description")
.Add(CultureInfo.GetCultureInfo("fr-FR"), "Description franaise"));
.Add(CultureInfo.GetCultureInfo("fr-FR"), "Description française"));
var manager = new OpenIddictScopeManager<CustomScope>(cache, logger, options, store.Object);
@ -582,7 +582,7 @@ public class OpenIddictScopeManagerTests
var description = await manager.GetLocalizedDescriptionAsync(scope, CultureInfo.GetCultureInfo("fr-FR"));
// Assert
Assert.Equal("Description franaise", description);
Assert.Equal("Description française", description);
}
[Fact]
@ -973,5 +973,5 @@ public class OpenIddictScopeManagerTests
Assert.DoesNotContain(results, static result => result != ValidationResult.Success);
}
public class CustomScope { }
public class CustomScope;
}

2
test/OpenIddict.Core.Tests/Managers/OpenIddictTokenManagerTests.cs

@ -1339,5 +1339,5 @@ public class OpenIddictTokenManagerTests
Assert.DoesNotContain(results, static result => result != ValidationResult.Success);
}
public class CustomToken { }
public class CustomToken;
}

96
test/OpenIddict.Core.Tests/OpenIddictCoreBuilderTests.cs

@ -115,6 +115,51 @@ public class OpenIddictCoreBuilderTests
Assert.Equal(typeof(OpenGenericAuthorizationManager<>), descriptor.ImplementationType);
}
[Fact]
public void ReplaceResourceManager_ThrowsAnExceptionForClosedSourceManager()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentException>(() => builder.ReplaceResourceManager(typeof(ClosedGenericResourceManager)));
Assert.Equal("type", exception.ParamName);
Assert.StartsWith(SR.GetResourceString(SR.ID0232), exception.Message);
}
[Fact]
public void ReplaceResourceManager_ThrowsAnExceptionForInvalidManager()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentException>(() => builder.ReplaceResourceManager(typeof(object)));
Assert.Equal("type", exception.ParamName);
Assert.StartsWith(SR.GetResourceString(SR.ID0232), exception.Message);
}
[Fact]
public void ReplaceResourceManager_OverridesDefaultOpenGenericManager()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.ReplaceResourceManager(typeof(OpenGenericResourceManager<>));
// Assert
var descriptor = Assert.Single(services, service =>
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(OpenIddictResourceManager<>));
Assert.Equal(typeof(OpenGenericResourceManager<>), descriptor.ImplementationType);
}
[Fact]
public void ReplaceScopeManager_ThrowsAnExceptionForClosedSourceManager()
{
@ -472,6 +517,23 @@ public class OpenIddictCoreBuilderTests
service.ImplementationFactory is not null);
}
[Fact]
public void SetDefaultResourceEntity_ReplacesUntypedManager()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.SetDefaultResourceEntity<CustomResource>();
// Assert
Assert.Contains(services, service =>
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictResourceManager) &&
service.ImplementationFactory is not null);
}
[Fact]
public void SetDefaultScopeEntity_ReplacesUntypedManager()
{
@ -516,10 +578,11 @@ private static OpenIddictCoreBuilder CreateBuilder(IServiceCollection services)
return services;
}
private class CustomApplication { }
private class CustomAuthorization { }
private class CustomScope { }
private class CustomToken { }
private class CustomApplication;
private class CustomAuthorization;
private class CustomResource;
private class CustomScope;
private class CustomToken;
private class ClosedGenericApplicationManager : OpenIddictApplicationManager<CustomApplication>
{
@ -571,6 +634,31 @@ private static OpenIddictCoreBuilder CreateBuilder(IServiceCollection services)
}
}
private class ClosedGenericResourceManager : OpenIddictResourceManager<CustomResource>
{
public ClosedGenericResourceManager(
IOpenIddictResourceCache<CustomResource> cache,
ILogger<OpenIddictResourceManager<CustomResource>> logger,
IOptionsMonitor<OpenIddictCoreOptions> options,
IOpenIddictResourceStore<CustomResource> store)
: base(cache, logger, options, store)
{
}
}
private class OpenGenericResourceManager<TResource> : OpenIddictResourceManager<TResource>
where TResource : class
{
public OpenGenericResourceManager(
IOpenIddictResourceCache<TResource> cache,
ILogger<OpenIddictResourceManager<TResource>> logger,
IOptionsMonitor<OpenIddictCoreOptions> options,
IOpenIddictResourceStore<TResource> store)
: base(cache, logger, options, store)
{
}
}
private class ClosedGenericScopeManager : OpenIddictScopeManager<CustomScope>
{
public ClosedGenericScopeManager(

20
test/OpenIddict.Core.Tests/OpenIddictCoreExtensionsTests.cs

@ -69,6 +69,7 @@ public class OpenIddictCoreExtensionsTests
[Theory]
[InlineData(typeof(OpenIddictApplicationManager<>))]
[InlineData(typeof(OpenIddictAuthorizationManager<>))]
[InlineData(typeof(OpenIddictResourceManager<>))]
[InlineData(typeof(OpenIddictScopeManager<>))]
[InlineData(typeof(OpenIddictTokenManager<>))]
public void AddCore_RegistersDefaultManagers(Type type)
@ -87,6 +88,7 @@ public class OpenIddictCoreExtensionsTests
[Theory]
[InlineData(typeof(IOpenIddictApplicationManager))]
[InlineData(typeof(IOpenIddictAuthorizationManager))]
[InlineData(typeof(IOpenIddictResourceManager))]
[InlineData(typeof(IOpenIddictScopeManager))]
[InlineData(typeof(IOpenIddictTokenManager))]
public void AddCore_RegistersUntypedProxies(Type type)
@ -138,6 +140,24 @@ public class OpenIddictCoreExtensionsTests
Assert.Equal(SR.GetResourceString(SR.ID0472), exception.Message);
}
[Fact]
public void AddCore_ResolvingUntypedResourceManagerThrowsAnException()
{
// Arrange
var services = new ServiceCollection();
var builder = new OpenIddictBuilder(services);
// Act
builder.AddCore();
// Assert
var provider = services.BuildServiceProvider();
var exception = Assert.Throws<InvalidOperationException>(provider.GetRequiredService<IOpenIddictResourceManager>);
Assert.Equal(SR.GetResourceString(SR.ID0472), exception.Message);
}
[Fact]
public void AddCore_ResolvingUntypedScopeManagerThrowsAnException()
{

15
test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkBuilderTests.cs

@ -33,7 +33,7 @@ public class OpenIddictEntityFrameworkBuilderTests
var builder = CreateBuilder(services);
// Act
builder.ReplaceDefaultEntities<CustomApplication, CustomAuthorization, CustomScope, CustomToken, long>();
builder.ReplaceDefaultEntities<CustomApplication, CustomAuthorization, CustomResource, CustomScope, CustomToken, long>();
// Assert
Assert.Contains(services, service =>
@ -44,6 +44,10 @@ public class OpenIddictEntityFrameworkBuilderTests
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictAuthorizationStore<CustomAuthorization>) &&
service.ImplementationType == typeof(OpenIddictEntityFrameworkAuthorizationStore<CustomAuthorization, CustomApplication, CustomToken, long>));
Assert.Contains(services, service =>
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictResourceStore<CustomResource>) &&
service.ImplementationType == typeof(OpenIddictEntityFrameworkResourceStore<CustomResource, long>));
Assert.Contains(services, service =>
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictScopeStore<CustomScope>) &&
@ -82,10 +86,11 @@ public class OpenIddictEntityFrameworkBuilderTests
return services;
}
public class CustomApplication : OpenIddictEntityFrameworkApplication<long, CustomAuthorization, CustomToken> { }
public class CustomAuthorization : OpenIddictEntityFrameworkAuthorization<long, CustomApplication, CustomToken> { }
public class CustomScope : OpenIddictEntityFrameworkScope<long> { }
public class CustomToken : OpenIddictEntityFrameworkToken<long, CustomApplication, CustomAuthorization> { }
public class CustomApplication : OpenIddictEntityFrameworkApplication<long, CustomAuthorization, CustomToken>;
public class CustomAuthorization : OpenIddictEntityFrameworkAuthorization<long, CustomApplication, CustomToken>;
public class CustomResource : OpenIddictEntityFrameworkResource<long>;
public class CustomScope : OpenIddictEntityFrameworkScope<long>;
public class CustomToken : OpenIddictEntityFrameworkToken<long, CustomApplication, CustomAuthorization>;
public class CustomDbContext : DbContext
{

8
test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkExtensionsTests.cs

@ -56,6 +56,10 @@ public class OpenIddictEntityFrameworkExtensionsTests
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictAuthorizationManager) &&
service.ImplementationFactory is not null);
Assert.Contains(services, service =>
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictResourceManager) &&
service.ImplementationFactory is not null);
Assert.Contains(services, service =>
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictScopeManager) &&
@ -85,6 +89,10 @@ public class OpenIddictEntityFrameworkExtensionsTests
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictAuthorizationStore<OpenIddictEntityFrameworkAuthorization>) &&
service.ImplementationType == typeof(OpenIddictEntityFrameworkAuthorizationStore));
Assert.Contains(services, service =>
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictResourceStore<OpenIddictEntityFrameworkResource>) &&
service.ImplementationType == typeof(OpenIddictEntityFrameworkResourceStore));
Assert.Contains(services, service =>
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictScopeStore<OpenIddictEntityFrameworkScope>) &&

15
test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreBuilderTests.cs

@ -33,7 +33,7 @@ public class OpenIddictEntityFrameworkCoreBuilderTests
var builder = CreateBuilder(services);
// Act
builder.ReplaceDefaultEntities<CustomApplication, CustomAuthorization, CustomScope, CustomToken, long>();
builder.ReplaceDefaultEntities<CustomApplication, CustomAuthorization, CustomResource, CustomScope, CustomToken, long>();
// Assert
Assert.Contains(services, service =>
@ -44,6 +44,10 @@ public class OpenIddictEntityFrameworkCoreBuilderTests
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictAuthorizationStore<CustomAuthorization>) &&
service.ImplementationType == typeof(OpenIddictEntityFrameworkCoreAuthorizationStore<CustomAuthorization, CustomApplication, CustomToken, long>));
Assert.Contains(services, service =>
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictResourceStore<CustomResource>) &&
service.ImplementationType == typeof(OpenIddictEntityFrameworkCoreResourceStore<CustomResource, long>));
Assert.Contains(services, service =>
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictScopeStore<CustomScope>) &&
@ -82,10 +86,11 @@ public class OpenIddictEntityFrameworkCoreBuilderTests
return services;
}
public class CustomApplication : OpenIddictEntityFrameworkCoreApplication<long, CustomAuthorization, CustomToken> { }
public class CustomAuthorization : OpenIddictEntityFrameworkCoreAuthorization<long, CustomApplication, CustomToken> { }
public class CustomScope : OpenIddictEntityFrameworkCoreScope<long> { }
public class CustomToken : OpenIddictEntityFrameworkCoreToken<long, CustomApplication, CustomAuthorization> { }
public class CustomApplication : OpenIddictEntityFrameworkCoreApplication<long, CustomAuthorization, CustomToken>;
public class CustomAuthorization : OpenIddictEntityFrameworkCoreAuthorization<long, CustomApplication, CustomToken>;
public class CustomResource : OpenIddictEntityFrameworkCoreResource<long>;
public class CustomScope : OpenIddictEntityFrameworkCoreScope<long>;
public class CustomToken : OpenIddictEntityFrameworkCoreToken<long, CustomApplication, CustomAuthorization>;
public class CustomDbContext : DbContext
{

8
test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreExtensionsTests.cs

@ -56,6 +56,10 @@ public class OpenIddictEntityFrameworkCoreExtensionsTests
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictAuthorizationManager) &&
service.ImplementationFactory is not null);
Assert.Contains(services, service =>
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictResourceManager) &&
service.ImplementationFactory is not null);
Assert.Contains(services, service =>
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictScopeManager) &&
@ -85,6 +89,10 @@ public class OpenIddictEntityFrameworkCoreExtensionsTests
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictAuthorizationStore<OpenIddictEntityFrameworkCoreAuthorization>) &&
service.ImplementationType == typeof(OpenIddictEntityFrameworkCoreAuthorizationStore));
Assert.Contains(services, service =>
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictResourceStore<OpenIddictEntityFrameworkCoreResource>) &&
service.ImplementationType == typeof(OpenIddictEntityFrameworkCoreResourceStore));
Assert.Contains(services, service =>
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictScopeStore<OpenIddictEntityFrameworkCoreScope>) &&

23
test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreHelpersTests.cs

@ -23,6 +23,8 @@ public class OpenIddictEntityFrameworkCoreHelpersTests
.Returns(builder.Object);
builder.Setup(mock => mock.ApplyConfiguration(It.IsAny<IEntityTypeConfiguration<OpenIddictEntityFrameworkCoreAuthorization>>()))
.Returns(builder.Object);
builder.Setup(mock => mock.ApplyConfiguration(It.IsAny<IEntityTypeConfiguration<OpenIddictEntityFrameworkCoreResource>>()))
.Returns(builder.Object);
builder.Setup(mock => mock.ApplyConfiguration(It.IsAny<IEntityTypeConfiguration<OpenIddictEntityFrameworkCoreScope>>()))
.Returns(builder.Object);
builder.Setup(mock => mock.ApplyConfiguration(It.IsAny<IEntityTypeConfiguration<OpenIddictEntityFrameworkCoreToken>>()))
@ -36,6 +38,8 @@ public class OpenIddictEntityFrameworkCoreHelpersTests
It.IsAny<OpenIddictEntityFrameworkCoreApplicationConfiguration<OpenIddictEntityFrameworkCoreApplication, OpenIddictEntityFrameworkCoreAuthorization, OpenIddictEntityFrameworkCoreToken, string>>()), Times.Once());
builder.Verify(mock => mock.ApplyConfiguration(
It.IsAny<OpenIddictEntityFrameworkCoreAuthorizationConfiguration<OpenIddictEntityFrameworkCoreAuthorization, OpenIddictEntityFrameworkCoreApplication, OpenIddictEntityFrameworkCoreToken, string>>()), Times.Once());
builder.Verify(mock => mock.ApplyConfiguration(
It.IsAny<OpenIddictEntityFrameworkCoreResourceConfiguration<OpenIddictEntityFrameworkCoreResource, string>>()), Times.Once());
builder.Verify(mock => mock.ApplyConfiguration(
It.IsAny<OpenIddictEntityFrameworkCoreScopeConfiguration<OpenIddictEntityFrameworkCoreScope, string>>()), Times.Once());
builder.Verify(mock => mock.ApplyConfiguration(
@ -51,6 +55,8 @@ public class OpenIddictEntityFrameworkCoreHelpersTests
.Returns(builder.Object);
builder.Setup(mock => mock.ApplyConfiguration(It.IsAny<IEntityTypeConfiguration<OpenIddictEntityFrameworkCoreAuthorization<long>>>()))
.Returns(builder.Object);
builder.Setup(mock => mock.ApplyConfiguration(It.IsAny<IEntityTypeConfiguration<OpenIddictEntityFrameworkCoreResource<long>>>()))
.Returns(builder.Object);
builder.Setup(mock => mock.ApplyConfiguration(It.IsAny<IEntityTypeConfiguration<OpenIddictEntityFrameworkCoreScope<long>>>()))
.Returns(builder.Object);
builder.Setup(mock => mock.ApplyConfiguration(It.IsAny<IEntityTypeConfiguration<OpenIddictEntityFrameworkCoreToken<long>>>()))
@ -64,6 +70,8 @@ public class OpenIddictEntityFrameworkCoreHelpersTests
It.IsAny<OpenIddictEntityFrameworkCoreApplicationConfiguration<OpenIddictEntityFrameworkCoreApplication<long>, OpenIddictEntityFrameworkCoreAuthorization<long>, OpenIddictEntityFrameworkCoreToken<long>, long>>()), Times.Once());
builder.Verify(mock => mock.ApplyConfiguration(
It.IsAny<OpenIddictEntityFrameworkCoreAuthorizationConfiguration<OpenIddictEntityFrameworkCoreAuthorization<long>, OpenIddictEntityFrameworkCoreApplication<long>, OpenIddictEntityFrameworkCoreToken<long>, long>>()), Times.Once());
builder.Verify(mock => mock.ApplyConfiguration(
It.IsAny<OpenIddictEntityFrameworkCoreResourceConfiguration<OpenIddictEntityFrameworkCoreResource<long>, long>>()), Times.Once());
builder.Verify(mock => mock.ApplyConfiguration(
It.IsAny<OpenIddictEntityFrameworkCoreScopeConfiguration<OpenIddictEntityFrameworkCoreScope<long>, long>>()), Times.Once());
builder.Verify(mock => mock.ApplyConfiguration(
@ -79,27 +87,32 @@ public class OpenIddictEntityFrameworkCoreHelpersTests
.Returns(builder.Object);
builder.Setup(mock => mock.ApplyConfiguration(It.IsAny<IEntityTypeConfiguration<CustomAuthorization>>()))
.Returns(builder.Object);
builder.Setup(mock => mock.ApplyConfiguration(It.IsAny<IEntityTypeConfiguration<CustomResource>>()))
.Returns(builder.Object);
builder.Setup(mock => mock.ApplyConfiguration(It.IsAny<IEntityTypeConfiguration<CustomScope>>()))
.Returns(builder.Object);
builder.Setup(mock => mock.ApplyConfiguration(It.IsAny<IEntityTypeConfiguration<CustomToken>>()))
.Returns(builder.Object);
// Act
builder.Object.UseOpenIddict<CustomApplication, CustomAuthorization, CustomScope, CustomToken, Guid>();
builder.Object.UseOpenIddict<CustomApplication, CustomAuthorization, CustomResource, CustomScope, CustomToken, Guid>();
// Assert
builder.Verify(mock => mock.ApplyConfiguration(
It.IsAny<OpenIddictEntityFrameworkCoreApplicationConfiguration<CustomApplication, CustomAuthorization, CustomToken, Guid>>()), Times.Once());
builder.Verify(mock => mock.ApplyConfiguration(
It.IsAny<OpenIddictEntityFrameworkCoreAuthorizationConfiguration<CustomAuthorization, CustomApplication, CustomToken, Guid>>()), Times.Once());
builder.Verify(mock => mock.ApplyConfiguration(
It.IsAny<OpenIddictEntityFrameworkCoreResourceConfiguration<CustomResource, Guid>>()), Times.Once());
builder.Verify(mock => mock.ApplyConfiguration(
It.IsAny<OpenIddictEntityFrameworkCoreScopeConfiguration<CustomScope, Guid>>()), Times.Once());
builder.Verify(mock => mock.ApplyConfiguration(
It.IsAny<OpenIddictEntityFrameworkCoreTokenConfiguration<CustomToken, CustomApplication, CustomAuthorization, Guid>>()), Times.Once());
}
public class CustomApplication : OpenIddictEntityFrameworkCoreApplication<Guid, CustomAuthorization, CustomToken> { }
public class CustomAuthorization : OpenIddictEntityFrameworkCoreAuthorization<Guid, CustomApplication, CustomToken> { }
public class CustomScope : OpenIddictEntityFrameworkCoreScope<Guid> { }
public class CustomToken : OpenIddictEntityFrameworkCoreToken<Guid, CustomApplication, CustomAuthorization> { }
public class CustomApplication : OpenIddictEntityFrameworkCoreApplication<Guid, CustomAuthorization, CustomToken>;
public class CustomAuthorization : OpenIddictEntityFrameworkCoreAuthorization<Guid, CustomApplication, CustomToken>;
public class CustomResource : OpenIddictEntityFrameworkCoreResource<Guid>;
public class CustomScope : OpenIddictEntityFrameworkCoreScope<Guid>;
public class CustomToken : OpenIddictEntityFrameworkCoreToken<Guid, CustomApplication, CustomAuthorization>;
}

58
test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbBuilderTests.cs

@ -61,6 +61,23 @@ public class OpenIddictMongoDbBuilderTests
service.ImplementationType == typeof(OpenIddictMongoDbAuthorizationStore<CustomAuthorization>));
}
[Fact]
public void ReplaceDefaultResourceEntity_StoreIsCorrectlyReplaced()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.ReplaceDefaultResourceEntity<CustomResource>();
// Assert
Assert.Contains(services, service =>
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictResourceStore<CustomResource>) &&
service.ImplementationType == typeof(OpenIddictMongoDbResourceStore<CustomResource>));
}
[Fact]
public void ReplaceDefaultScopeEntity_StoreIsCorrectlyReplaced()
{
@ -159,6 +176,38 @@ public class OpenIddictMongoDbBuilderTests
Assert.Equal("custom_collection", options.AuthorizationsCollectionName);
}
[Theory]
[InlineData(null)]
[InlineData("")]
public void SetResourcesCollectionName_ThrowsAnExceptionForNullOrEmptyCollectionName(string? name)
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.ThrowsAny<ArgumentException>(() => builder.SetResourcesCollectionName(name!));
Assert.Equal("name", exception.ParamName);
}
[Fact]
public void SetResourcesCollectionName_CollectionNameIsCorrectlySet()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.SetResourcesCollectionName("custom_collection");
// Assert
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptionsMonitor<OpenIddictMongoDbOptions>>().CurrentValue;
Assert.Equal("custom_collection", options.ResourcesCollectionName);
}
[Theory]
[InlineData(null)]
[InlineData("")]
@ -268,8 +317,9 @@ public class OpenIddictMongoDbBuilderTests
return services;
}
public class CustomApplication : OpenIddictMongoDbApplication { }
public class CustomAuthorization : OpenIddictMongoDbAuthorization { }
public class CustomScope : OpenIddictMongoDbScope { }
public class CustomToken : OpenIddictMongoDbToken { }
public class CustomApplication : OpenIddictMongoDbApplication;
public class CustomAuthorization : OpenIddictMongoDbAuthorization;
public class CustomResource : OpenIddictMongoDbResource;
public class CustomScope : OpenIddictMongoDbScope;
public class CustomToken : OpenIddictMongoDbToken;
}

8
test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbExtensionsTests.cs

@ -56,6 +56,10 @@ public class OpenIddictMongoDbExtensionsTests
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictAuthorizationManager) &&
service.ImplementationFactory is not null);
Assert.Contains(services, service =>
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictResourceManager) &&
service.ImplementationFactory is not null);
Assert.Contains(services, service =>
service.Lifetime == ServiceLifetime.Scoped &&
service.ServiceType == typeof(IOpenIddictScopeManager) &&
@ -85,6 +89,10 @@ public class OpenIddictMongoDbExtensionsTests
service.Lifetime == ServiceLifetime.Singleton &&
service.ServiceType == typeof(IOpenIddictAuthorizationStore<OpenIddictMongoDbAuthorization>) &&
service.ImplementationType == typeof(OpenIddictMongoDbAuthorizationStore));
Assert.Contains(services, service =>
service.Lifetime == ServiceLifetime.Singleton &&
service.ServiceType == typeof(IOpenIddictResourceStore<OpenIddictMongoDbResource>) &&
service.ImplementationType == typeof(OpenIddictMongoDbResourceStore));
Assert.Contains(services, service =>
service.Lifetime == ServiceLifetime.Singleton &&
service.ServiceType == typeof(IOpenIddictScopeStore<OpenIddictMongoDbScope>) &&

153
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs

@ -1087,6 +1087,14 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}));
options.Services.AddSingleton(CreateResourceManager(mock =>
{
mock.Setup(manager => manager.FindByNamesAsync(
It.Is<ImmutableArray<string>>(resources => resources.Length == 1 && resources[0] == "urn:unregistered_resource"),
It.IsAny<CancellationToken>()))
.Returns(AsyncEnumerable.Empty<OpenIddictResource>());
}));
});
await using var client = await server.CreateClientAsync();
@ -1144,6 +1152,78 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.NotNull(response.AccessToken);
}
[Fact]
public async Task ValidateAuthorizationRequest_RequestIsValidatedWhenRegisteredResourceIsSpecified()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
var resource = new OpenIddictResource();
options.RegisterResources("urn:resource_registered_in_options");
options.SetDeviceAuthorizationEndpointUris(Array.Empty<Uri>());
options.SetRevocationEndpointUris(Array.Empty<Uri>());
options.Configure(options => options.GrantTypes.Remove(GrantTypes.DeviceCode));
options.DisableTokenStorage();
options.DisableSlidingRefreshTokenExpiration();
options.Services.AddSingleton(CreateApplicationManager(mock =>
{
var application = new OpenIddictApplication();
mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.GetSettingsAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(ImmutableDictionary.Create<string, string>());
}));
options.Services.AddSingleton(CreateResourceManager(mock =>
{
mock.Setup(manager => manager.FindByNamesAsync(
It.Is<ImmutableArray<string>>(resources => resources.Length == 1 && resources[0] == "urn:resource_registered_in_database"),
It.IsAny<CancellationToken>()))
.Returns(new[] { resource }.ToAsyncEnumerable());
mock.Setup(manager => manager.GetNameAsync(resource, It.IsAny<CancellationToken>()))
.ReturnsAsync("urn:resource_registered_in_database");
}));
options.AddEventHandler<HandleAuthorizationRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetClaim(Claims.Subject, "Bob le Magnifique");
return ValueTask.CompletedTask;
}));
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/authorize", new OpenIddictRequest
{
ClientId = "Fabrikam",
Nonce = "n-0S6_WzA2Mj",
RedirectUri = "http://www.fabrikam.com/path",
Resources = ["urn:resource_registered_in_database", "urn:resource_registered_in_options"],
ResponseType = ResponseTypes.Token
});
// Assert
Assert.Null(response.Error);
Assert.Null(response.ErrorDescription);
Assert.Null(response.ErrorUri);
Assert.NotNull(response.AccessToken);
}
[Fact]
public async Task ValidateAuthorizationRequest_UnknownResponseModeParameterIsRejected()
{
@ -3921,6 +4001,14 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}));
options.Services.AddSingleton(CreateResourceManager(mock =>
{
mock.Setup(manager => manager.FindByNamesAsync(
It.Is<ImmutableArray<string>>(resources => resources.Length == 1 && resources[0] == "urn:unregistered_resource"),
It.IsAny<CancellationToken>()))
.Returns(AsyncEnumerable.Empty<OpenIddictResource>());
}));
});
await using var client = await server.CreateClientAsync();
@ -3930,7 +4018,7 @@ public abstract partial class OpenIddictServerIntegrationTests
{
ClientId = "Fabrikam",
RedirectUri = "http://www.fabrikam.com/path",
Resources = ["urn:registered_resource"],
Resources = ["urn:unregistered_resource"],
ResponseType = ResponseTypes.Code
});
@ -3969,6 +4057,69 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.NotNull(response.RequestUri);
}
[Fact]
public async Task ValidatePushedAuthorizationRequest_RequestIsValidatedWhenRegisteredResourceIsSpecified()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
var resource = new OpenIddictResource();
options.RegisterResources("urn:resource_registered_in_options");
options.SetDeviceAuthorizationEndpointUris(Array.Empty<Uri>());
options.SetRevocationEndpointUris(Array.Empty<Uri>());
options.Configure(options => options.GrantTypes.Remove(GrantTypes.DeviceCode));
options.DisableTokenStorage();
options.DisableSlidingRefreshTokenExpiration();
options.Services.AddSingleton(CreateApplicationManager(mock =>
{
var application = new OpenIddictApplication();
mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.GetSettingsAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(ImmutableDictionary.Create<string, string>());
}));
options.Services.AddSingleton(CreateResourceManager(mock =>
{
mock.Setup(manager => manager.FindByNamesAsync(
It.Is<ImmutableArray<string>>(resources => resources.Length == 1 && resources[0] == "urn:resource_registered_in_database"),
It.IsAny<CancellationToken>()))
.Returns(new[] { resource }.ToAsyncEnumerable());
mock.Setup(manager => manager.GetNameAsync(resource, It.IsAny<CancellationToken>()))
.ReturnsAsync("urn:resource_registered_in_database");
}));
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/par", new OpenIddictRequest
{
ClientId = "Fabrikam",
Nonce = "n-0S6_WzA2Mj",
RedirectUri = "http://www.fabrikam.com/path",
Resources = ["urn:resource_registered_in_database", "urn:resource_registered_in_options"],
ResponseType = ResponseTypes.Token
});
// Assert
Assert.Null(response.Error);
Assert.Null(response.ErrorDescription);
Assert.Null(response.ErrorUri);
Assert.NotNull(response.RequestUri);
}
[Fact]
public async Task ValidatePushedAuthorizationRequest_UnknownResponseModeParameterIsRejected()
{

68
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs

@ -2251,7 +2251,17 @@ public abstract partial class OpenIddictServerIntegrationTests
public async Task ValidateTokenRequest_RequestIsRejectedWhenUnregisteredResourceIsSpecified()
{
// Arrange
await using var server = await CreateServerAsync();
await using var server = await CreateServerAsync(options =>
{
options.Services.AddSingleton(CreateResourceManager(mock =>
{
mock.Setup(manager => manager.FindByNamesAsync(
It.Is<ImmutableArray<string>>(resources => resources.Length == 1 && resources[0] == "urn:unregistered_resource"),
It.IsAny<CancellationToken>()))
.Returns(AsyncEnumerable.Empty<OpenIddictResource>());
}));
});
await using var client = await server.CreateClientAsync();
// Act
@ -2314,6 +2324,62 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.NotNull(response.AccessToken);
}
[Fact]
public async Task ValidateTokenRequest_RequestIsValidatedWhenRegisteredResourceIsSpecified()
{
// Arrange
var resource = new OpenIddictResource();
var manager = CreateResourceManager(mock =>
{
mock.Setup(manager => manager.FindByNamesAsync(
It.Is<ImmutableArray<string>>(resources => resources.Length == 1 && resources[0] == "urn:resource_registered_in_database"),
It.IsAny<CancellationToken>()))
.Returns(new[] { resource }.ToAsyncEnumerable());
mock.Setup(manager => manager.GetNameAsync(resource, It.IsAny<CancellationToken>()))
.ReturnsAsync("urn:resource_registered_in_database");
});
await using var server = await CreateServerAsync(options =>
{
options.RegisterResources("urn:resource_registered_in_options");
options.SetDeviceAuthorizationEndpointUris(Array.Empty<Uri>());
options.SetRevocationEndpointUris(Array.Empty<Uri>());
options.Configure(options => options.GrantTypes.Remove(GrantTypes.DeviceCode));
options.DisableTokenStorage();
options.DisableSlidingRefreshTokenExpiration();
options.Services.AddSingleton(manager);
options.AddEventHandler<HandleTokenRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetClaim(Claims.Subject, "Bob le Magnifique");
return ValueTask.CompletedTask;
}));
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
GrantType = GrantTypes.Password,
Username = "johndoe",
Password = "A3ddj3w",
Resources = ["urn:resource_registered_in_database", "urn:resource_registered_in_options"]
});
// Assert
Assert.Null(response.Error);
Assert.Null(response.ErrorDescription);
Assert.Null(response.ErrorUri);
Assert.NotNull(response.AccessToken);
}
[Fact]
public async Task ValidateTokenRequest_RequestIsRejectedWhenClientAssertionIsSpecifiedWithoutType()
{

25
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs

@ -5201,11 +5201,13 @@ public abstract partial class OpenIddictServerIntegrationTests
{
options.SetDefaultApplicationEntity<OpenIddictApplication>()
.SetDefaultAuthorizationEntity<OpenIddictAuthorization>()
.SetDefaultResourceEntity<OpenIddictResource>()
.SetDefaultScopeEntity<OpenIddictScope>()
.SetDefaultTokenEntity<OpenIddictToken>();
options.Services.AddSingleton(CreateApplicationManager())
.AddSingleton(CreateAuthorizationManager())
.AddSingleton(CreateResourceManager())
.AddSingleton(CreateScopeManager())
.AddSingleton(CreateTokenManager());
})
@ -5313,6 +5315,20 @@ public abstract partial class OpenIddictServerIntegrationTests
return manager.Object;
}
protected OpenIddictResourceManager<OpenIddictResource> CreateResourceManager(
Action<Mock<OpenIddictResourceManager<OpenIddictResource>>>? configuration = null)
{
var manager = new Mock<OpenIddictResourceManager<OpenIddictResource>>(
Mock.Of<IOpenIddictResourceCache<OpenIddictResource>>(),
OutputHelper.ToLogger<OpenIddictResourceManager<OpenIddictResource>>(),
Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(),
Mock.Of<IOpenIddictResourceStore<OpenIddictResource>>());
configuration?.Invoke(manager);
return manager.Object;
}
protected OpenIddictScopeManager<OpenIddictScope> CreateScopeManager(
Action<Mock<OpenIddictScopeManager<OpenIddictScope>>>? configuration = null)
{
@ -5341,8 +5357,9 @@ public abstract partial class OpenIddictServerIntegrationTests
return manager.Object;
}
public class OpenIddictApplication { }
public class OpenIddictAuthorization { }
public class OpenIddictScope { }
public class OpenIddictToken { }
public class OpenIddictApplication;
public class OpenIddictAuthorization;
public class OpenIddictResource;
public class OpenIddictScope;
public class OpenIddictToken;
}

4
test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTests.cs

@ -449,6 +449,6 @@ public abstract partial class OpenIddictValidationIntegrationTests
return manager.Object;
}
public class OpenIddictAuthorization { }
public class OpenIddictToken { }
public class OpenIddictAuthorization;
public class OpenIddictToken;
}

Loading…
Cancel
Save