Skip to content

Make async query execution compatible with WASM#3474

Open
gathogojr wants to merge 2 commits intoOData:mainfrom
gathogojr:fix/3452-wasm-compatibility
Open

Make async query execution compatible with WASM#3474
gathogojr wants to merge 2 commits intoOData:mainfrom
gathogojr:fix/3452-wasm-compatibility

Conversation

@gathogojr
Copy link
Contributor

Issues

This pull request fixes #3452.

Description

This PR introduces a modernized, end‑to‑end asynchronous request/response path for OData Client that removes synchronous waits from async flows, restores Blazor WASM compatibility, and improves cancellation/timeout behavior—without breaking existing public APIs. It lays the groundwork for deprecating APM (Begin/End) in the next major release.

Motivation

  • Customer regression in 8.4.3 (WASM): Blazor WebAssembly disallows blocking waits; certain internal paths performed Task.Wait() after SendAsync, surfacing “Cannot wait on monitors on this runtime.”
  • Underlying cause: Introducing HttpCompletionOption.ResponseHeadersRead allowed faster cancellation but exposed latent blocking in downstream conversion that synchronously waited on ReadAsStreamAsync().
  • Architectural debt: Async APIs were often thin wrappers over APM (Begin/End + FromAsync), not truly async/await, making timeouts and cancellation brittle across runtimes.

Design Overview

New async-native pathway

Introduce and route async callers through a unified, non-blocking pipeline:

DataServiceContext.GetResponseAsync
 → ODataRequestMessageWrapper.GetResponseAsync
   → DataServiceClientRequestMessage.GetResponseAsync
     → HttpClientRequestMessage.GetResponseAsync
        → SendInternalAsync(HttpCompletionOption.ResponseHeadersRead, CTs)
        → ConvertHttpWebResponseAsync

This path never blocks and honours cancellation via linked tokens (abort + per‑request timeout + external token).

Cancellation/Timeout

All sends use a linked CancellationToken that merges:

  • Abort token (from Abort()),
  • Per-request timeout token,
  • Optional external token.
    This yields prompt, deterministic cancellation across stages (pre‑content, headers, body).

Key Code Changes

HTTP transport (HttpClientRequestMessage)

  • Add async response conversion to avoid sync waits: ConvertHttpWebResponseAsync(HttpResponseMessage) and GetResponseAsync(CancellationToken).
  • Rework CreateSendTask(CancellationToken) to build a linked token source and to send with ResponseHeadersRead for earlier cancellation; apply cached content headers prior to send.

Note: The previous implementation synchronously waited while reading the content stream during conversion, which is invalid on WASM; the new async conversion eliminates that path.

Request/response surface (selected)

  • Async entry points throughout the stack now flow through the new path:
    • ODataRequestMessageWrapper.GetResponseAsync(CancellationToken)
    • RequestInfo.GetResponseAsync(ODataRequestMessageWrapper, bool, CancellationToken)
    • Async query/request orchestrators (QueryResult.ExecuteQueryAsync, GetReadStreamResult.ExecuteQueryAsync, etc.) use the new async response and streaming logic.

High-level APIs

  • DataServiceQuery<T>.ExecuteAsync(...), DataServiceActionQuerySingle<T>.GetValueAsync(...), and other public async APIs continue to exist but now route through the modernized path under the hood.

API Surface (additions / changes)

No public breaking changes in this PR. APM (Begin/End) remains for compatibility; async paths are modernized internally.

Key new / updated async members (representative):

  • Transport / messaging

    • DataServiceClientRequestMessage.GetResponseAsync(CancellationToken) (virtual)
    • HttpClientRequestMessage.GetResponseAsync(CancellationToken) / CreateSendTask(CancellationToken) / ConvertHttpWebResponseAsync(...)
    • ODataRequestMessageWrapper.GetResponseAsync(CancellationToken) (internal)
    • RequestInfo.GetResponseAsync(...) (internal)
  • Query & streaming

    • QueryResult.ExecuteQueryAsync(CancellationToken) / ProcessResponseStreamAsync(CancellationToken) (internal)
    • GetReadStreamResult.ExecuteQueryAsync(CancellationToken) (internal)
  • Save / batch flows

    • SaveResult.CreateNextChangeAsync(CancellationToken)
    • BatchSaveResult.BatchRequestAsync(CancellationToken)
    • DeepInsertSaveResult.DeepInsertRequestAsync<T>(T, CancellationToken)
    • BulkUpdateSaveResult.BulkUpdateRequestAsync<T>(CancellationToken, T[])
    • DataServiceContext.*Async methods

Checklist (Uncheck if it is not completed)

  • Test cases added
  • Build and test with one-click build and test script passed

Additional work necessary

If documentation update is needed, please add "Docs Needed" label to the issue and provide details about the required document change in the issue.

@gathogojr gathogojr force-pushed the fix/3452-wasm-compatibility branch from b0028e9 to 0e4adf3 Compare January 12, 2026 07:45
{
InvalidOperationException exception = WebUtil.GetHttpWebResponse(ex, ref this.batchResponseMessage);

// For non-async batch requests we rethrow the WebException. This is shipped behavior.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also update the comments to make sure it's still readable later?

if (this.batchResponseMessage != null)
{
// For non-async batch requests we call the test hook to get the response stream but we cannot consume it
// because we rethrow what we caught and the customer need to be able to read the response stream from the WebException.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also update the comments to make sure it's still readable later?

/// </summary>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>A task that represents the asynchronous batch request operation.</returns>
internal async Task BatchRequestAsync(CancellationToken cancellationToken = default)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Task

Can we use ValueTask?

.ConfigureAwait(false);
}

// TODO: In future releases, expose this method as public GetAllPagesAsync to support IAsyncEnumerable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we wait for future release since we support it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xuzhg Because in the current version, there's already a GetAllPagesAsync method that returns IEnumerable<T>. We'd switch that with IAsyncEnumerable<T> in a major release because it'd be a breaking change

this.outputResponseStream = null;
}
else if (copy.Position < copy.Length)
{ // In Silverlight, generally 3 bytes less than advertised by ContentLength are read
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silverlight

do we still to mean about 'SilverLight'?

if (buffer == null)
{
refBuffer = buffer = new byte[1000];
refBuffer = buffer = new byte[1000]; // Why does this use 1000 instead of DefaultBufferSizeForStreamCopy?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you check the history to see "why"?

xuzhg
xuzhg previously approved these changes Jan 13, 2026
Copy link
Member

@xuzhg xuzhg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

@gathogojr gathogojr force-pushed the fix/3452-wasm-compatibility branch from 0e4adf3 to 88a2eb8 Compare January 19, 2026 13:30
@gathogojr gathogojr force-pushed the fix/3452-wasm-compatibility branch 2 times, most recently from 60aff18 to 240a800 Compare January 22, 2026 05:46
@gathogojr gathogojr force-pushed the fix/3452-wasm-compatibility branch from 240a800 to 28f1005 Compare January 23, 2026 06:41
@gathogojr gathogojr force-pushed the fix/3452-wasm-compatibility branch from 2a3a08e to fb311b7 Compare January 26, 2026 20:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OData Client 8.4.3 breaks usage of the client in WASM based environments

2 participants