Repository navigation
Known Limitations
Last verified against v3.0.0
Every limitation here is real today, has a documented cause, and states its workaround. Resolved gaps move to Release Notes and Versioning — they do not linger on this list.
D365 OData accepts a cross-company=true parameter that returns rows across every legal entity the service principal can read. IntegratoR does not surface it — every query is scoped to the company you set through the entity composite key.
Read one company at a time and merge the results:
var companies = new[] { "USMF", "JFI" };
var journals = new List<LedgerJournalHeader>();
foreach (string company in companies)
{
var query = new GetByFilterQuery<LedgerJournalHeader>(
header => header.DataAreaId == company && header.IsPosted == NoYes.No);
Result<IEnumerable<LedgerJournalHeader>> result = await mediator.Send(query, cancellationToken);
if (result.IsFailed)
{
IntegrationError? error = result.GetError();
// IntegrationError { Code, Type }: a D365 read failure surfaces here — inspect and stop or skip.
break;
}
journals.AddRange(result.Value);
}Workaround status: not planned. The likely shape is a per-query opt-in marker (ICrossCompanyQuery) handled by a dedicated pipeline behaviour; the design is not worked out.
The LINQ-to-OData translator (IntegratoRODataExpressionTranslator) covers filter, select, and expand expressions plus Any / All lambdas. Advanced $expand shapes — nested expand with a filter, expand carrying a select inside an Any — are not covered and throw NotSupportedException at translation time.
The failure is caught before the request reaches D365, so an unsupported expression never produces a malformed call. Through a query, ODataService returns it as a failed result, {Entity}.UnexpectedError, with the NotSupportedException in error.Exception:
// Supported: a flat expand translates cleanly.
var supported = new GetByFilterQuery<LedgerJournalHeader>(
header => header.DataAreaId == "USMF");
// Unsupported: a nested expand-with-filter throws NotSupportedException at translation,
// not a runtime OData error. The expression never leaves the process.The supported shapes are pinned by tests/IntegratoR.OData.Tests/Common/Filters/IntegratoRODataExpressionTranslatorTests.cs.
Workaround status: coverage is added on demand. The translator stays narrow on purpose — predictable, D365-compatible output over exhaustive LINQ coverage.
LedgerJournalLine carries fields that are both required in C# and [ODataField(IgnoreOnCreate = true)] — the value is compiler-mandatory but excluded from the create payload. AccountDisplayValue and TransDate are the confirmed pair today; the wider set of IgnoreOnCreate fields has not been audited against D365's create metadata.
// LedgerJournalLine.cs — required by the compiler, yet dropped from the create body:
[JsonPropertyName("AccountDisplayValue")]
[ODataField(IgnoreOnCreate = true)]
public virtual required string AccountDisplayValue { get; set; }Caution
A field marked both required and IgnoreOnCreate is silent payload exclusion — D365 either computes a default and accepts the create, or rejects it with HTTP 400 and a server-side validation message, depending on the journal template. Read the entity source before building a LedgerJournalLine create so you know which values D365 actually receives.
Workaround status: queued. Each field needs verifying against D365 metadata — server-generated (keep IgnoreOnCreate) or consumer-supplied (drop it) — and lands with a reflection-based test pinning the attribute state.
The named "ODataClient" retry policy has no HTTP-method guard. It retries POST, PATCH, DELETE, and every $batch chunk POST exactly like a read — on a 5xx, 408, or 429 response, a network failure, or a TaskCanceledException. Single-entity writes are retried again by the operation-level policy inside ODataService (a transient ODataClientException or a TaskCanceledException), so the two layers compound. Calls routed through PanoramicData.OData.Client — creates, reads, counts, and single-key updates and deletes — also pass through that library's own retry loop, which EnableRetries does not switch off; see Configure Resilience.
A retry is safe only when the write is idempotent. A create that D365 committed but answered with a 5xx — or too late for the client — is sent again, and the repeat creates a duplicate: a second journal header, or a second set of lines from a retried Atomic chunk. A repeated update re-applies the same values; a repeated batch delete reports HTTP 404 failures for rows that are already gone.
// Turns off IntegratoR's two retry policies; reads lose them too. PanoramicData's own loop still runs.
services.AddIntegratoR(configuration, integrator =>
integrator.ConfigureOData(settings => settings.Resilience.EnableRetries = false));Workaround status: a fix that limits retries to idempotent requests is planned after v3.0.0. Until then, either disable retries as above or make creates detectable — set a value you can filter on (for example Description or IntegrationKey on LedgerJournalHeader) and query for it before re-running a failed import.
ODataResilienceSettings.RetryCount defaults to 3 and its XML doc states a valid range of 1 to 10. ODataSettingsValidator does not check that range — it validates authentication headers, the selected mode and its credentials, and the batch chunk-size bound (Batch.MaxOperationsPerChunk, 1–200). A RetryCount of 0 or 50 binds and runs.
Caution
Keep RetryCount within 1–10 yourself. A value of 0 disables retries for that policy; a large value multiplies load against D365 and delays the surfaced failure. Nothing rejects an out-of-range value at startup.
Workaround status: the range check is a candidate addition to ODataSettingsValidator; not yet implemented.
Atomic batch mode runs each chunk as one all-or-nothing OData changeset, but a dataset larger than the chunk size (ODataSettings.Batch.MaxOperationsPerChunk, default 150; D365 caps a $batch at 200 operations) is split across several changesets. Each chunk commits or rolls back independently — a 1,000-row import is not a single transaction.
// StopOnFirstFailedChunk (default true in Atomic mode) stops submitting further chunks after one
// fails, but chunks already committed stay committed. Inspect the BatchOutcome to see what landed.
if (result.IsFailed && result.GetError() is BatchIntegrationError batchError)
{
int committed = batchError.Outcome.Succeeded;
IEnumerable<BatchItemResult> toRetry = batchError.Outcome.Failures;
}Note
The $batch write path (both modes) is covered by unit and integration tests but has not yet been verified against a live D365 environment; v3.0.0 ships without that run. ContinueOnError applies operations as individual requests — a single-$batch transport (Prefer: odata.continue-on-error) is undocumented for D365 F&O and not implemented.
Workaround status: inherent to D365's $batch operation cap. Size imports to the chunk limit and keep retries idempotent on the composite key.
ContinueOnError sends each item in a chunk as its own request. If one of those requests throws — an open circuit breaker, a connection failure, an HttpClient timeout — every item in the chunk is reported failed with StatusCode 0, including items sent earlier in the chunk that D365 already committed.
Workaround status: reporting each item's own outcome is planned after v3.0.0. Until then, query by a business key before re-sending a chunk that came back with StatusCode 0, or use a smaller MaxOperationsPerChunk to limit how many items one failure can misreport.
Both Result<T> JSON converters write each error as code, message, and type only, and rebuild a plain IntegrationError on read. A failed Result<BatchOutcome> that crosses a serialiser — a Durable Functions activity return, a Newtonsoft HTTP payload — comes back without its BatchIntegrationError, so the Outcome is gone. Every error's Exception is dropped the same way.
// In a Durable Functions orchestrator, after the activity returned a failed batch:
Result<BatchOutcome> result = await context.CallActivityAsync<Result<BatchOutcome>>("ImportLines", lines);
IntegrationError? error = result.GetError(); // plain IntegrationError, Code "{Entity}.BatchFailed"
bool hasOutcome = error is BatchIntegrationError; // false — the outcome did not surviveThe per-item errors that follow the header survive as plain IntegrationErrors: code {Entity}.BatchOperationFailed, message [Index i, chunk c, HTTP s] <D365 message>.
Workaround status: inherent to the shared wire shape both converters use. Read the BatchOutcome inside the activity and return what the orchestrator needs — for example the failed indices — as the value, or parse the per-item messages.
GetDimensionOrdersQuery reads the environment's segment delimiter from DimensionParameters and maps it to a char with DimensionSegmentDelimiterExtensions.GetCharValue. Only DimensionSegmentDelimiter.Hyphen is mapped. Every other member — Period, Underscore, Bar, Tilde, and the double-character variants — throws ArgumentOutOfRangeException.
The exception is not converted to a Result: it propagates out of mediator.Send, and LoggingBehaviour logs it at Error and re-throws.
// Environment whose DimensionParameters delimiter is Period:
Result<DimensionFormat> result = await mediator.Send(
new GetDimensionOrdersQuery("Sachkontodimensionen", DimensionHierarchyType.DataEntityLedgerDimensionFormat),
cancellationToken); // throws ArgumentOutOfRangeException — no failed Result comes backWorkaround status: not scheduled. Environments on the default hyphen are unaffected. For another delimiter, query DimensionParameters and DimensionIntegrationFormat yourself and split the format string on the configured delimiter.
These are acknowledged and parked — no fix is scheduled:
-
D365 innererror text may reach
IntegrationError.Message. A downstream error body can carry server detail into the surfaced message. TreatIntegrationError.Messageas server-authored when logging externally. -
Non-401 failures expose
exception.Message. Only the auth short-circuit is sanitised to a generic reason phrase; other paths pass the exception message through. -
Smoke-trigger handler tests absent. The three HTTP trigger handlers behind
smoke/ledger-journal,smoke/financial-dimensions, andsmoke/ledger-journal-batchhave no isolated unit coverage. The batch trigger has also not yet been run against a live D365 environment. -
IEntity.GetLoggingContext()couples telemetry to the domain contract. Splitting it into a dedicated logging interface is a MAJOR-breaking change, deferred to a future major version.
These members are [Obsolete] since v2.0.0 and still ship in v3.0.0. Removal is planned for a future major version (v4 at the earliest). Migrate off them now:
| Obsolete member | Replacement |
|---|---|
BaseEntity<TKey> |
Non-generic BaseEntity + GetCompositeKey()
|
IODataService<T>.FindAll |
FindAllAsync |
IODataService<T>.QueryAsync with a Func-based orderBy (the ordering was never applied) |
The QueryAsync overload taking a typed (KeySelector, Descending) orderBy list |
ICacheableQuery.GenerateCacheKey() / GetCacheKeyValues()
|
The CacheKey property |
ODataBatchException, ODataMetadataProvider
|
Handled internally; no consumer replacement needed |
IAuthenticator overload without a CancellationToken
|
The CancellationToken overload |
Get Started
Use Cases
- Handle Errors
- Configure Resilience
- Add Validation
- Cache Query Results
- Work with Dimensions
- Run Smoke Tests
- Extend the Pipeline
- Test with TestKit
Reference