diff --git a/samples/Mvc.Server/Controllers/AuthorizationController.cs b/samples/Mvc.Server/Controllers/AuthorizationController.cs index 2b9d07c0..775664cb 100644 --- a/samples/Mvc.Server/Controllers/AuthorizationController.cs +++ b/samples/Mvc.Server/Controllers/AuthorizationController.cs @@ -216,7 +216,7 @@ namespace Mvc.Server // In every other case, render the consent form. default: return View(new AuthorizeViewModel { - ApplicationName = await _applicationManager.GetDisplayNameAsync(application), + ApplicationName = await _applicationManager.GetLocalizedDisplayNameAsync(application), Scope = request.Scope }); } @@ -325,7 +325,7 @@ namespace Mvc.Server // Render a form asking the user to confirm the authorization demand. return View(new VerifyViewModel { - ApplicationName = await _applicationManager.GetDisplayNameAsync(application), + ApplicationName = await _applicationManager.GetLocalizedDisplayNameAsync(application), Scope = string.Join(" ", result.Principal.GetScopes()), UserCode = request.UserCode }); diff --git a/src/OpenIddict.Abstractions/Descriptors/OpenIddictApplicationDescriptor.cs b/src/OpenIddict.Abstractions/Descriptors/OpenIddictApplicationDescriptor.cs index 2ad8aad2..280ee25d 100644 --- a/src/OpenIddict.Abstractions/Descriptors/OpenIddictApplicationDescriptor.cs +++ b/src/OpenIddict.Abstractions/Descriptors/OpenIddictApplicationDescriptor.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; namespace OpenIddict.Abstractions { @@ -25,7 +26,7 @@ namespace OpenIddict.Abstractions /// Gets or sets the consent type /// associated with the application. /// - public virtual string ConsentType { get; set; } + public string ConsentType { get; set; } /// /// Gets or sets the display name @@ -33,6 +34,12 @@ namespace OpenIddict.Abstractions /// public string DisplayName { get; set; } + /// + /// Gets the localized display names associated with the application. + /// + public Dictionary DisplayNames { get; } + = new Dictionary(); + /// /// Gets the permissions associated with the application. /// diff --git a/src/OpenIddict.Abstractions/Descriptors/OpenIddictAuthorizationDescriptor.cs b/src/OpenIddict.Abstractions/Descriptors/OpenIddictAuthorizationDescriptor.cs index 5f5029d6..13915489 100644 --- a/src/OpenIddict.Abstractions/Descriptors/OpenIddictAuthorizationDescriptor.cs +++ b/src/OpenIddict.Abstractions/Descriptors/OpenIddictAuthorizationDescriptor.cs @@ -38,6 +38,6 @@ namespace OpenIddict.Abstractions /// /// Gets or sets the type of the authorization. /// - public virtual string Type { get; set; } + public string Type { get; set; } } } diff --git a/src/OpenIddict.Abstractions/Descriptors/OpenIddictScopeDescriptor.cs b/src/OpenIddict.Abstractions/Descriptors/OpenIddictScopeDescriptor.cs index 24433411..5329a81e 100644 --- a/src/OpenIddict.Abstractions/Descriptors/OpenIddictScopeDescriptor.cs +++ b/src/OpenIddict.Abstractions/Descriptors/OpenIddictScopeDescriptor.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; namespace OpenIddict.Abstractions { @@ -12,23 +13,33 @@ namespace OpenIddict.Abstractions /// Gets or sets the description /// associated with the scope. /// - public virtual string Description { get; set; } + public string Description { get; set; } + + /// + /// Gets the localized descriptions associated with the scope. + /// + public Dictionary Descriptions { get; } = new Dictionary(); /// /// Gets or sets the display name /// associated with the scope. /// - public virtual string DisplayName { get; set; } + public string DisplayName { get; set; } + + /// + /// Gets the localized display names associated with the scope. + /// + public Dictionary DisplayNames { get; } = new Dictionary(); /// /// Gets or sets the unique name /// associated with the scope. /// - public virtual string Name { get; set; } + public string Name { get; set; } /// /// Gets the resources associated with the scope. /// - public virtual HashSet Resources { get; } = new HashSet(StringComparer.Ordinal); + public HashSet Resources { get; } = new HashSet(StringComparer.Ordinal); } } diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs index 7e3b99e6..a518a4b4 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -198,6 +199,17 @@ namespace OpenIddict.Abstractions /// ValueTask GetDisplayNameAsync([NotNull] object application, CancellationToken cancellationToken = default); + /// + /// Retrieves the localized display names associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the localized display names associated with the application. + /// + ValueTask> GetDisplayNamesAsync([NotNull] object application, CancellationToken cancellationToken = default); + /// /// Retrieves the unique identifier associated with an application. /// @@ -209,6 +221,33 @@ namespace OpenIddict.Abstractions /// ValueTask GetIdAsync([NotNull] object application, CancellationToken cancellationToken = default); + /// + /// Retrieves the localized display name associated with an application + /// 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. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the matching localized display name associated with the application. + /// + ValueTask GetLocalizedDisplayNameAsync([NotNull] object application, CancellationToken cancellationToken = default); + + /// + /// Retrieves the localized display name associated with an application + /// and corresponding to the specified culture or one of its parents. + /// If no matching value can be found, the non-localized value is returned. + /// + /// The application. + /// The culture (typically ). + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the matching localized display name associated with the application. + /// + ValueTask GetLocalizedDisplayNameAsync([NotNull] object application, [NotNull] CultureInfo culture, CancellationToken cancellationToken = default); + /// /// Retrieves the permissions associated with an application. /// diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs index 7f6f8b03..eec7d0fb 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -149,6 +150,17 @@ namespace OpenIddict.Abstractions /// ValueTask GetDescriptionAsync([NotNull] object scope, CancellationToken cancellationToken = default); + /// + /// Retrieves the localized descriptions associated with an scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the localized descriptions associated with the scope. + /// + ValueTask> GetDescriptionsAsync([NotNull] object scope, CancellationToken cancellationToken = default); + /// /// Retrieves the display name associated with a scope. /// @@ -160,6 +172,17 @@ namespace OpenIddict.Abstractions /// ValueTask GetDisplayNameAsync([NotNull] object scope, CancellationToken cancellationToken = default); + /// + /// Retrieves the localized display names associated with an scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the localized display names associated with the scope. + /// + ValueTask> GetDisplayNamesAsync([NotNull] object scope, CancellationToken cancellationToken = default); + /// /// Retrieves the unique identifier associated with a scope. /// @@ -171,6 +194,60 @@ namespace OpenIddict.Abstractions /// ValueTask GetIdAsync([NotNull] object scope, CancellationToken cancellationToken = default); + /// + /// Retrieves the localized description associated with an scope + /// 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. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the matching localized description associated with the scope. + /// + ValueTask GetLocalizedDescriptionAsync([NotNull] object scope, CancellationToken cancellationToken = default); + + /// + /// Retrieves the localized description associated with an scope + /// and corresponding to the specified culture or one of its parents. + /// If no matching value can be found, the non-localized value is returned. + /// + /// The scope. + /// The culture (typically ). + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the matching localized description associated with the scope. + /// + ValueTask GetLocalizedDescriptionAsync([NotNull] object scope, [NotNull] CultureInfo culture, CancellationToken cancellationToken = default); + + /// + /// Retrieves the localized display name associated with an scope + /// 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. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the display name associated with the scope. + /// + ValueTask GetLocalizedDisplayNameAsync([NotNull] object scope, CancellationToken cancellationToken = default); + + /// + /// Retrieves the localized display name associated with an scope + /// and corresponding to the specified culture or one of its parents. + /// If no matching value can be found, the non-localized value is returned. + /// + /// The scope. + /// The culture (typically ). + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the display name associated with the scope. + /// + ValueTask GetLocalizedDisplayNameAsync([NotNull] object scope, [NotNull] CultureInfo culture, CancellationToken cancellationToken = default); + /// /// Retrieves the name associated with a scope. /// diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs index 470b1b42..7f976d25 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs @@ -374,7 +374,7 @@ namespace OpenIddict.Abstractions return parameters; } - return ImmutableDictionary.Create(); + return ImmutableDictionary.Create(StringComparer.Ordinal); } /// diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs index 96264937..4f77cd95 100644 --- a/src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs +++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs @@ -12,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using System.Text.Json; +using System.Globalization; namespace OpenIddict.Abstractions { @@ -170,6 +171,17 @@ namespace OpenIddict.Abstractions /// ValueTask GetDisplayNameAsync([NotNull] TApplication application, CancellationToken cancellationToken); + /// + /// Retrieves the localized display names associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the localized display names associated with the application. + /// + ValueTask> GetDisplayNamesAsync([NotNull] TApplication application, CancellationToken cancellationToken); + /// /// Retrieves the unique identifier associated with an application. /// @@ -315,6 +327,16 @@ namespace OpenIddict.Abstractions /// A that can be used to monitor the asynchronous operation. ValueTask SetDisplayNameAsync([NotNull] TApplication application, [CanBeNull] string name, CancellationToken cancellationToken); + /// + /// Sets the localized display names associated with an application. + /// + /// The application. + /// The localized display names associated with the application. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + ValueTask SetDisplayNamesAsync([NotNull] TApplication application, + [CanBeNull] ImmutableDictionary names, CancellationToken cancellationToken); + /// /// Sets the permissions associated with an application. /// diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictScopeStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictScopeStore.cs index 6ea1eb31..e41c9f71 100644 --- a/src/OpenIddict.Abstractions/Stores/IOpenIddictScopeStore.cs +++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictScopeStore.cs @@ -12,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using System.Text.Json; +using System.Globalization; namespace OpenIddict.Abstractions { @@ -124,6 +125,17 @@ namespace OpenIddict.Abstractions /// ValueTask GetDescriptionAsync([NotNull] TScope scope, CancellationToken cancellationToken); + /// + /// Retrieves the localized descriptions associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the localized descriptions associated with the specified scope. + /// + ValueTask> GetDescriptionsAsync([NotNull] TScope scope, CancellationToken cancellationToken); + /// /// Retrieves the display name associated with a scope. /// @@ -135,6 +147,17 @@ namespace OpenIddict.Abstractions /// ValueTask GetDisplayNameAsync([NotNull] TScope scope, CancellationToken cancellationToken); + /// + /// Retrieves the localized display names associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the localized display names associated with the scope. + /// + ValueTask> GetDisplayNamesAsync([NotNull] TScope scope, CancellationToken cancellationToken); + /// /// Retrieves the unique identifier associated with a scope. /// @@ -220,6 +243,16 @@ namespace OpenIddict.Abstractions /// A that can be used to monitor the asynchronous operation. ValueTask SetDescriptionAsync([NotNull] TScope scope, [CanBeNull] string description, CancellationToken cancellationToken); + /// + /// Sets the localized descriptions associated with a scope. + /// + /// The scope. + /// The localized descriptions associated with the authorization. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + ValueTask SetDescriptionsAsync([NotNull] TScope scope, + [CanBeNull] ImmutableDictionary descriptions, CancellationToken cancellationToken); + /// /// Sets the display name associated with a scope. /// @@ -229,6 +262,16 @@ namespace OpenIddict.Abstractions /// A that can be used to monitor the asynchronous operation. ValueTask SetDisplayNameAsync([NotNull] TScope scope, [CanBeNull] string name, CancellationToken cancellationToken); + /// + /// Sets the localized display names associated with a scope. + /// + /// The scope. + /// The localized display names associated with the scope. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + ValueTask SetDisplayNamesAsync([NotNull] TScope scope, + [CanBeNull] ImmutableDictionary names, CancellationToken cancellationToken); + /// /// Sets the name associated with a scope. /// diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs index a0f345b0..2af8a372 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs @@ -9,6 +9,7 @@ using System.Buffers.Binary; using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; +using System.Globalization; using System.Linq; using System.Runtime.CompilerServices; using System.Security.Cryptography; @@ -551,6 +552,32 @@ namespace OpenIddict.Core return Store.GetDisplayNameAsync(application, cancellationToken); } + /// + /// Retrieves the localized display names associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the localized display names associated with the application. + /// + public virtual async ValueTask> GetDisplayNamesAsync( + [NotNull] TApplication application, CancellationToken cancellationToken = default) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + var names = await Store.GetDisplayNamesAsync(application, cancellationToken); + if (names == null || names.Count == 0) + { + return ImmutableDictionary.Create(); + } + + return names; + } + /// /// Retrieves the unique identifier associated with an application. /// @@ -570,6 +597,67 @@ namespace OpenIddict.Core return Store.GetIdAsync(application, cancellationToken); } + /// + /// Retrieves the localized display name associated with an application + /// 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. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the matching localized display name associated with the application. + /// + public virtual ValueTask GetLocalizedDisplayNameAsync( + [NotNull] TApplication application, CancellationToken cancellationToken = default) + => GetLocalizedDisplayNameAsync(application, CultureInfo.CurrentUICulture, cancellationToken); + + /// + /// Retrieves the localized display name associated with an application + /// and corresponding to the specified culture or one of its parents. + /// If no matching value can be found, the non-localized value is returned. + /// + /// The application. + /// The culture (typically ). + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the matching localized display name associated with the application. + /// + public virtual async ValueTask GetLocalizedDisplayNameAsync( + [NotNull] TApplication application, [NotNull] CultureInfo culture, CancellationToken cancellationToken = default) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (culture == null) + { + throw new ArgumentNullException(nameof(culture)); + } + + var names = await Store.GetDisplayNamesAsync(application, cancellationToken); + if (names == null || names.IsEmpty) + { + return await Store.GetDisplayNameAsync(application, cancellationToken); + } + + do + { + if (names.TryGetValue(culture, out var name)) + { + return name; + } + + culture = culture.Parent; + } + + while (culture != CultureInfo.InvariantCulture); + + return await Store.GetDisplayNameAsync(application, cancellationToken); + } + /// /// Retrieves the permissions associated with an application. /// @@ -819,12 +907,13 @@ namespace OpenIddict.Core await Store.SetClientTypeAsync(application, descriptor.Type, cancellationToken); await Store.SetConsentTypeAsync(application, descriptor.ConsentType, cancellationToken); await Store.SetDisplayNameAsync(application, descriptor.DisplayName, cancellationToken); - await Store.SetPermissionsAsync(application, ImmutableArray.CreateRange(descriptor.Permissions), cancellationToken); + await Store.SetDisplayNamesAsync(application, descriptor.DisplayNames.ToImmutableDictionary(), cancellationToken); + await Store.SetPermissionsAsync(application, descriptor.Permissions.ToImmutableArray(), cancellationToken); await Store.SetPostLogoutRedirectUrisAsync(application, ImmutableArray.CreateRange( descriptor.PostLogoutRedirectUris.Select(address => address.OriginalString)), cancellationToken); await Store.SetRedirectUrisAsync(application, ImmutableArray.CreateRange( descriptor.RedirectUris.Select(address => address.OriginalString)), cancellationToken); - await Store.SetRequirementsAsync(application, ImmutableArray.CreateRange(descriptor.Requirements), cancellationToken); + await Store.SetRequirementsAsync(application, descriptor.Requirements.ToImmutableArray(), cancellationToken); } /// @@ -860,6 +949,12 @@ namespace OpenIddict.Core descriptor.Requirements.Clear(); descriptor.Requirements.UnionWith(await Store.GetRequirementsAsync(application, cancellationToken)); + descriptor.DisplayNames.Clear(); + foreach (var pair in await Store.GetDisplayNamesAsync(application, cancellationToken)) + { + descriptor.DisplayNames.Add(pair.Key, pair.Value); + } + descriptor.PostLogoutRedirectUris.Clear(); foreach (var address in await Store.GetPostLogoutRedirectUrisAsync(application, cancellationToken)) { @@ -1456,9 +1551,18 @@ namespace OpenIddict.Core ValueTask IOpenIddictApplicationManager.GetDisplayNameAsync(object application, CancellationToken cancellationToken) => GetDisplayNameAsync((TApplication) application, cancellationToken); + ValueTask> IOpenIddictApplicationManager.GetDisplayNamesAsync(object application, CancellationToken cancellationToken) + => GetDisplayNamesAsync((TApplication) application, cancellationToken); + ValueTask IOpenIddictApplicationManager.GetIdAsync(object application, CancellationToken cancellationToken) => GetIdAsync((TApplication) application, cancellationToken); + ValueTask IOpenIddictApplicationManager.GetLocalizedDisplayNameAsync(object application, CancellationToken cancellationToken) + => GetLocalizedDisplayNameAsync((TApplication) application, cancellationToken); + + ValueTask IOpenIddictApplicationManager.GetLocalizedDisplayNameAsync(object application, CultureInfo culture, CancellationToken cancellationToken) + => GetLocalizedDisplayNameAsync((TApplication) application, culture, cancellationToken); + ValueTask> IOpenIddictApplicationManager.GetPermissionsAsync(object application, CancellationToken cancellationToken) => GetPermissionsAsync((TApplication) application, cancellationToken); diff --git a/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs b/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs index baa43048..8ce33f07 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; +using System.Globalization; using System.Linq; using System.Runtime.CompilerServices; using System.Text; @@ -420,6 +421,32 @@ namespace OpenIddict.Core return Store.GetDescriptionAsync(scope, cancellationToken); } + /// + /// Retrieves the localized descriptions associated with an scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the localized descriptions associated with the scope. + /// + public virtual async ValueTask> GetDescriptionsAsync( + [NotNull] TScope scope, CancellationToken cancellationToken = default) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + var descriptions = await Store.GetDescriptionsAsync(scope, cancellationToken); + if (descriptions == null || descriptions.Count == 0) + { + return ImmutableDictionary.Create(); + } + + return descriptions; + } + /// /// Retrieves the display name associated with a scope. /// @@ -439,6 +466,32 @@ namespace OpenIddict.Core return Store.GetDisplayNameAsync(scope, cancellationToken); } + /// + /// Retrieves the localized display names associated with an scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the localized display names associated with the scope. + /// + public virtual async ValueTask> GetDisplayNamesAsync( + [NotNull] TScope scope, CancellationToken cancellationToken = default) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + var names = await Store.GetDisplayNamesAsync(scope, cancellationToken); + if (names == null || names.Count == 0) + { + return ImmutableDictionary.Create(); + } + + return names; + } + /// /// Retrieves the unique identifier associated with a scope. /// @@ -458,6 +511,126 @@ namespace OpenIddict.Core return Store.GetIdAsync(scope, cancellationToken); } + /// + /// Retrieves the localized display name associated with an scope + /// 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. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the matching display name associated with the scope. + /// + public virtual ValueTask GetLocalizedDisplayNameAsync([NotNull] TScope scope, CancellationToken cancellationToken = default) + => GetLocalizedDisplayNameAsync(scope, CultureInfo.CurrentUICulture, cancellationToken); + + /// + /// Retrieves the localized display name associated with an scope + /// and corresponding to the specified culture or one of its parents. + /// If no matching value can be found, the non-localized value is returned. + /// + /// The scope. + /// The culture (typically ). + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the matching display name associated with the scope. + /// + public virtual async ValueTask GetLocalizedDisplayNameAsync( + [NotNull] TScope scope, [NotNull] CultureInfo culture, CancellationToken cancellationToken = default) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (culture == null) + { + throw new ArgumentNullException(nameof(culture)); + } + + var names = await Store.GetDisplayNamesAsync(scope, cancellationToken); + if (names == null || names.IsEmpty) + { + return await Store.GetDisplayNameAsync(scope, cancellationToken); + } + + do + { + if (names.TryGetValue(culture, out var name)) + { + return name; + } + + culture = culture.Parent; + } + + while (culture != CultureInfo.InvariantCulture); + + return await Store.GetDisplayNameAsync(scope, cancellationToken); + } + + /// + /// Retrieves the localized description associated with an scope + /// 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. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the matching localized description associated with the scope. + /// + public virtual ValueTask GetLocalizedDescriptionAsync([NotNull] TScope scope, CancellationToken cancellationToken = default) + => GetLocalizedDescriptionAsync(scope, CultureInfo.CurrentUICulture, cancellationToken); + + /// + /// Retrieves the localized description associated with an scope + /// and corresponding to the specified culture or one of its parents. + /// If no matching value can be found, the non-localized value is returned. + /// + /// The scope. + /// The culture (typically ). + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the matching localized description associated with the scope. + /// + public virtual async ValueTask GetLocalizedDescriptionAsync( + [NotNull] TScope scope, [NotNull] CultureInfo culture, CancellationToken cancellationToken = default) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (culture == null) + { + throw new ArgumentNullException(nameof(culture)); + } + + var descriptions = await Store.GetDescriptionsAsync(scope, cancellationToken); + if (descriptions == null || descriptions.IsEmpty) + { + return await Store.GetDescriptionAsync(scope, cancellationToken); + } + + do + { + if (descriptions.TryGetValue(culture, out var description)) + { + return description; + } + + culture = culture.Parent; + } + + while (culture != CultureInfo.InvariantCulture); + + return await Store.GetDescriptionAsync(scope, cancellationToken); + } + /// /// Retrieves the name associated with a scope. /// @@ -592,7 +765,9 @@ namespace OpenIddict.Core } await Store.SetDescriptionAsync(scope, descriptor.Description, cancellationToken); + await Store.SetDescriptionsAsync(scope, descriptor.Descriptions.ToImmutableDictionary(), cancellationToken); await Store.SetDisplayNameAsync(scope, descriptor.DisplayName, cancellationToken); + await Store.SetDisplayNamesAsync(scope, descriptor.DisplayNames.ToImmutableDictionary(), cancellationToken); await Store.SetNameAsync(scope, descriptor.Name, cancellationToken); await Store.SetResourcesAsync(scope, descriptor.Resources.ToImmutableArray(), cancellationToken); } @@ -625,6 +800,18 @@ namespace OpenIddict.Core descriptor.Name = await Store.GetNameAsync(scope, cancellationToken); descriptor.Resources.Clear(); descriptor.Resources.UnionWith(await Store.GetResourcesAsync(scope, cancellationToken)); + + descriptor.DisplayNames.Clear(); + foreach (var pair in await Store.GetDisplayNamesAsync(scope, cancellationToken)) + { + descriptor.DisplayNames.Add(pair.Key, pair.Value); + } + + descriptor.Descriptions.Clear(); + foreach (var pair in await Store.GetDescriptionsAsync(scope, cancellationToken)) + { + descriptor.Descriptions.Add(pair.Key, pair.Value); + } } /// @@ -784,12 +971,30 @@ namespace OpenIddict.Core ValueTask IOpenIddictScopeManager.GetDescriptionAsync(object scope, CancellationToken cancellationToken) => GetDescriptionAsync((TScope) scope, cancellationToken); + ValueTask> IOpenIddictScopeManager.GetDescriptionsAsync(object scope, CancellationToken cancellationToken) + => GetDescriptionsAsync((TScope) scope, cancellationToken); + ValueTask IOpenIddictScopeManager.GetDisplayNameAsync(object scope, CancellationToken cancellationToken) => GetDisplayNameAsync((TScope) scope, cancellationToken); + ValueTask> IOpenIddictScopeManager.GetDisplayNamesAsync(object scope, CancellationToken cancellationToken) + => GetDisplayNamesAsync((TScope) scope, cancellationToken); + ValueTask IOpenIddictScopeManager.GetIdAsync(object scope, CancellationToken cancellationToken) => GetIdAsync((TScope) scope, cancellationToken); + ValueTask IOpenIddictScopeManager.GetLocalizedDescriptionAsync(object scope, CancellationToken cancellationToken) + => GetLocalizedDescriptionAsync((TScope) scope, cancellationToken); + + ValueTask IOpenIddictScopeManager.GetLocalizedDescriptionAsync(object scope, CultureInfo culture, CancellationToken cancellationToken) + => GetLocalizedDescriptionAsync((TScope) scope, culture, cancellationToken); + + ValueTask IOpenIddictScopeManager.GetLocalizedDisplayNameAsync(object scope, CancellationToken cancellationToken) + => GetLocalizedDisplayNameAsync((TScope) scope, cancellationToken); + + ValueTask IOpenIddictScopeManager.GetLocalizedDisplayNameAsync(object scope, CultureInfo culture, CancellationToken cancellationToken) + => GetLocalizedDisplayNameAsync((TScope) scope, culture, cancellationToken); + ValueTask IOpenIddictScopeManager.GetNameAsync(object scope, CancellationToken cancellationToken) => GetNameAsync((TScope) scope, cancellationToken); diff --git a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs index d8812432..f9e9b29d 100644 --- a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs +++ b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs @@ -63,6 +63,13 @@ namespace OpenIddict.EntityFramework.Models /// public virtual string DisplayName { get; set; } + /// + /// Gets or sets the localized display names + /// associated with the current application, + /// serialized as a JSON object. + /// + public virtual string DisplayNames { get; set; } + /// /// Gets or sets the unique identifier /// associated with the current application. diff --git a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkScope.cs b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkScope.cs index b9c14066..9f719c28 100644 --- a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkScope.cs +++ b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkScope.cs @@ -38,12 +38,25 @@ namespace OpenIddict.EntityFramework.Models /// public virtual string Description { get; set; } + /// + /// Gets or sets the localized public descriptions associated + /// with the current scope, serialized as a JSON object. + /// + public virtual string Descriptions { get; set; } + /// /// Gets or sets the display name /// associated with the current scope. /// public virtual string DisplayName { get; set; } + /// + /// Gets or sets the localized display names + /// associated with the current application, + /// serialized as a JSON object. + /// + public virtual string DisplayNames { get; set; } + /// /// Gets or sets the unique identifier /// associated with the current scope. diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkApplicationStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkApplicationStore.cs index b0ade607..ef7bfa77 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkApplicationStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkApplicationStore.cs @@ -11,6 +11,8 @@ using System.ComponentModel; using System.Data; using System.Data.Entity; using System.Data.Entity.Infrastructure; +using System.Globalization; +using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Text; @@ -484,6 +486,42 @@ namespace OpenIddict.EntityFramework return new ValueTask(application.DisplayName); } + /// + /// Retrieves the localized display names associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the localized display names associated with the application. + /// + public virtual ValueTask> GetDisplayNamesAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (string.IsNullOrEmpty(application.DisplayNames)) + { + return new ValueTask>(ImmutableDictionary.Create()); + } + + // 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("7762c378-c113-4564-b14b-1402b3949aaa", "\x1e", application.DisplayNames); + var names = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + return JsonSerializer.Deserialize>(application.DisplayNames) + .ToImmutableDictionary(name => CultureInfo.GetCultureInfo(name.Key), name => name.Value); + }); + + return new ValueTask>(names); + } + /// /// Retrieves the unique identifier associated with an application. /// @@ -852,6 +890,51 @@ namespace OpenIddict.EntityFramework return default; } + /// + /// Sets the localized display names associated with an application. + /// + /// The application. + /// The localized display names associated with the application. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + public virtual ValueTask SetDisplayNamesAsync([NotNull] TApplication application, + [CanBeNull] ImmutableDictionary names, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (names == null || names.IsEmpty) + { + application.DisplayNames = null; + + return default; + } + + 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(); + + application.DisplayNames = Encoding.UTF8.GetString(stream.ToArray()); + + return default; + } + /// /// Sets the permissions associated with an application. /// diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkScopeStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkScopeStore.cs index b1ac4fa4..c43a3c24 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkScopeStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkScopeStore.cs @@ -10,6 +10,8 @@ using System.Collections.Immutable; using System.ComponentModel; using System.Data.Entity; using System.Data.Entity.Infrastructure; +using System.Globalization; +using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Text; @@ -311,6 +313,42 @@ namespace OpenIddict.EntityFramework return new ValueTask(scope.Description); } + /// + /// Retrieves the localized descriptions associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the localized descriptions associated with the specified scope. + /// + public virtual ValueTask> GetDescriptionsAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (string.IsNullOrEmpty(scope.Descriptions)) + { + return new ValueTask>(ImmutableDictionary.Create()); + } + + // 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("42891062-8f69-43ba-9111-db7e8ded2553", "\x1e", scope.Descriptions); + var descriptions = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + return JsonSerializer.Deserialize>(scope.Descriptions) + .ToImmutableDictionary(description => CultureInfo.GetCultureInfo(description.Key), description => description.Value); + }); + + return new ValueTask>(descriptions); + } + /// /// Retrieves the display name associated with a scope. /// @@ -330,6 +368,42 @@ namespace OpenIddict.EntityFramework return new ValueTask(scope.DisplayName); } + /// + /// Retrieves the localized display names associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the localized display names associated with the scope. + /// + public virtual ValueTask> GetDisplayNamesAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (string.IsNullOrEmpty(scope.DisplayNames)) + { + return new ValueTask>(ImmutableDictionary.Create()); + } + + // 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("e17d437b-bdd2-43f3-974e-46d524f4bae1", "\x1e", scope.DisplayNames); + var names = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + return JsonSerializer.Deserialize>(scope.DisplayNames) + .ToImmutableDictionary(name => CultureInfo.GetCultureInfo(name.Key), name => name.Value); + }); + + return new ValueTask>(names); + } + /// /// Retrieves the unique identifier associated with a scope. /// @@ -529,6 +603,51 @@ namespace OpenIddict.EntityFramework return default; } + /// + /// Sets the localized descriptions associated with a scope. + /// + /// The scope. + /// The localized descriptions associated with the authorization. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + public virtual ValueTask SetDescriptionsAsync([NotNull] TScope scope, + [CanBeNull] ImmutableDictionary descriptions, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (descriptions == null || descriptions.IsEmpty) + { + scope.Descriptions = null; + + return default; + } + + 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(); + + scope.Descriptions = Encoding.UTF8.GetString(stream.ToArray()); + + return default; + } + /// /// Sets the display name associated with a scope. /// @@ -548,6 +667,51 @@ namespace OpenIddict.EntityFramework return default; } + /// + /// Sets the localized display names associated with a scope. + /// + /// The scope. + /// The localized display names associated with the scope. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + public virtual ValueTask SetDisplayNamesAsync([NotNull] TScope scope, + [CanBeNull] ImmutableDictionary names, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (names == null || names.IsEmpty) + { + scope.DisplayNames = null; + + return default; + } + + 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(); + + scope.DisplayNames = Encoding.UTF8.GetString(stream.ToArray()); + + return default; + } + /// /// Sets the name associated with a scope. /// diff --git a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreApplication.cs b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreApplication.cs index 45e86c23..3188a9ec 100644 --- a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreApplication.cs +++ b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreApplication.cs @@ -71,6 +71,13 @@ namespace OpenIddict.EntityFrameworkCore.Models /// public virtual string DisplayName { get; set; } + /// + /// Gets or sets the localized display names + /// associated with the current application, + /// serialized as a JSON object. + /// + public virtual string DisplayNames { get; set; } + /// /// Gets or sets the unique identifier /// associated with the current application. diff --git a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreScope.cs b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreScope.cs index 2acac51e..a1e0e9ce 100644 --- a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreScope.cs +++ b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreScope.cs @@ -38,12 +38,25 @@ namespace OpenIddict.EntityFrameworkCore.Models /// public virtual string Description { get; set; } + /// + /// Gets or sets the localized public descriptions associated + /// with the current scope, serialized as a JSON object. + /// + public virtual string Descriptions { get; set; } + /// /// Gets or sets the display name /// associated with the current scope. /// public virtual string DisplayName { get; set; } + /// + /// Gets or sets the localized display names + /// associated with the current application, + /// serialized as a JSON object. + /// + public virtual string DisplayNames { get; set; } + /// /// Gets or sets the unique identifier /// associated with the current scope. diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs index 72068d10..8c885386 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs @@ -9,6 +9,8 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel; using System.Data; +using System.Globalization; +using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Text; @@ -528,6 +530,42 @@ namespace OpenIddict.EntityFrameworkCore return new ValueTask(application.DisplayName); } + /// + /// Retrieves the localized display names associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the localized display names associated with the application. + /// + public virtual ValueTask> GetDisplayNamesAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (string.IsNullOrEmpty(application.DisplayNames)) + { + return new ValueTask>(ImmutableDictionary.Create()); + } + + // 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("7762c378-c113-4564-b14b-1402b3949aaa", "\x1e", application.DisplayNames); + var names = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + return JsonSerializer.Deserialize>(application.DisplayNames) + .ToImmutableDictionary(name => CultureInfo.GetCultureInfo(name.Key), name => name.Value); + }); + + return new ValueTask>(names); + } + /// /// Retrieves the unique identifier associated with an application. /// @@ -896,6 +934,51 @@ namespace OpenIddict.EntityFrameworkCore return default; } + /// + /// Sets the localized display names associated with an application. + /// + /// The application. + /// The localized display names associated with the application. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + public virtual ValueTask SetDisplayNamesAsync([NotNull] TApplication application, + [CanBeNull] ImmutableDictionary names, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (names == null || names.IsEmpty) + { + application.DisplayNames = null; + + return default; + } + + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + Indented = false + }); + + writer.WriteStartObject(); + + foreach (var pair in names) + { + writer.WritePropertyName(pair.Key.Name); + writer.WriteStringValue(pair.Value); + } + + writer.WriteEndObject(); + writer.Flush(); + + application.DisplayNames = Encoding.UTF8.GetString(stream.ToArray()); + + return default; + } + /// /// Sets the permissions associated with an application. /// diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreScopeStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreScopeStore.cs index b3527d67..305f60f6 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreScopeStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreScopeStore.cs @@ -8,6 +8,8 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel; +using System.Globalization; +using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Text; @@ -327,6 +329,42 @@ namespace OpenIddict.EntityFrameworkCore return new ValueTask(scope.Description); } + /// + /// Retrieves the localized descriptions associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the localized descriptions associated with the specified scope. + /// + public virtual ValueTask> GetDescriptionsAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (string.IsNullOrEmpty(scope.Descriptions)) + { + return new ValueTask>(ImmutableDictionary.Create()); + } + + // 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("42891062-8f69-43ba-9111-db7e8ded2553", "\x1e", scope.Descriptions); + var descriptions = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + return JsonSerializer.Deserialize>(scope.Descriptions) + .ToImmutableDictionary(description => CultureInfo.GetCultureInfo(description.Key), description => description.Value); + }); + + return new ValueTask>(descriptions); + } + /// /// Retrieves the display name associated with a scope. /// @@ -346,6 +384,42 @@ namespace OpenIddict.EntityFrameworkCore return new ValueTask(scope.DisplayName); } + /// + /// Retrieves the localized display names associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the localized display names associated with the scope. + /// + public virtual ValueTask> GetDisplayNamesAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (string.IsNullOrEmpty(scope.DisplayNames)) + { + return new ValueTask>(ImmutableDictionary.Create()); + } + + // 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("e17d437b-bdd2-43f3-974e-46d524f4bae1", "\x1e", scope.DisplayNames); + var names = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + return JsonSerializer.Deserialize>(scope.DisplayNames) + .ToImmutableDictionary(name => CultureInfo.GetCultureInfo(name.Key), name => name.Value); + }); + + return new ValueTask>(names); + } + /// /// Retrieves the unique identifier associated with a scope. /// @@ -545,6 +619,51 @@ namespace OpenIddict.EntityFrameworkCore return default; } + /// + /// Sets the localized descriptions associated with a scope. + /// + /// The scope. + /// The localized descriptions associated with the authorization. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + public virtual ValueTask SetDescriptionsAsync([NotNull] TScope scope, + [CanBeNull] ImmutableDictionary descriptions, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (descriptions == null || descriptions.IsEmpty) + { + scope.Descriptions = null; + + return default; + } + + 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(); + + scope.Descriptions = Encoding.UTF8.GetString(stream.ToArray()); + + return default; + } + /// /// Sets the display name associated with a scope. /// @@ -564,6 +683,51 @@ namespace OpenIddict.EntityFrameworkCore return default; } + /// + /// Sets the localized display names associated with a scope. + /// + /// The scope. + /// The localized display names associated with the scope. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + public virtual ValueTask SetDisplayNamesAsync([NotNull] TScope scope, + [CanBeNull] ImmutableDictionary names, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (names == null || names.IsEmpty) + { + scope.DisplayNames = null; + + return default; + } + + 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(); + + scope.DisplayNames = Encoding.UTF8.GetString(stream.ToArray()); + + return default; + } + /// /// Sets the name associated with a scope. /// diff --git a/src/OpenIddict.MongoDb.Models/OpenIddict.MongoDb.Models.csproj b/src/OpenIddict.MongoDb.Models/OpenIddict.MongoDb.Models.csproj index 0b6fd067..bfde10fb 100644 --- a/src/OpenIddict.MongoDb.Models/OpenIddict.MongoDb.Models.csproj +++ b/src/OpenIddict.MongoDb.Models/OpenIddict.MongoDb.Models.csproj @@ -13,6 +13,7 @@ + diff --git a/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbApplication.cs b/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbApplication.cs index e885c4a5..148a5252 100644 --- a/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbApplication.cs +++ b/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbApplication.cs @@ -5,7 +5,10 @@ */ using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; +using System.Globalization; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; @@ -52,6 +55,14 @@ namespace OpenIddict.MongoDb.Models [BsonElement("display_name"), BsonIgnoreIfNull] public virtual string DisplayName { get; set; } + /// + /// Gets or sets the localized display names + /// associated with the current application. + /// + [BsonElement("display_names"), BsonIgnoreIfNull] + public virtual IReadOnlyDictionary DisplayNames { get; set; } + = ImmutableDictionary.Create(); + /// /// Gets or sets the unique identifier /// associated with the current application. @@ -63,13 +74,13 @@ namespace OpenIddict.MongoDb.Models /// Gets or sets the permissions associated with the current application. /// [BsonElement("permissions"), BsonIgnoreIfDefault] - public virtual string[] Permissions { get; set; } = Array.Empty(); + public virtual IReadOnlyList Permissions { get; set; } = ImmutableArray.Create(); /// /// Gets or sets the logout callback URLs associated with the current application. /// [BsonElement("post_logout_redirect_uris"), BsonIgnoreIfDefault] - public virtual string[] PostLogoutRedirectUris { get; set; } = Array.Empty(); + public virtual IReadOnlyList PostLogoutRedirectUris { get; set; } = ImmutableArray.Create(); /// /// Gets or sets the additional properties associated with the current application. @@ -81,13 +92,13 @@ namespace OpenIddict.MongoDb.Models /// Gets or sets the callback URLs associated with the current application. /// [BsonElement("redirect_uris"), BsonIgnoreIfDefault] - public virtual string[] RedirectUris { get; set; } = Array.Empty(); + public virtual IReadOnlyList RedirectUris { get; set; } = ImmutableArray.Create(); /// /// Gets or sets the requirements associated with the current application. /// [BsonElement("requirements"), BsonIgnoreIfDefault] - public virtual string[] Requirements { get; set; } = Array.Empty(); + public virtual IReadOnlyList Requirements { get; set; } = ImmutableArray.Create(); /// /// Gets or sets the application type diff --git a/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbAuthorization.cs b/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbAuthorization.cs index 19ef8faf..f3b646b4 100644 --- a/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbAuthorization.cs +++ b/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbAuthorization.cs @@ -5,6 +5,8 @@ */ using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; @@ -47,7 +49,7 @@ namespace OpenIddict.MongoDb.Models /// Gets or sets the scopes associated with the current authorization. /// [BsonElement("scopes"), BsonIgnoreIfDefault] - public virtual string[] Scopes { get; set; } = Array.Empty(); + public virtual IReadOnlyList Scopes { get; set; } = ImmutableArray.Create(); /// /// Gets or sets the status of the current authorization. diff --git a/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbScope.cs b/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbScope.cs index 413e50fd..f23ee263 100644 --- a/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbScope.cs +++ b/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbScope.cs @@ -5,7 +5,10 @@ */ using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; +using System.Globalization; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; @@ -30,6 +33,14 @@ namespace OpenIddict.MongoDb.Models [BsonElement("description"), BsonIgnoreIfNull] public virtual string Description { get; set; } + /// + /// Gets or sets the localized public descriptions + /// associated with the current scope. + /// + [BsonElement("descriptions"), BsonIgnoreIfNull] + public virtual IReadOnlyDictionary Descriptions { get; set; } + = ImmutableDictionary.Create(); + /// /// Gets or sets the display name /// associated with the current scope. @@ -37,6 +48,14 @@ namespace OpenIddict.MongoDb.Models [BsonElement("display_name"), BsonIgnoreIfNull] public virtual string DisplayName { get; set; } + /// + /// Gets or sets the localized display names + /// associated with the current scope. + /// + [BsonElement("display_names"), BsonIgnoreIfNull] + public virtual IReadOnlyDictionary DisplayNames { get; set; } + = ImmutableDictionary.Create(); + /// /// Gets or sets the unique identifier /// associated with the current scope. @@ -61,6 +80,6 @@ namespace OpenIddict.MongoDb.Models /// Gets or sets the resources associated with the current scope. /// [BsonElement("resources"), BsonIgnoreIfDefault] - public virtual string[] Resources { get; set; } = Array.Empty(); + public virtual IReadOnlyList Resources { get; set; } = ImmutableArray.Create(); } } diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbApplicationStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbApplicationStore.cs index 1e7f484c..7a0a4272 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbApplicationStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbApplicationStore.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Globalization; using System.Linq; using System.Runtime.CompilerServices; using System.Text; @@ -370,6 +371,30 @@ namespace OpenIddict.MongoDb return new ValueTask(application.DisplayName); } + /// + /// Retrieves the localized display names associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the localized display names associated with the application. + /// + public virtual ValueTask> GetDisplayNamesAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (application.DisplayNames == null || application.DisplayNames.Count == 0) + { + return new ValueTask>(ImmutableDictionary.Create()); + } + + return new ValueTask>(application.DisplayNames.ToImmutableDictionary()); + } + /// /// Retrieves the unique identifier associated with an application. /// @@ -406,7 +431,7 @@ namespace OpenIddict.MongoDb throw new ArgumentNullException(nameof(application)); } - if (application.Permissions == null || application.Permissions.Length == 0) + if (application.Permissions == null || application.Permissions.Count == 0) { return new ValueTask>(ImmutableArray.Create()); } @@ -431,7 +456,7 @@ namespace OpenIddict.MongoDb throw new ArgumentNullException(nameof(application)); } - if (application.PostLogoutRedirectUris == null || application.PostLogoutRedirectUris.Length == 0) + if (application.PostLogoutRedirectUris == null || application.PostLogoutRedirectUris.Count == 0) { return new ValueTask>(ImmutableArray.Create()); } @@ -481,7 +506,7 @@ namespace OpenIddict.MongoDb throw new ArgumentNullException(nameof(application)); } - if (application.RedirectUris == null || application.RedirectUris.Length == 0) + if (application.RedirectUris == null || application.RedirectUris.Count == 0) { return new ValueTask>(ImmutableArray.Create()); } @@ -505,7 +530,7 @@ namespace OpenIddict.MongoDb throw new ArgumentNullException(nameof(application)); } - if (application.Requirements == null || application.Requirements.Length == 0) + if (application.Requirements == null || application.Requirements.Count == 0) { return new ValueTask>(ImmutableArray.Create()); } @@ -704,6 +729,26 @@ namespace OpenIddict.MongoDb return default; } + /// + /// Sets the localized display names associated with an application. + /// + /// The application. + /// The localized display names associated with the application. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + public virtual ValueTask SetDisplayNamesAsync([NotNull] TApplication application, + [CanBeNull] ImmutableDictionary names, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + application.DisplayNames = names; + + return default; + } + /// /// Sets the permissions associated with an application. /// diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs index c33a0da0..0c5e1728 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs @@ -515,7 +515,7 @@ namespace OpenIddict.MongoDb throw new ArgumentNullException(nameof(authorization)); } - if (authorization.Scopes == null || authorization.Scopes.Length == 0) + if (authorization.Scopes == null || authorization.Scopes.Count == 0) { return new ValueTask>(ImmutableArray.Create()); } diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbScopeStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbScopeStore.cs index a1ca929b..b4d3fa51 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbScopeStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbScopeStore.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Globalization; using System.Linq; using System.Runtime.CompilerServices; using System.Text; @@ -283,6 +284,30 @@ namespace OpenIddict.MongoDb return new ValueTask(scope.Description); } + /// + /// Retrieves the localized descriptions associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the localized descriptions associated with the specified scope. + /// + public virtual ValueTask> GetDescriptionsAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (scope.Descriptions == null || scope.Descriptions.Count == 0) + { + return new ValueTask>(ImmutableDictionary.Create()); + } + + return new ValueTask>(scope.Descriptions.ToImmutableDictionary()); + } + /// /// Retrieves the display name associated with a scope. /// @@ -302,6 +327,30 @@ namespace OpenIddict.MongoDb return new ValueTask(scope.DisplayName); } + /// + /// Retrieves the localized display names associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the localized display names associated with the scope. + /// + public virtual ValueTask> GetDisplayNamesAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (scope.DisplayNames == null || scope.DisplayNames.Count == 0) + { + return new ValueTask>(ImmutableDictionary.Create()); + } + + return new ValueTask>(scope.DisplayNames.ToImmutableDictionary()); + } + /// /// Retrieves the unique identifier associated with a scope. /// @@ -381,7 +430,7 @@ namespace OpenIddict.MongoDb throw new ArgumentNullException(nameof(scope)); } - if (scope.Resources == null || scope.Resources.Length == 0) + if (scope.Resources == null || scope.Resources.Count == 0) { return new ValueTask>(ImmutableArray.Create()); } @@ -497,6 +546,46 @@ namespace OpenIddict.MongoDb return default; } + /// + /// Sets the localized descriptions associated with a scope. + /// + /// The scope. + /// The localized descriptions associated with the authorization. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + public virtual ValueTask SetDescriptionsAsync([NotNull] TScope scope, + [CanBeNull] ImmutableDictionary descriptions, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + scope.Descriptions = descriptions; + + return default; + } + + /// + /// Sets the localized display names associated with a scope. + /// + /// The scope. + /// The localized display names associated with the scope. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + public virtual ValueTask SetDisplayNamesAsync([NotNull] TScope scope, + [CanBeNull] ImmutableDictionary names, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + scope.DisplayNames = names; + + return default; + } + /// /// Sets the display name associated with a scope. ///