From a0b11d9bb7812ffa88a1e7ed6dbf980aeec67a2f Mon Sep 17 00:00:00 2001 From: Pavils Gulenko Date: Fri, 17 Jul 2026 10:24:02 +0300 Subject: [PATCH] feat: Add entity set name resolver --- .../UnitTests/ODataClientQueryTests.cs | 44 +++++++++++++++++++ .../ODataClient.Query.cs | 6 +++ .../ODataClientOptions.cs | 10 +++++ README.md | 4 ++ 4 files changed, 64 insertions(+) diff --git a/PanoramicData.OData.Client.Test/UnitTests/ODataClientQueryTests.cs b/PanoramicData.OData.Client.Test/UnitTests/ODataClientQueryTests.cs index 7bc3d4a..84e6b72 100644 --- a/PanoramicData.OData.Client.Test/UnitTests/ODataClientQueryTests.cs +++ b/PanoramicData.OData.Client.Test/UnitTests/ODataClientQueryTests.cs @@ -12,6 +12,8 @@ internal sealed class EntitySetAttribute(string entitySet) : Attribute [EntitySet("Mailbox")] internal sealed class GeneratedMailbox { } +internal sealed class CustomResolvedEntity { } + /// /// Unit tests for ODataClient query operations. /// @@ -119,6 +121,48 @@ public void For_EntitySetAttribute_OverridesPluralization() url.Should().Be("Mailbox"); } + /// + /// Tests that the configured entity set name resolver overrides the built-in conventions. + /// + [Fact] + public void For_EntitySetNameResolver_UsesResolvedName() + { + using var httpClient = new HttpClient(_mockHandler.Object) { BaseAddress = new Uri("https://test.odata.org/") }; + using var client = new ODataClient(new ODataClientOptions + { + BaseUrl = "https://test.odata.org/", + HttpClient = httpClient, + Logger = NullLogger.Instance, + RetryCount = 0, + EntitySetNameResolver = type => type == typeof(CustomResolvedEntity) ? "custom_entities" : null + }); + + var url = client.For().BuildUrl(); + + url.Should().Be("custom_entities"); + } + + /// + /// Tests that a whitespace resolver result uses the existing entity set attribute convention. + /// + [Fact] + public void For_EntitySetNameResolverReturnsWhitespace_UsesEntitySetAttribute() + { + using var httpClient = new HttpClient(_mockHandler.Object) { BaseAddress = new Uri("https://test.odata.org/") }; + using var client = new ODataClient(new ODataClientOptions + { + BaseUrl = "https://test.odata.org/", + HttpClient = httpClient, + Logger = NullLogger.Instance, + RetryCount = 0, + EntitySetNameResolver = _ => " " + }); + + var url = client.For().BuildUrl(); + + url.Should().Be("Mailbox"); + } + /// /// Tests that AutoPluralization = false uses the type name as-is. /// diff --git a/PanoramicData.OData.Client/ODataClient.Query.cs b/PanoramicData.OData.Client/ODataClient.Query.cs index 858f219..13d2f50 100644 --- a/PanoramicData.OData.Client/ODataClient.Query.cs +++ b/PanoramicData.OData.Client/ODataClient.Query.cs @@ -469,6 +469,12 @@ private string GetEntitySetName() { var type = typeof(T); + var resolvedEntitySetName = _options.EntitySetNameResolver?.Invoke(type); + if (!string.IsNullOrWhiteSpace(resolvedEntitySetName)) + { + return resolvedEntitySetName; + } + // Respect [EntitySet("...")] attribute from Microsoft.OData.Client generated DTOs var entitySetAttr = type.GetCustomAttributes(false) .FirstOrDefault(a => a.GetType().Name == "EntitySetAttribute"); diff --git a/PanoramicData.OData.Client/ODataClientOptions.cs b/PanoramicData.OData.Client/ODataClientOptions.cs index 60196a1..edca0ff 100644 --- a/PanoramicData.OData.Client/ODataClientOptions.cs +++ b/PanoramicData.OData.Client/ODataClientOptions.cs @@ -87,6 +87,16 @@ public class ODataClientOptions /// public bool AutoPluralization { get; set; } = true; + /// + /// Gets or sets a function that resolves entity set names for For<T>() calls. + /// + /// + /// When the function returns a non-empty value, the client uses it instead of the built-in + /// entity-set attribute and pluralization conventions. Return null or whitespace to + /// use those existing conventions. + /// + public Func? EntitySetNameResolver { get; set; } + /// /// Gets or sets a value indicating whether a 404 Not Found response should return null /// instead of throwing an . diff --git a/README.md b/README.md index cccf488..5fcd2ff 100644 --- a/README.md +++ b/README.md @@ -403,6 +403,10 @@ var client = new ODataClient(new ODataClientOptions // Optional: Custom JSON serialization settings JsonSerializerOptions = customOptions, + + // Optional: Override entity set names used by For() + // Return null to use the existing attribute and pluralization conventions + EntitySetNameResolver = type => type == typeof(LegacyProduct) ? "legacy_products" : null, // Optional: Configure headers for every request ConfigureRequest = request =>