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 =>