Skip to content

Commit 0663600

Browse files
authored
Document .NET 11 ChangeToken.OnChange async-overload rebinding behavior (dotnet#55214)
1 parent 8a189e0 commit 0663600

4 files changed

Lines changed: 92 additions & 1 deletion

File tree

docs/core/compatibility/11.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ See [Breaking changes in EF Core 11](/ef/core/what-is-new/ef-core-11.0/breaking-
5858

5959
| Title | Type of change |
6060
|-------|-------------------|
61+
| [ChangeToken.OnChange async overloads rebind existing Task-returning callbacks](extensions/11/changetoken-onchange-async-overloads-rebind-callbacks.md) | Behavioral change |
6162
| [IHost.RunAsync and IHost.StopAsync throw when a BackgroundService fails](extensions/11/ihost-runasync-stopasync-throw-backgroundservice-failure.md) | Behavioral change |
6263
| [Some Microsoft.Extensions packages included in shared framework](extensions/11/extensions-in-shared-framework.md) | Behavioral change |
6364

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
---
2+
title: "Breaking change: ChangeToken.OnChange async overloads rebind existing Task-returning callbacks"
3+
description: "Learn about the breaking change in .NET 11 where new ChangeToken.OnChange async overloads can change overload binding for existing async callbacks."
4+
ms.date: 08/03/2026
5+
ai-usage: ai-assisted
6+
---
7+
8+
# ChangeToken.OnChange async overloads rebind existing Task-returning callbacks
9+
10+
Starting in .NET 11, <xref:Microsoft.Extensions.Primitives.ChangeToken.OnChange*> adds async callback overloads. After you recompile against .NET 11, existing calls that pass an `async` lambda or a `Task`-returning callback can silently bind to a different overload and behave differently at runtime.
11+
12+
## Version introduced
13+
14+
.NET 11 Preview 7
15+
16+
## Previous behavior
17+
18+
Previously, `ChangeToken.OnChange` only exposed synchronous callback overloads:
19+
20+
```csharp
21+
public static IDisposable OnChange(Func<IChangeToken?> changeTokenProducer, Action changeTokenConsumer);
22+
public static IDisposable OnChange<TState>(Func<IChangeToken?> changeTokenProducer, Action<TState> changeTokenConsumer, TState state);
23+
```
24+
25+
If you passed an `async` lambda, the compiler bound the call to an `Action` overload and compiled the lambda as `async void`. The callback behaved as fire-and-forget. `ChangeToken.OnChange` re-registered for the next change as soon as the callback yielded at its first incomplete `await`, and exceptions thrown later surfaced on the synchronization context or thread pool.
26+
27+
```csharp
28+
// Bound to OnChange(Func<IChangeToken?>, Action) and compiled as 'async void'.
29+
ChangeToken.OnChange(config.GetReloadToken, async () =>
30+
{
31+
await Task.Delay(1000);
32+
Console.WriteLine("Reloaded");
33+
});
34+
```
35+
36+
## New behavior
37+
38+
Starting in .NET 11, `ChangeToken.OnChange` includes two asynchronous callback overloads:
39+
40+
```csharp
41+
public static IDisposable OnChange(Func<IChangeToken?> changeTokenProducer, Func<Task> changeTokenConsumer);
42+
public static IDisposable OnChange<TState>(Func<IChangeToken?> changeTokenProducer, Func<TState, Task> changeTokenConsumer, TState state);
43+
```
44+
45+
If you pass an `async` lambda or another `Task`-returning callback, the compiler now binds to `Func<Task>` or `Func<TState, Task>`. The callback compiles as `async Task` instead of `async void`, and `ChangeToken.OnChange` re-registers only after the returned task completes. If multiple changes occur while the callback task runs, `ChangeToken.OnChange` coalesces those changes into one later callback invocation.
46+
47+
```csharp
48+
// Binds to OnChange(Func<IChangeToken?>, Func<Task>) and compiles as 'async Task'.
49+
// The token re-registers after the returned task completes.
50+
ChangeToken.OnChange(config.GetReloadToken, async () =>
51+
{
52+
await Task.Delay(1000);
53+
Console.WriteLine("Reloaded");
54+
});
55+
```
56+
57+
This overload rebinding is silent. The same source code still compiles, and the compiler reports no ambiguity.
58+
59+
## Type of breaking change
60+
61+
This change is a [behavioral change](../../categories.md#behavioral-change).
62+
63+
## Reason for change
64+
65+
To support correct asynchronous workflows, `ChangeToken.OnChange` now provides callback overloads that return `Task`. Before .NET 11, asynchronous callback logic required `async void` or synchronous blocking, which made error handling and callback timing harder to control. For API approval details, see [dotnet/runtime#69099](https://github.com/dotnet/runtime/issues/69099).
66+
67+
## Recommended action
68+
69+
For most code, no action is required. The new binding usually improves behavior because asynchronous work now completes before `ChangeToken.OnChange` re-registers for the next notification.
70+
71+
If you need the previous fire-and-forget `async void` behavior, cast the callback to `Action` (or `Action<TState>`) so your call continues to bind to the synchronous overload:
72+
73+
```csharp
74+
ChangeToken.OnChange(config.GetReloadToken, (Action)(async () =>
75+
{
76+
await Task.Delay(1000);
77+
Console.WriteLine("Reloaded");
78+
}));
79+
```
80+
81+
You can't control this behavior with an AppContext switch or configuration setting. Overload selection happens at compile time.
82+
83+
## Affected APIs
84+
85+
- `OnChange(Func<IChangeToken?> changeTokenProducer, Func<Task> changeTokenConsumer)` <!-- <xref:Microsoft.Extensions.Primitives.ChangeToken.OnChange(System.Func{Microsoft.Extensions.Primitives.IChangeToken},System.Func{System.Threading.Tasks.Task})?displayProperty=fullName> -->
86+
- `OnChange<TState>(Func<IChangeToken?> changeTokenProducer, Func<TState, Task> changeTokenConsumer, TState state)` <!-- <xref:Microsoft.Extensions.Primitives.ChangeToken.OnChange``1(System.Func{Microsoft.Extensions.Primitives.IChangeToken},System.Func{``0,System.Threading.Tasks.Task},``0)?displayProperty=fullName> -->
87+
- <xref:Microsoft.Extensions.Primitives.ChangeToken.OnChange(System.Func{Microsoft.Extensions.Primitives.IChangeToken},System.Action)?displayProperty=fullName>
88+
- <xref:Microsoft.Extensions.Primitives.ChangeToken.OnChange``1(System.Func{Microsoft.Extensions.Primitives.IChangeToken},System.Action{``0},``0)?displayProperty=fullName>

docs/core/compatibility/toc.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ items:
4646
href: deployment/11/runtimeconfigdev-configproperties-precedence.md
4747
- name: Extensions
4848
items:
49+
- name: ChangeToken.OnChange async overloads rebind existing Task-returning callbacks
50+
href: extensions/11/changetoken-onchange-async-overloads-rebind-callbacks.md
4951
- name: IHost.RunAsync and IHost.StopAsync throw when a BackgroundService fails
5052
href: extensions/11/ihost-runasync-stopasync-throw-backgroundservice-failure.md
5153
- name: Some Microsoft.Extensions packages included in shared framework

docs/core/extensions/primitives.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ As an alternative to calling `RegisterChangeCallback`, you could use the <xref:M
4848

4949
:::code source="./snippets/primitives/change/Example.Static.cs" id="Static":::
5050

51-
Much like previous examples, you'll need an implementation of `IChangeToken` that is produced by the `changeTokenProducer`. The producer is defined as a `Func<IChangeToken>` and it's expected that this will return a new token every invocation. The `consumer` is either an `Action` when not using `state`, or an `Action<TState>` where the generic type `TState` flows through the change notification.
51+
Much like previous examples, you'll need an implementation of `IChangeToken` that is produced by the `changeTokenProducer`. The producer is defined as a `Func<IChangeToken>` and it's expected that this will return a new token every invocation. The `consumer` is an `Action` or `Func<Task>` when not using `state`, or an `Action<TState>` or `Func<TState, Task>` where the generic type `TState` flows through the change notification.
5252

5353
## String tokenizers, segments, and values
5454

0 commit comments

Comments
 (0)