Skip to content

BuildDatasyncOfflineOptions throws PlatformNotSupportedException on .NET 11 Blazor WebAssembly #574

Description

@yasmoradi

Describe the bug

On .NET 11, PullAsync throws PlatformNotSupportedException on the first call in a Blazor
WebAssembly app. The same code works on .NET 10.

The blocking lock in BuildDatasyncOfflineOptions is the cause:

OfflineDbContext.BuildDatasyncOfflineOptions()  // OfflineDbContext.cs#L147
  -> DisposableLock.AcquireLock()               // DisposableLock.cs#L23
       -> SemaphoreSlim.Wait()

.NET 11 makes every blocking SemaphoreSlim.Wait(...) throw on single-threaded browser-wasm, even
when the semaphore is free. SemaphoreSlim.Wait has been [UnsupportedOSPlatform("browser")] for
several releases; .NET 11 started enforcing it (dotnet/runtime#123329, confirmed as by design in
dotnet/runtime#131859). Through .NET 10 an uncontended Wait() just took the count and returned,
so this lock worked by luck.

Stack trace, from our production telemetry:

System.PlatformNotSupportedException: Arg_PlatformNotSupported
   at System.Runtime.CompilerServices.RuntimeFeature.ThrowIfMultithreadingIsNotSupported()
   at System.Threading.SemaphoreSlim.WaitCore(Int64, CancellationToken)
   at System.Threading.SemaphoreSlim.Wait()
   at CommunityToolkit.Datasync.Client.Threading.DisposableLock.AcquireLock()
   at CommunityToolkit.Datasync.Client.Offline.OfflineDbContext.BuildDatasyncOfflineOptions()
   at CommunityToolkit.Datasync.Client.Offline.OfflineDbContext.PullAsync(IEnumerable`1, PullOptions, CancellationToken)

Push breaks too, but only once the queue is non-empty: PushOperationAsync dereferences the
Lazy<OfflineOptions> (OperationsQueueManager.cs#L339), which runs the same method. With an empty
queue push returns early at L291 and looks healthy, which is why a page load reports the failure
as a pull failure.

To Reproduce

  1. Blazor WebAssembly app on .NET 11 (single-threaded, no WasmEnableThreads), client 10.1.2.
  2. Call await dbContext.PullAsync().
  3. PlatformNotSupportedException.

new SemaphoreSlim(1).Wait(); in the same app throws the same way, if the one-liner is easier.

What I actually observed is the production stack above from our Blazor WASM app; the push path and
the .NET 10 comparison are read off the sources rather than measured, and I haven't reduced it to a
standalone repro project yet. Happy to build one if that helps.

Expected behavior

Pull and push work as they did on .NET 10.

What platforms?

  • Client: Blazor WebAssembly, browser-wasm single-threaded, .NET 11,
    CommunityToolkit.Datasync.Client 10.1.2. Browser and host OS make no difference. MAUI, WPF and
    Windows heads are unaffected, they have real threads.
  • Server: not involved, the request is never sent.

Additional context

There's no workaround from the consuming app: the lock is acquired before the
if (OfflineOptions is null) check, BuildDatasyncOfflineOptions is internal, and Wait(0)
throws as well (the zero-timeout fast path only covers the already-empty case).

Looks contained: DisposableLock.cs#L23 is the only blocking wait in the client, and
AcquireLock() has just two call sites, OfflineDbContext.cs#L147 and AsyncLockDictionary.cs#L52.

grpc-dotnet hit exactly this and fixed it on their side: grpc/grpc-dotnet#2756.

One thing that would surface the rest: the client is an OS-neutral library, so CA1416 never checks
it against browser. Adding

<ItemGroup>
  <SupportedPlatform Include="browser" />
</ItemGroup>

to the client project makes the analyzer flag these call sites (suggested by @jkotas in
dotnet/runtime#131859).

I realise Blazor WASM isn't in your tested matrix (#174), so treat the priority as you see fit.
I'm glad to send a PR if you'd like one.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions