All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Add
MaximumRetryAfterDelayoption toODataClientOptions(default 30 seconds). A server-suppliedRetry-Afterheader is now honoured in preference toRetryDelay, bounded by this value so that a large or malformed header cannot stall the caller. Set toTimeSpan.Zeroto ignoreRetry-Afterentirely and always useRetryDelay
- Retry HTTP 408 (Request Timeout) and 429 (Too Many Requests) alongside 5xx. Previously any status below 500 was returned to the caller immediately, so a 408 from an intervening proxy or a 429 from a rate limiter was never retried. Both are cases where the server rejected the request without processing it, so retrying is safe even for methods that are not idempotent. Other 4xx statuses remain non-retryable, in particular 409, which is a routine "already exists" outcome for callers that create-or-overwrite
- Fix
.OrderBy(p => p.Nav.Prop)and.NavigateTo(p => p.Nav.Prop)resolving only the leaf property name (e.g.$orderby=FirstName) instead of the full navigation path ($orderby=BestFriend/FirstName) for nested (dotted) property selectors
- Add
RetryAttemptLogLeveloption toODataClientOptions(defaultDebug) controlling the level at which individual failed attempts that will be retried are logged - set toWarningto log every attempt prominently, orNoneto disable per-attempt logging entirely - Log a single
Warningwhen all retries are exhausted (new EventIds 22 and 23), including the transient-exception path which previously logged nothing on the final failed attempt
- Individual failed attempts that will be retried are now logged at
Debugby default instead ofWarning, so transient failures that recover no longer flood consumer logs
- Fix non-nullable enum properties in LINQ filter expressions emitting integer values instead of quoted OData enum member names
- Render enum literals in LINQ filter expressions as quoted OData enum member names instead of underlying numeric values
- Remove duplicate
Content-Transfer-Encoding: binaryheader in batch requests;HttpMessageContentconstructor already adds it, so the extra call inODataClient.Batch.cswas redundant
- Add
Batch_OperationContent_ShouldIncludeRequiredHeadersExactlyOnceto assert batch part headers appear exactly once in the serialized multipart body - Add
DateTimeKind.Utcassertion toRead_SimpleDateFormat_ParsesCorrectlyto make intent explicit and guard against future converter changes - Add regression coverage for inline enum literals and captured enum variables in filter expressions
- Parse all
<Schema>elements in CSDL metadata documents; previously only the first schema was read, causing entity types and entity sets to be missing for services (e.g. Northwind) that split their definitions across multiple schemas
- Fix
"Invalid request URI"when a providedHttpClienthas noBaseAddressset - the client now automatically setsBaseAddressfromoptions.BaseUrlif the providedHttpClientdoes not already have one
- Fix batch requests throwing
"This operation is not supported for a relative URI"- batch operation URLs are relative by design;HttpMessageContentnow resolves them against the client base URL before building the request line andHostheader
- Fix
GetFirstOrDefaultAsyncandGetSingleAsyncwith a key set: now deserializes the response as a single JSON object instead of expecting a{"value":[...]}collection wrapper, matching the actual response shape of single-entity endpoints. Also no longer appends$topwhich is rejected by some APIs (e.g. Exchange Online) on single-entity URLs
- Add
AutoPluralizationoption toODataClientOptions(defaulttrue) - set tofalseto use type names as-is, avoiding automatic pluralization for APIs such as Exchange Online that use singular endpoint names (e.g.Mailboxinstead ofMailboxes) - Respect
[EntitySet("...")]attribute on generated DTOs (e.g. from Microsoft.OData.Client tooling) when deriving entity set names inFor<T>(), without requiring a reference toMicrosoft.OData.Client
- Add
FindEntriesAsync()toODataQueryBuilder<T>as a Simple.OData.Client-compatible alias forGetAllAsync(), returningIEnumerable<T>directly - enabling the.NavigateTo(expr).As<T>().FindEntriesAsync()chain without needing.Value
- Add non-generic
NavigateTo(expr)overload toODataQueryBuilder<T>returningFluentODataQueryBuilder,As<TResult>()onFluentODataQueryBuilderfor re-typing, andFindEntriesAsync()alias - enabling Simple.OData.Client-compatible NavigateTo/As/FindEntriesAsync chain - Add
NavigateTo<TNav>()method toODataQueryBuilder<T>andNavigateTo()toFluentODataQueryBuilder, enabling navigation to dependent collections via EntitySet(key)/NavigationProperty URL paths
- Fix IgnoreResourceNotFoundException being ignored in fluent .For(...).Key(...).GetEntryAsync() - now returns null on 404 as expected
- Fix PATCH body serialization:
UpdateAsyncnow passes the runtime type toJsonContent.Create, preventingDictionary<string, object?>patch bodies from being serialized as empty{}objects - Fix
ODataTypeAnnotationConverterto exclude dictionary types (Dictionary<,>,IDictionaryimplementors) andtypeof(object)from@odata.typeannotation injection, preventing corrupt PATCH bodies that caused ASP.NET ODataDelta<T>model binding to return null and produce 400 "A PATCH request body is required" responses
- Add
QueryOptions(string)method toODataQueryBuilder<T>andFluentODataQueryBuilderfor verbatim vendor-specific query parameters (e.g.PropertySet=Minimum,AddressList) without quoting
- Add
GetByKeyOrDefaultAsyncmethod - always returns null on 404 without requiringIgnoreResourceNotFoundExceptionoption - Add
IgnoreResourceNotFoundExceptionoption toODataClientOptions- returns null instead of throwingODataNotFoundExceptionon 404 responses
- Fix
ODataTypeAnnotationConvertertype detection to exclude OData framework types (includingDelta<T>), preventing ASP.NET Core OData PATCHDelta<T>model binding from being intercepted
- Fix
ODataTypeAnnotationConverterto identify and excludeDelta<T>and other OData framework types, preventingArgumentNullExceptionwhen aDeltaparameter is null in PATCH operations
- Add
ODataTypeAnnotationConverter- automatically injects@odata.typeinto POST/PATCH bodies when serializing a derived type, enabling polymorphicCreateAsynccalls against OData servers using Table-Per-Hierarchy (TPH) inheritance - Add
ODataTypeAnnotationAttribute- optional attribute to override the auto-derived type name (e.g.TypeName = "#MyNamespace.Employee") or force annotation inclusion on non-polymorphic types (AlwaysInclude = true)
- Fix DateTime formatting consistency - FormatFunctionParameterValue and FormatArrayElementValue now respect DateTimeKind
- Fix DateTime filter formatting for Unspecified kind - now formats without Z suffix to match OData Edm.DateTime type, preventing timezone conversion errors
- Fix date-only string parsing to treat as UTC instead of local time, preventing timezone conversion errors
- Add fluent execution methods (GetAsync, GetAllAsync, GetFirstOrDefaultAsync, GetSingleAsync, GetSingleOrDefaultAsync, GetCountAsync) directly on ODataQueryBuilder for streamlined query execution
- CODE_COVERAGE.md with comprehensive test coverage plan for full code coverage
- NestedExpandBuilder for configuring nested expand options (select, expand, filter, orderby, top, skip)
- ExpandWithSelect method for expand with nested select syntax (fixes #4)
- Fluent batch API with clean method chaining (
CreateBatch().Get<T>().Create().Delete().ExecuteAsync()) Changeset(Action<ODataChangesetBuilder>)pattern for atomic batch operations- Index-based result access on
ODataBatchResponse(response[0],response.GetResult<T>(0)) HasErrorsproperty onODataBatchResponseTryGetResult<T>(int index, out T? result)for safe result access- Nested expand expression support (
p => new { p.Parent, p.Parent!.Children }produces$expand=Parent($expand=Children)) Function()andApply()methods toFluentODataQueryBuilderfor feature parity- Changelog system with
Add-ChangelogEntry.ps1script - Automatic version replacement in
Publish.ps1
- Split multi-type files into one type per file for better maintainability
ODataBatchBuildermethods now return builder for fluent chaining (breaking change from string operation IDs)ODataChangesetBuildermethods now return builder for fluent chainingCreateChangeset()replaced withChangeset(Action<ODataChangesetBuilder>)pattern
- Non-fluent batch API methods that returned operation IDs
- Initial public beta release
- OData V4 query builder with LINQ expression support
- Full CRUD operations (Create, Read, Update, Delete)
- Batch request support with changesets
- Delta query support for change tracking
- Metadata parsing and caching
- Service document retrieval
- Singleton entity support
- Stream property support
- Entity reference management
- Cross-join queries
- Async long-running operation support
- Retry policies with configurable delays
- ETag-based optimistic concurrency
- Comprehensive logging via
ILogger - Fluent and typed query APIs