Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions PanoramicData.OData.Client.Test/UnitTests/ODataClientQueryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ internal sealed class EntitySetAttribute(string entitySet) : Attribute
[EntitySet("Mailbox")]
internal sealed class GeneratedMailbox { }

internal sealed class CustomResolvedEntity { }

/// <summary>
/// Unit tests for ODataClient query operations.
/// </summary>
Expand Down Expand Up @@ -119,6 +121,48 @@ public void For_EntitySetAttribute_OverridesPluralization()
url.Should().Be("Mailbox");
}

/// <summary>
/// Tests that the configured entity set name resolver overrides the built-in conventions.
/// </summary>
[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<CustomResolvedEntity>().BuildUrl();

url.Should().Be("custom_entities");
}

/// <summary>
/// Tests that a whitespace resolver result uses the existing entity set attribute convention.
/// </summary>
[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<GeneratedMailbox>().BuildUrl();

url.Should().Be("Mailbox");
}

/// <summary>
/// Tests that AutoPluralization = false uses the type name as-is.
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions PanoramicData.OData.Client/ODataClient.Query.cs
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,12 @@ private string GetEntitySetName<T>()
{
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");
Expand Down
10 changes: 10 additions & 0 deletions PanoramicData.OData.Client/ODataClientOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,16 @@ public class ODataClientOptions
/// </remarks>
public bool AutoPluralization { get; set; } = true;

/// <summary>
/// Gets or sets a function that resolves entity set names for <c>For&lt;T&gt;()</c> calls.
/// </summary>
/// <remarks>
/// When the function returns a non-empty value, the client uses it instead of the built-in
/// entity-set attribute and pluralization conventions. Return <c>null</c> or whitespace to
/// use those existing conventions.
/// </remarks>
public Func<Type, string?>? EntitySetNameResolver { get; set; }

/// <summary>
/// Gets or sets a value indicating whether a 404 Not Found response should return <c>null</c>
/// instead of throwing an <see cref="ODataNotFoundException"/>.
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>()
// 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 =>
Expand Down