From ba830f06d345c8b8e8dd301ba10e6501a2df719c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Oct 2025 19:49:34 +0000 Subject: [PATCH 1/3] Initial plan From b8d334ae2c193ef7b4deda56adcf2d37914b363b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Oct 2025 19:54:55 +0000 Subject: [PATCH 2/3] Add breaking change documentation for BackgroundService ExecuteAsync Co-authored-by: gewarren <24882762+gewarren@users.noreply.github.com> --- docs/core/compatibility/10.0.md | 1 + .../backgroundservice-executeasync-task.md | 85 +++++++++++++++++++ docs/core/compatibility/toc.yml | 2 + 3 files changed, 88 insertions(+) create mode 100644 docs/core/compatibility/extensions/10.0/backgroundservice-executeasync-task.md diff --git a/docs/core/compatibility/10.0.md b/docs/core/compatibility/10.0.md index 7d0c66625293f..4a55d76c7dc8a 100644 --- a/docs/core/compatibility/10.0.md +++ b/docs/core/compatibility/10.0.md @@ -74,6 +74,7 @@ If you're migrating an app to .NET 10, the breaking changes listed here might af | Title | Type of change | Introduced version | |-------|---------------------|--------------------| +| [BackgroundService runs all of ExecuteAsync as a Task](extensions/10.0/backgroundservice-executeasync-task.md) | Behavioral change | Preview 1 | | [Null values preserved in configuration](extensions/10.0/configuration-null-values-preserved.md) | Behavioral change | Preview 7 | | [Message no longer duplicated in Console log output](extensions/10.0/console-json-logging-duplicate-messages.md) | Behavioral change | Preview 7 | | [ProviderAliasAttribute moved to Microsoft.Extensions.Logging.Abstractions assembly](extensions/10.0/provideraliasattribute-moved-assembly.md) | Source incompatible | Preview 4 | diff --git a/docs/core/compatibility/extensions/10.0/backgroundservice-executeasync-task.md b/docs/core/compatibility/extensions/10.0/backgroundservice-executeasync-task.md new file mode 100644 index 0000000000000..c32ed91e3020f --- /dev/null +++ b/docs/core/compatibility/extensions/10.0/backgroundservice-executeasync-task.md @@ -0,0 +1,85 @@ +--- +title: "Breaking change: BackgroundService runs all of ExecuteAsync as a Task" +description: "Learn about the breaking change in .NET 10 where BackgroundService now runs the entirety of ExecuteAsync on a background thread instead of running the synchronous portion on the main thread." +ms.date: 10/13/2025 +ai-usage: ai-assisted +ms.custom: https://github.com/dotnet/runtime/issues/116181 +--- + +# BackgroundService runs all of ExecuteAsync as a Task + + now runs the entirety of on a background thread. Previously, the synchronous portion of `ExecuteAsync` (before the first `await`) ran on the main thread during service startup, blocking other services from starting. Only code after the first `await` ran on a background thread. + +## Version introduced + +.NET 10 Preview 1 + +## Previous behavior + +Previously, the synchronous portion of `ExecuteAsync` ran on the main thread and blocked other services from starting. For example: + +```csharp +public class MyBackgroundService : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + // This code ran on the main thread and blocked other services + DoSomeSynchronousWork(); + + // Only after this first await did the code run on a background thread + await Task.Delay(1000, stoppingToken); + + // This code ran on a background thread + while (!stoppingToken.IsCancellationRequested) + { + await DoWorkAsync(stoppingToken); + } + } +} +``` + +This behavior allowed implementers to partition synchronous work before the first `await` and asynchronous work after. However, most implementers didn't understand or expect this nuance. + +## New behavior + +Starting in .NET 10, all of `ExecuteAsync` runs on a background thread, and no part of it blocks other services from starting: + +```csharp +public class MyBackgroundService : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + // All of this code now runs on a background thread + DoSomeSynchronousWork(); + + await Task.Delay(1000, stoppingToken); + + while (!stoppingToken.IsCancellationRequested) + { + await DoWorkAsync(stoppingToken); + } + } +} +``` + +## Type of breaking change + +This is a [behavioral change](../../categories.md#behavioral-change). + +## Reason for change + +The previous behavior was a common pitfall that didn't meet user expectations. Most implementers of `BackgroundService` didn't understand that the synchronous portion before the first `await` would block other services from starting during application startup. + +## Recommended action + +If you require any part of your `BackgroundService.ExecuteAsync` to run earlier during startup (synchronously and blocking other services), you can do any one of the following: + +1. Place that code in the constructor, and it executes as part of the service construction. +2. Override `StartAsync` and do some work before calling `base.StartAsync`. `StartAsync` retains the behavior that its synchronous portion runs synchronously during startup and blocks other services from starting. +3. Implement on your `BackgroundService` if you want to run code at a more specific time during service startup. +4. Forgo `BackgroundService` entirely and implement your own . + +## Affected APIs + +- +- diff --git a/docs/core/compatibility/toc.yml b/docs/core/compatibility/toc.yml index 18c08be85a233..9336afe940e56 100644 --- a/docs/core/compatibility/toc.yml +++ b/docs/core/compatibility/toc.yml @@ -84,6 +84,8 @@ items: href: /ef/core/what-is-new/ef-core-10.0/breaking-changes?toc=/dotnet/core/compatibility/toc.json&bc=/dotnet/breadcrumb/toc.json - name: Extensions items: + - name: BackgroundService runs all of ExecuteAsync as a Task + href: extensions/10.0/backgroundservice-executeasync-task.md - name: Message no longer duplicated in Console log output href: extensions/10.0/console-json-logging-duplicate-messages.md - name: Null values preserved in configuration From 153711e66e2e8264c51079cdc0bbab18b590bb82 Mon Sep 17 00:00:00 2001 From: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Date: Mon, 13 Oct 2025 17:26:13 -0700 Subject: [PATCH 3/3] Apply suggestions from code review --- .../backgroundservice-executeasync-task.md | 59 +++---------------- 1 file changed, 9 insertions(+), 50 deletions(-) diff --git a/docs/core/compatibility/extensions/10.0/backgroundservice-executeasync-task.md b/docs/core/compatibility/extensions/10.0/backgroundservice-executeasync-task.md index c32ed91e3020f..a41155ff77ced 100644 --- a/docs/core/compatibility/extensions/10.0/backgroundservice-executeasync-task.md +++ b/docs/core/compatibility/extensions/10.0/backgroundservice-executeasync-task.md @@ -12,74 +12,33 @@ ms.custom: https://github.com/dotnet/runtime/issues/116181 ## Version introduced -.NET 10 Preview 1 +.NET 10 ## Previous behavior -Previously, the synchronous portion of `ExecuteAsync` ran on the main thread and blocked other services from starting. For example: - -```csharp -public class MyBackgroundService : BackgroundService -{ - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - // This code ran on the main thread and blocked other services - DoSomeSynchronousWork(); - - // Only after this first await did the code run on a background thread - await Task.Delay(1000, stoppingToken); - - // This code ran on a background thread - while (!stoppingToken.IsCancellationRequested) - { - await DoWorkAsync(stoppingToken); - } - } -} -``` - -This behavior allowed implementers to partition synchronous work before the first `await` and asynchronous work after. However, most implementers didn't understand or expect this nuance. +Previously, the synchronous portion of `ExecuteAsync` ran on the main thread and blocked other services from starting. ## New behavior -Starting in .NET 10, all of `ExecuteAsync` runs on a background thread, and no part of it blocks other services from starting: - -```csharp -public class MyBackgroundService : BackgroundService -{ - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - // All of this code now runs on a background thread - DoSomeSynchronousWork(); - - await Task.Delay(1000, stoppingToken); - - while (!stoppingToken.IsCancellationRequested) - { - await DoWorkAsync(stoppingToken); - } - } -} -``` +Starting in .NET 10, all of `ExecuteAsync` runs on a background thread, and no part of it blocks other services from starting. ## Type of breaking change -This is a [behavioral change](../../categories.md#behavioral-change). +This change is a [behavioral change](../../categories.md#behavioral-change). ## Reason for change -The previous behavior was a common pitfall that didn't meet user expectations. Most implementers of `BackgroundService` didn't understand that the synchronous portion before the first `await` would block other services from starting during application startup. +The previous behavior was a common pitfall that didn't meet user expectations. Most implementers of `BackgroundService` didn't understand that the synchronous portion before the first `await` blocked other services from starting during application startup. ## Recommended action If you require any part of your `BackgroundService.ExecuteAsync` to run earlier during startup (synchronously and blocking other services), you can do any one of the following: -1. Place that code in the constructor, and it executes as part of the service construction. -2. Override `StartAsync` and do some work before calling `base.StartAsync`. `StartAsync` retains the behavior that its synchronous portion runs synchronously during startup and blocks other services from starting. -3. Implement on your `BackgroundService` if you want to run code at a more specific time during service startup. -4. Forgo `BackgroundService` entirely and implement your own . +- Place the code that needs to run synchronously in the constructor, and it executes as part of the service construction. +- Override `StartAsync` and do some work before calling `base.StartAsync`. `StartAsync` retains the behavior that its synchronous portion runs synchronously during startup and blocks other services from starting. +- If you want to run code at a more specific time during service startup, implement on your `BackgroundService`. +- Forgo `BackgroundService` entirely and implement your own . ## Affected APIs - --