From bd4dc32f51ffd87c0a04c5ce076d79a6a2b99d74 Mon Sep 17 00:00:00 2001 From: Ghaith Prosoft Date: Mon, 12 Jan 2026 14:09:47 +0300 Subject: [PATCH 1/5] Add support for managing organizations in Keycloak Introduced a new `IKcOrganizations` interface and its implementation in `KcOrganizations` to enable CRUD operations, listing, and filtering of organizations in Keycloak. Updated the `IKeycloakClient` and `KeycloakClient` classes to expose the `Organizations` client. Added supporting models: - `KcOrganization` to represent organization resources. - `KcOrganizationDomain` to represent domain information. - `KcOrganizationFilter` to enable filtering options for queries. Updated `NETCore.Keycloak.Client.csproj` to clean up formatting and remove unnecessary metadata files. --- .../Abstraction/IKcOrganizations.cs | 108 ++++++++++++++ .../Abstraction/IKeycloakClient.cs | 5 + .../Implementation/KcOrganizations.cs | 136 ++++++++++++++++++ .../Implementation/KeycloakClient.cs | 4 + .../Models/Organizations/KcOrganization.cs | 59 ++++++++ .../Organizations/KcOrganizationDomain.cs | 21 +++ .../Organizations/KcOrganizationFilter.cs | 114 +++++++++++++++ .../NETCore.Keycloak.Client.csproj | 23 ++- 8 files changed, 458 insertions(+), 12 deletions(-) create mode 100644 NETCore.Keycloak.Client/HttpClients/Abstraction/IKcOrganizations.cs create mode 100644 NETCore.Keycloak.Client/HttpClients/Implementation/KcOrganizations.cs create mode 100644 NETCore.Keycloak.Client/Models/Organizations/KcOrganization.cs create mode 100644 NETCore.Keycloak.Client/Models/Organizations/KcOrganizationDomain.cs create mode 100644 NETCore.Keycloak.Client/Models/Organizations/KcOrganizationFilter.cs diff --git a/NETCore.Keycloak.Client/HttpClients/Abstraction/IKcOrganizations.cs b/NETCore.Keycloak.Client/HttpClients/Abstraction/IKcOrganizations.cs new file mode 100644 index 0000000..2911a4d --- /dev/null +++ b/NETCore.Keycloak.Client/HttpClients/Abstraction/IKcOrganizations.cs @@ -0,0 +1,108 @@ +using NETCore.Keycloak.Client.Models; +using NETCore.Keycloak.Client.Models.Organizations; + +namespace NETCore.Keycloak.Client.HttpClients.Abstraction; + +/// +/// Keycloak organizations REST client +/// +public interface IKcOrganizations +{ + /// + /// Creates a new organization in a specified Keycloak realm. + /// + /// POST /{realm}/organizations + /// + /// The Keycloak realm where the organization will be created. + /// The access token used for authentication. + /// The organization representation to create. + /// Optional cancellation token. + /// A indicating the result. + Task> CreateAsync( + string realm, + string accessToken, + KcOrganization organization, + CancellationToken cancellationToken = default); + + /// + /// Updates an existing organization in a specified Keycloak realm. + /// + /// PUT /{realm}/organizations/{organizationId} + /// + /// The Keycloak realm where the organization exists. + /// The access token used for authentication. + /// The ID of the organization to update. + /// The updated organization representation. + /// Optional cancellation token. + /// A indicating the result. + Task> UpdateAsync( + string realm, + string accessToken, + string organizationId, + KcOrganization organization, + CancellationToken cancellationToken = default); + + /// + /// Deletes an organization from a specified Keycloak realm. + /// + /// DELETE /{realm}/organizations/{organizationId} + /// + /// The Keycloak realm where the organization exists. + /// The access token used for authentication. + /// The ID of the organization to delete. + /// Optional cancellation token. + /// A indicating the result. + Task> DeleteAsync( + string realm, + string accessToken, + string organizationId, + CancellationToken cancellationToken = default); + + /// + /// Retrieves a specific organization by its ID from a specified Keycloak realm. + /// + /// GET /{realm}/organizations/{organizationId} + /// + /// The Keycloak realm to query. + /// The access token used for authentication. + /// The ID of the organization to retrieve. + /// Optional cancellation token. + /// A with the organization details. + Task> GetAsync( + string realm, + string accessToken, + string organizationId, + CancellationToken cancellationToken = default); + + /// + /// Retrieves a list of organizations from a specified Keycloak realm, optionally filtered by criteria. + /// + /// GET /{realm}/organizations + /// + /// The Keycloak realm from which organizations will be listed. + /// The access token used for authentication. + /// Optional filter criteria. + /// Optional cancellation token. + /// A containing an enumerable of organizations. + Task>> ListAsync( + string realm, + string accessToken, + KcOrganizationFilter filter = null, + CancellationToken cancellationToken = default); + + /// + /// Retrieves the count of organizations in a specified Keycloak realm, optionally filtered. + /// + /// GET /{realm}/organizations/count + /// + /// The Keycloak realm to query. + /// The access token used for authentication. + /// Optional filter criteria. + /// Optional cancellation token. + /// A with the count of organizations. + Task> CountAsync( + string realm, + string accessToken, + KcOrganizationFilter filter = null, + CancellationToken cancellationToken = default); +} diff --git a/NETCore.Keycloak.Client/HttpClients/Abstraction/IKeycloakClient.cs b/NETCore.Keycloak.Client/HttpClients/Abstraction/IKeycloakClient.cs index 70409ea..d5edde5 100644 --- a/NETCore.Keycloak.Client/HttpClients/Abstraction/IKeycloakClient.cs +++ b/NETCore.Keycloak.Client/HttpClients/Abstraction/IKeycloakClient.cs @@ -76,4 +76,9 @@ public interface IKeycloakClient /// See for detailed operations. /// public IKcScopeMappings ScopeMappings { get; } + + /// + /// Gets the organizations REST client for managing organizations. + /// + public IKcOrganizations Organizations { get; } } diff --git a/NETCore.Keycloak.Client/HttpClients/Implementation/KcOrganizations.cs b/NETCore.Keycloak.Client/HttpClients/Implementation/KcOrganizations.cs new file mode 100644 index 0000000..036ed76 --- /dev/null +++ b/NETCore.Keycloak.Client/HttpClients/Implementation/KcOrganizations.cs @@ -0,0 +1,136 @@ +using Microsoft.Extensions.Logging; +using NETCore.Keycloak.Client.HttpClients.Abstraction; +using NETCore.Keycloak.Client.Models; +using NETCore.Keycloak.Client.Models.Organizations; + +namespace NETCore.Keycloak.Client.HttpClients.Implementation; + +/// +/// Organization service API for managing organizations in Keycloak. +/// +internal sealed class KcOrganizations(string baseUrl, ILogger logger) : KcHttpClientBase(logger, baseUrl), IKcOrganizations +{ + // Primary constructor on the class declaration is used; no explicit ctor body required. + + public Task> CreateAsync( + string realm, + string accessToken, + KcOrganization organization, + CancellationToken cancellationToken = default) + { + ValidateAccess(realm, accessToken); + ValidateNotNull(nameof(organization), organization); + + var url = $"{BaseUrl}/{realm}/organizations"; + return ProcessRequestAsync( + url, + HttpMethod.Post, + accessToken, + "Unable to create organization", + organization, + "application/json", + cancellationToken); + } + + public Task> UpdateAsync( + string realm, + string accessToken, + string organizationId, + KcOrganization organization, + CancellationToken cancellationToken = default) + { + ValidateAccess(realm, accessToken); + ValidateRequiredString(nameof(organizationId), organizationId); + ValidateNotNull(nameof(organization), organization); + + var url = $"{BaseUrl}/{realm}/organizations/{organizationId}"; + return ProcessRequestAsync( + url, + HttpMethod.Put, + accessToken, + "Unable to update organization", + organization, + "application/json", + cancellationToken); + } + + public Task> DeleteAsync( + string realm, + string accessToken, + string organizationId, + CancellationToken cancellationToken = default) + { + ValidateAccess(realm, accessToken); + ValidateRequiredString(nameof(organizationId), organizationId); + + var url = $"{BaseUrl}/{realm}/organizations/{organizationId}"; + return ProcessRequestAsync( + url, + HttpMethod.Delete, + accessToken, + "Unable to delete organization", + null, + "application/json", + cancellationToken); + } + + public Task> GetAsync( + string realm, + string accessToken, + string organizationId, + CancellationToken cancellationToken = default) + { + ValidateAccess(realm, accessToken); + ValidateRequiredString(nameof(organizationId), organizationId); + + var url = $"{BaseUrl}/{realm}/organizations/{organizationId}"; + return ProcessRequestAsync( + url, + HttpMethod.Get, + accessToken, + "Unable to get organization", + null, + "application/json", + cancellationToken); + } + + public Task>> ListAsync( + string realm, + string accessToken, + KcOrganizationFilter filter = null, + CancellationToken cancellationToken = default) + { + ValidateAccess(realm, accessToken); + filter ??= new KcOrganizationFilter(); + + var url = $"{BaseUrl}/{realm}/organizations{filter.BuildQuery()}"; + return ProcessRequestAsync>( + url, + HttpMethod.Get, + accessToken, + "Unable to list organizations", + null, + "application/json", + cancellationToken); + } + + public Task> CountAsync( + string realm, + string accessToken, + KcOrganizationFilter filter = null, + CancellationToken cancellationToken = default) + { + ValidateAccess(realm, accessToken); + filter ??= new KcOrganizationFilter(); + + var url = $"{BaseUrl}/{realm}/organizations/count{filter.BuildQuery()}"; + return ProcessRequestAsync( + url, + HttpMethod.Get, + accessToken, + "Unable to count organizations", + null, + "application/json", + cancellationToken); + } +} diff --git a/NETCore.Keycloak.Client/HttpClients/Implementation/KeycloakClient.cs b/NETCore.Keycloak.Client/HttpClients/Implementation/KeycloakClient.cs index a62d077..9ced037 100644 --- a/NETCore.Keycloak.Client/HttpClients/Implementation/KeycloakClient.cs +++ b/NETCore.Keycloak.Client/HttpClients/Implementation/KeycloakClient.cs @@ -44,6 +44,9 @@ public sealed class KeycloakClient : IKeycloakClient /// public IKcScopeMappings ScopeMappings { get; } + /// + public IKcOrganizations Organizations { get; } + /// /// Initializes a new instance of the class. /// Provides access to various Keycloak API services through respective clients. @@ -86,5 +89,6 @@ public KeycloakClient(string baseUrl, ILogger logger = null) ProtocolMappers = new KcProtocolMappers(adminUrl, logger); ScopeMappings = new KcScopeMappings(adminUrl, logger); RoleMappings = new KcRoleMappings(adminUrl, logger); + Organizations = new KcOrganizations(adminUrl, logger); } } diff --git a/NETCore.Keycloak.Client/Models/Organizations/KcOrganization.cs b/NETCore.Keycloak.Client/Models/Organizations/KcOrganization.cs new file mode 100644 index 0000000..7704067 --- /dev/null +++ b/NETCore.Keycloak.Client/Models/Organizations/KcOrganization.cs @@ -0,0 +1,59 @@ +using System.Text.Json.Serialization; + +namespace NETCore.Keycloak.Client.Models.Organizations; + +/// +/// Represents an organization resource. +/// +public sealed class KcOrganization +{ + /// + /// Gets or sets the organization id. + /// + [JsonPropertyName("id")] + public string Id { get; set; } + + /// + /// Gets or sets the organization name. + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// Gets or sets the organization alias. + /// + [JsonPropertyName("alias")] + public string Alias { get; set; } + + /// + /// Gets or sets a value indicating whether the organization is enabled. + /// + [JsonPropertyName("enabled")] + public bool? Enabled { get; set; } + + /// + /// Gets or sets the organization description. + /// + [JsonPropertyName("description")] + public string Description { get; set; } + + /// + /// Gets or sets the redirect URL for the organization. + /// + [JsonPropertyName("redirectUrl")] + public string RedirectUrl { get; set; } + + /// + /// Custom attributes. + /// Key = attribute name + /// Value = list of values (Keycloak style) + /// + [JsonPropertyName("attributes")] + public Dictionary> Attributes { get; set; } + + /// + /// Gets or sets the organization domains. + /// + [JsonPropertyName("domains")] + public List Domains { get; set; } = new(); +} diff --git a/NETCore.Keycloak.Client/Models/Organizations/KcOrganizationDomain.cs b/NETCore.Keycloak.Client/Models/Organizations/KcOrganizationDomain.cs new file mode 100644 index 0000000..7a0b79e --- /dev/null +++ b/NETCore.Keycloak.Client/Models/Organizations/KcOrganizationDomain.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace NETCore.Keycloak.Client.Models.Organizations; + +/// +/// Represents organization domain information. +/// +public sealed class KcOrganizationDomain +{ + /// + /// Gets or sets the domain name. + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// Gets or sets a value indicating whether the domain is verified. + /// + [JsonPropertyName("verified")] + public bool? Verified { get; set; } +} diff --git a/NETCore.Keycloak.Client/Models/Organizations/KcOrganizationFilter.cs b/NETCore.Keycloak.Client/Models/Organizations/KcOrganizationFilter.cs new file mode 100644 index 0000000..693294d --- /dev/null +++ b/NETCore.Keycloak.Client/Models/Organizations/KcOrganizationFilter.cs @@ -0,0 +1,114 @@ +using System.Globalization; +using System.Text; +using System.Text.Json.Serialization; +using NETCore.Keycloak.Client.Models.Common; + +namespace NETCore.Keycloak.Client.Models.Organizations; + +/// +/// Filter model for organizations queries. +/// +public sealed class KcOrganizationFilter : KcFilter +{ + /// + /// Filter by organization name (partial or exact if Exact = true) + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// Filter by organization alias + /// + [JsonPropertyName("alias")] + public string Alias { get; set; } + + /// + /// Filter by enabled state + /// + [JsonPropertyName("enabled")] + public bool? Enabled { get; set; } + + /// + /// Filter by domain name + /// + [JsonPropertyName("domain")] + public string Domain { get; set; } + + /// + /// Filter by verified domain + /// + [JsonPropertyName("domainVerified")] + public bool? DomainVerified { get; set; } + + /// + /// Require exact matching for name / alias + /// + [JsonPropertyName("exact")] + public bool? Exact { get; set; } + + /// + /// Free text search (Keycloak standard search param) + /// + [JsonPropertyName("search")] + public string Search { get; set; } + + /// + /// Build query string + /// + public new string BuildQuery() + { + var sb = new StringBuilder($"?max={Max}"); + + if ( BriefRepresentation.HasValue ) + { + _ = sb.Append("&briefRepresentation=") + .Append(BriefRepresentation.Value.ToString().ToLower(CultureInfo.CurrentCulture)); + } + + if ( !string.IsNullOrWhiteSpace(Name) ) + { + _ = sb.Append("&name=").Append(Name); + } + + if ( !string.IsNullOrWhiteSpace(Alias) ) + { + _ = sb.Append("&alias=").Append(Alias); + } + + if ( Enabled.HasValue ) + { + _ = sb.Append("&enabled=") + .Append(Enabled.Value.ToString().ToLower(CultureInfo.CurrentCulture)); + } + + if ( !string.IsNullOrWhiteSpace(Domain) ) + { + _ = sb.Append("&domain=").Append(Domain); + } + + if ( DomainVerified.HasValue ) + { + _ = sb.Append("&domainVerified=") + .Append(DomainVerified.Value.ToString().ToLower(CultureInfo.CurrentCulture)); + } + + if ( Exact.HasValue ) + { + _ = sb.Append("&exact=") + .Append(Exact.Value.ToString().ToLower(CultureInfo.CurrentCulture)); + } + + if ( First.HasValue ) + { + _ = sb.Append("&first=") + .Append(First.Value.ToString(CultureInfo.CurrentCulture)); + } + + if ( !string.IsNullOrWhiteSpace(Search) ) + { + _ = sb.Append("&search=").Append(Search); + } + + return sb.ToString(); + } +} diff --git a/NETCore.Keycloak.Client/NETCore.Keycloak.Client.csproj b/NETCore.Keycloak.Client/NETCore.Keycloak.Client.csproj index 2cbe2e8..bdd5ec3 100644 --- a/NETCore.Keycloak.Client/NETCore.Keycloak.Client.csproj +++ b/NETCore.Keycloak.Client/NETCore.Keycloak.Client.csproj @@ -28,17 +28,16 @@ - - - - - - - - - - - - + + + + + + + + + + + From dfe0064ca131bbe518c3a549eba7aa475cd6f6d1 Mon Sep 17 00:00:00 2001 From: Ghaith Prosoft Date: Tue, 13 Jan 2026 09:57:27 +0300 Subject: [PATCH 2/5] Refactor Keycloak models and enhance documentation Updated XML documentation for `KcOrganization`, `KcOrganizationDomain`, and `KcOrganizationFilter` to improve clarity and align with Keycloak API references. Added `` tags for property descriptions. Refactored `KcOrganizationFilter`: - Replaced multiple filtering properties with a generic `Q` property. - Updated `Exact` property to indicate exact match behavior. - Switched from `System.Text.Json` to `Newtonsoft.Json` for serialization. - Rewrote `BuildQuery` for cleaner and more maintainable query string construction. Improved consistency in naming conventions and descriptions across classes. Removed redundant properties and comments. Aligned code structure with Keycloak API standards. --- .../Models/Organizations/KcOrganization.cs | 32 ++++- .../Organizations/KcOrganizationDomain.cs | 10 +- .../Organizations/KcOrganizationFilter.cs | 111 ++++++------------ 3 files changed, 74 insertions(+), 79 deletions(-) diff --git a/NETCore.Keycloak.Client/Models/Organizations/KcOrganization.cs b/NETCore.Keycloak.Client/Models/Organizations/KcOrganization.cs index 7704067..08f3e37 100644 --- a/NETCore.Keycloak.Client/Models/Organizations/KcOrganization.cs +++ b/NETCore.Keycloak.Client/Models/Organizations/KcOrganization.cs @@ -3,57 +3,81 @@ namespace NETCore.Keycloak.Client.Models.Organizations; /// -/// Represents an organization resource. +/// Represents an organization resource in Keycloak. +/// /// public sealed class KcOrganization { /// /// Gets or sets the organization id. /// + /// + /// A string representing the unique identifier of the organization. + /// [JsonPropertyName("id")] public string Id { get; set; } /// /// Gets or sets the organization name. /// + /// + /// A string representing the display name of the organization. + /// [JsonPropertyName("name")] public string Name { get; set; } /// /// Gets or sets the organization alias. /// + /// + /// A string representing an alternate identifier or alias for the organization. + /// [JsonPropertyName("alias")] public string Alias { get; set; } /// /// Gets or sets a value indicating whether the organization is enabled. /// + /// + /// A nullable boolean that is true when the organization is enabled, false when disabled, + /// or null if the enabled state is not set. + /// [JsonPropertyName("enabled")] public bool? Enabled { get; set; } /// /// Gets or sets the organization description. /// + /// + /// A string containing a human-readable description for the organization. + /// [JsonPropertyName("description")] public string Description { get; set; } /// /// Gets or sets the redirect URL for the organization. /// + /// + /// A string representing the redirect URL associated with the organization (for example, after login or registration flows). + /// [JsonPropertyName("redirectUrl")] public string RedirectUrl { get; set; } /// - /// Custom attributes. - /// Key = attribute name - /// Value = list of values (Keycloak style) + /// Custom attributes associated with the organization. /// + /// + /// A dictionary where the key is the attribute name and the value is a list of values for that attribute (Keycloak style). + /// [JsonPropertyName("attributes")] public Dictionary> Attributes { get; set; } /// /// Gets or sets the organization domains. /// + /// + /// A collection of representing domains associated with the organization. + /// [JsonPropertyName("domains")] public List Domains { get; set; } = new(); } diff --git a/NETCore.Keycloak.Client/Models/Organizations/KcOrganizationDomain.cs b/NETCore.Keycloak.Client/Models/Organizations/KcOrganizationDomain.cs index 7a0b79e..492541a 100644 --- a/NETCore.Keycloak.Client/Models/Organizations/KcOrganizationDomain.cs +++ b/NETCore.Keycloak.Client/Models/Organizations/KcOrganizationDomain.cs @@ -3,19 +3,27 @@ namespace NETCore.Keycloak.Client.Models.Organizations; /// -/// Represents organization domain information. +/// Represents an organization domain in Keycloak. +/// /// public sealed class KcOrganizationDomain { /// /// Gets or sets the domain name. /// + /// + /// A string representing the domain name (for example, "example.com"). + /// [JsonPropertyName("name")] public string Name { get; set; } /// /// Gets or sets a value indicating whether the domain is verified. /// + /// + /// A nullable boolean indicating whether the domain has been verified by Keycloak. + /// True if verified; false if not; null if the verification state is unknown. + /// [JsonPropertyName("verified")] public bool? Verified { get; set; } } diff --git a/NETCore.Keycloak.Client/Models/Organizations/KcOrganizationFilter.cs b/NETCore.Keycloak.Client/Models/Organizations/KcOrganizationFilter.cs index 693294d..df9b80d 100644 --- a/NETCore.Keycloak.Client/Models/Organizations/KcOrganizationFilter.cs +++ b/NETCore.Keycloak.Client/Models/Organizations/KcOrganizationFilter.cs @@ -1,114 +1,77 @@ using System.Globalization; using System.Text; -using System.Text.Json.Serialization; using NETCore.Keycloak.Client.Models.Common; +using Newtonsoft.Json; namespace NETCore.Keycloak.Client.Models.Organizations; /// -/// Filter model for organizations queries. +/// Represents a filter for querying Keycloak organizations. /// public sealed class KcOrganizationFilter : KcFilter { /// - /// Filter by organization name (partial or exact if Exact = true) + /// Gets or sets a query string for searching custom attributes, formatted as 'key1:value1 key2:value2'. /// - [JsonPropertyName("name")] - public string Name { get; set; } + /// + /// A string representing the custom attribute search query. + /// + [JsonProperty("q")] + public string Q { get; set; } /// - /// Filter by organization alias + /// Gets or sets a value indicating whether the query parameters must match exactly. /// - [JsonPropertyName("alias")] - public string Alias { get; set; } - - /// - /// Filter by enabled state - /// - [JsonPropertyName("enabled")] - public bool? Enabled { get; set; } - - /// - /// Filter by domain name - /// - [JsonPropertyName("domain")] - public string Domain { get; set; } - - /// - /// Filter by verified domain - /// - [JsonPropertyName("domainVerified")] - public bool? DomainVerified { get; set; } - - /// - /// Require exact matching for name / alias - /// - [JsonPropertyName("exact")] + /// + /// true if the parameters must match exactly; otherwise, false. + /// + [JsonProperty("exact")] public bool? Exact { get; set; } /// - /// Free text search (Keycloak standard search param) - /// - [JsonPropertyName("search")] - public string Search { get; set; } - - /// - /// Build query string + /// Builds the query string based on the filter properties. /// + /// + /// A string containing the query parameters to be appended to a URL. + /// public new string BuildQuery() { - var sb = new StringBuilder($"?max={Max}"); - - if ( BriefRepresentation.HasValue ) - { - _ = sb.Append("&briefRepresentation=") - .Append(BriefRepresentation.Value.ToString().ToLower(CultureInfo.CurrentCulture)); - } - - if ( !string.IsNullOrWhiteSpace(Name) ) - { - _ = sb.Append("&name=").Append(Name); - } - if ( !string.IsNullOrWhiteSpace(Alias) ) - { - _ = sb.Append("&alias=").Append(Alias); - } + var builder = new StringBuilder($"?max={Max}"); - if ( Enabled.HasValue ) + // Include brief representation if specified + if ( BriefRepresentation != null ) { - _ = sb.Append("&enabled=") - .Append(Enabled.Value.ToString().ToLower(CultureInfo.CurrentCulture)); + _ = builder.Append(CultureInfo.CurrentCulture, + $"&briefRepresentation={BriefRepresentation.ToString().ToLower(CultureInfo.CurrentCulture)}"); } - if ( !string.IsNullOrWhiteSpace(Domain) ) + // Include pagination offset if specified + if ( First != null ) { - _ = sb.Append("&domain=").Append(Domain); + _ = builder.Append(CultureInfo.CurrentCulture, + $"&first={string.Create(CultureInfo.CurrentCulture, $"{First}").ToLower(CultureInfo.CurrentCulture)}"); } - if ( DomainVerified.HasValue ) + // Include custom attribute query if specified + if ( !string.IsNullOrWhiteSpace(Q) ) { - _ = sb.Append("&domainVerified=") - .Append(DomainVerified.Value.ToString().ToLower(CultureInfo.CurrentCulture)); + _ = builder.Append(CultureInfo.CurrentCulture, $"&q={Q}"); } - if ( Exact.HasValue ) - { - _ = sb.Append("&exact=") - .Append(Exact.Value.ToString().ToLower(CultureInfo.CurrentCulture)); - } - - if ( First.HasValue ) + // Include general search query if specified + if ( !string.IsNullOrWhiteSpace(Search) ) { - _ = sb.Append("&first=") - .Append(First.Value.ToString(CultureInfo.CurrentCulture)); + _ = builder.Append(CultureInfo.CurrentCulture, $"&search={Search}"); } - if ( !string.IsNullOrWhiteSpace(Search) ) + // Include exact match filter if specified + if ( Exact != null ) { - _ = sb.Append("&search=").Append(Search); + _ = builder.Append(CultureInfo.CurrentCulture, + $"&exact={Exact.ToString().ToLower(CultureInfo.CurrentCulture)}"); } - return sb.ToString(); + return builder.ToString(); } } From 9c190dd364ee6826a8f70fc10efe1f52f639270f Mon Sep 17 00:00:00 2001 From: Ghaith Prosoft Date: Tue, 13 Jan 2026 10:05:34 +0300 Subject: [PATCH 3/5] Improve Keycloak client documentation and consistency Updated `IKcOrganizations` interface and `KcOrganizations` implementation to enhance XML documentation, align with the latest Keycloak REST API, and improve maintainability. - Added `using` directives for required dependencies in `IKcOrganizations.cs`. - Updated XML documentation for all methods in `IKcOrganizations` to include detailed return type descriptions and exception handling details. - Replaced outdated Keycloak API links in `KcOrganization` and `KcOrganizationDomain` with updated references. - Used `` tags in `KcOrganizations` to ensure consistency with the interface and reduce redundancy. - Implemented methods in `KcOrganizations` with improved documentation and alignment with the updated interface. --- .../Abstraction/IKcOrganizations.cs | 34 +++++++++++++++---- .../Implementation/KcOrganizations.cs | 13 ++++--- .../Models/Organizations/KcOrganization.cs | 2 +- .../Organizations/KcOrganizationDomain.cs | 2 +- 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/NETCore.Keycloak.Client/HttpClients/Abstraction/IKcOrganizations.cs b/NETCore.Keycloak.Client/HttpClients/Abstraction/IKcOrganizations.cs index 2911a4d..99c0a23 100644 --- a/NETCore.Keycloak.Client/HttpClients/Abstraction/IKcOrganizations.cs +++ b/NETCore.Keycloak.Client/HttpClients/Abstraction/IKcOrganizations.cs @@ -1,10 +1,12 @@ +using NETCore.Keycloak.Client.Exceptions; using NETCore.Keycloak.Client.Models; using NETCore.Keycloak.Client.Models.Organizations; namespace NETCore.Keycloak.Client.HttpClients.Abstraction; /// -/// Keycloak organizations REST client +/// Keycloak organizations REST client. +/// /// public interface IKcOrganizations { @@ -17,7 +19,10 @@ public interface IKcOrganizations /// The access token used for authentication. /// The organization representation to create. /// Optional cancellation token. - /// A indicating the result. + /// + /// A indicating the result of the operation. + /// + /// Thrown if any required parameter is null, empty, or invalid. Task> CreateAsync( string realm, string accessToken, @@ -34,7 +39,10 @@ Task> CreateAsync( /// The ID of the organization to update. /// The updated organization representation. /// Optional cancellation token. - /// A indicating the result. + /// + /// A indicating the result of the operation. + /// + /// Thrown if any required parameter is null, empty, or invalid. Task> UpdateAsync( string realm, string accessToken, @@ -51,7 +59,10 @@ Task> UpdateAsync( /// The access token used for authentication. /// The ID of the organization to delete. /// Optional cancellation token. - /// A indicating the result. + /// + /// A indicating the result of the operation. + /// + /// Thrown if any required parameter is null, empty, or invalid. Task> DeleteAsync( string realm, string accessToken, @@ -67,7 +78,10 @@ Task> DeleteAsync( /// The access token used for authentication. /// The ID of the organization to retrieve. /// Optional cancellation token. - /// A with the organization details. + /// + /// A containing the details. + /// + /// Thrown if any required parameter is null, empty, or invalid. Task> GetAsync( string realm, string accessToken, @@ -83,7 +97,10 @@ Task> GetAsync( /// The access token used for authentication. /// Optional filter criteria. /// Optional cancellation token. - /// A containing an enumerable of organizations. + /// + /// A containing an enumerable of objects. + /// + /// Thrown if any required parameter is null, empty, or invalid. Task>> ListAsync( string realm, string accessToken, @@ -99,7 +116,10 @@ Task>> ListAsync( /// The access token used for authentication. /// Optional filter criteria. /// Optional cancellation token. - /// A with the count of organizations. + /// + /// A with the count of organizations. + /// + /// Thrown if any required parameter is null, empty, or invalid. Task> CountAsync( string realm, string accessToken, diff --git a/NETCore.Keycloak.Client/HttpClients/Implementation/KcOrganizations.cs b/NETCore.Keycloak.Client/HttpClients/Implementation/KcOrganizations.cs index 036ed76..3bc7098 100644 --- a/NETCore.Keycloak.Client/HttpClients/Implementation/KcOrganizations.cs +++ b/NETCore.Keycloak.Client/HttpClients/Implementation/KcOrganizations.cs @@ -5,13 +5,13 @@ namespace NETCore.Keycloak.Client.HttpClients.Implementation; -/// -/// Organization service API for managing organizations in Keycloak. -/// -internal sealed class KcOrganizations(string baseUrl, ILogger logger) : KcHttpClientBase(logger, baseUrl), IKcOrganizations +/// +internal sealed class KcOrganizations(string baseUrl, + ILogger logger) : KcHttpClientBase(logger, baseUrl), IKcOrganizations { // Primary constructor on the class declaration is used; no explicit ctor body required. + /// public Task> CreateAsync( string realm, string accessToken, @@ -32,6 +32,7 @@ public Task> CreateAsync( cancellationToken); } + /// public Task> UpdateAsync( string realm, string accessToken, @@ -54,6 +55,7 @@ public Task> UpdateAsync( cancellationToken); } + /// public Task> DeleteAsync( string realm, string accessToken, @@ -74,6 +76,7 @@ public Task> DeleteAsync( cancellationToken); } + /// public Task> GetAsync( string realm, string accessToken, @@ -94,6 +97,7 @@ public Task> GetAsync( cancellationToken); } + /// public Task>> ListAsync( string realm, string accessToken, @@ -114,6 +118,7 @@ public Task>> ListAsync( cancellationToken); } + /// public Task> CountAsync( string realm, string accessToken, diff --git a/NETCore.Keycloak.Client/Models/Organizations/KcOrganization.cs b/NETCore.Keycloak.Client/Models/Organizations/KcOrganization.cs index 08f3e37..a59f7e8 100644 --- a/NETCore.Keycloak.Client/Models/Organizations/KcOrganization.cs +++ b/NETCore.Keycloak.Client/Models/Organizations/KcOrganization.cs @@ -4,7 +4,7 @@ namespace NETCore.Keycloak.Client.Models.Organizations; /// /// Represents an organization resource in Keycloak. -/// +/// /// public sealed class KcOrganization { diff --git a/NETCore.Keycloak.Client/Models/Organizations/KcOrganizationDomain.cs b/NETCore.Keycloak.Client/Models/Organizations/KcOrganizationDomain.cs index 492541a..f4424e6 100644 --- a/NETCore.Keycloak.Client/Models/Organizations/KcOrganizationDomain.cs +++ b/NETCore.Keycloak.Client/Models/Organizations/KcOrganizationDomain.cs @@ -4,7 +4,7 @@ namespace NETCore.Keycloak.Client.Models.Organizations; /// /// Represents an organization domain in Keycloak. -/// +/// /// public sealed class KcOrganizationDomain { From c4cb15f01e866af43a9842ec75b67be713cb794c Mon Sep 17 00:00:00 2001 From: Ghaith Prosoft Date: Tue, 13 Jan 2026 10:09:40 +0300 Subject: [PATCH 4/5] Refactor BuildQuery in KcOrganizationFilter --- .../Models/Organizations/KcOrganizationFilter.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/NETCore.Keycloak.Client/Models/Organizations/KcOrganizationFilter.cs b/NETCore.Keycloak.Client/Models/Organizations/KcOrganizationFilter.cs index df9b80d..81910a1 100644 --- a/NETCore.Keycloak.Client/Models/Organizations/KcOrganizationFilter.cs +++ b/NETCore.Keycloak.Client/Models/Organizations/KcOrganizationFilter.cs @@ -36,7 +36,6 @@ public sealed class KcOrganizationFilter : KcFilter /// public new string BuildQuery() { - var builder = new StringBuilder($"?max={Max}"); // Include brief representation if specified From c2cbfa4bcbc4b9659f84fc8fee1b8718e9a7d52d Mon Sep 17 00:00:00 2001 From: Ghaith Prosoft Date: Tue, 13 Jan 2026 11:04:21 +0300 Subject: [PATCH 5/5] Refactor Domains property in KcOrganization class --- NETCore.Keycloak.Client/Models/Organizations/KcOrganization.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NETCore.Keycloak.Client/Models/Organizations/KcOrganization.cs b/NETCore.Keycloak.Client/Models/Organizations/KcOrganization.cs index a59f7e8..339dd82 100644 --- a/NETCore.Keycloak.Client/Models/Organizations/KcOrganization.cs +++ b/NETCore.Keycloak.Client/Models/Organizations/KcOrganization.cs @@ -79,5 +79,5 @@ public sealed class KcOrganization /// A collection of representing domains associated with the organization. /// [JsonPropertyName("domains")] - public List Domains { get; set; } = new(); + public ICollection Domains { get; set; } = []; }