-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Capture ExecutionContext for Dispatcher.InvokeAsync #19163
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
MrJul
merged 11 commits into
AvaloniaUI:master
from
dotnet-campus:t/walterlv/dispatcher-execution-context
Aug 14, 2025
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
7673e99
Capture ExecutionContext for Dispatcher.InvokeAsync
walterlv 7ecc6e0
Implement CulturePreservingExecutionContext
walterlv e899e16
Add IsFlowSuppressed checking
walterlv c690d9d
Add NET6_0_OR_GREATER because only the Restore need it.
walterlv a522745
Use `ExecutionContext.Run` instead of `ExecutionContext.Restore`.
walterlv 749b837
Pass this to avoid lambda capture.
walterlv 65d6034
Use ExecutionContext directly on NET6_0_OR_GREATER
walterlv 0e6a440
on NET6_0_OR_GREATER, use Restore so we can get a simple stack trace.
walterlv 468d963
Add unit tests.
walterlv b257b5b
All test code must run inside Task.Run to avoid interfering with the …
walterlv e73db7a
First, test Task.Run to ensure that the preceding validation always p…
walterlv File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
156 changes: 156 additions & 0 deletions
156
src/Avalonia.Base/Threading/CulturePreservingExecutionContext.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| #if NET6_0_OR_GREATER | ||
| // In .NET Core, the security context and call context are not supported, however, | ||
| // the impersonation context and culture would typically flow with the execution context. | ||
| // See: https://learn.microsoft.com/en-us/dotnet/api/system.threading.executioncontext | ||
| // | ||
| // So we can safely use ExecutionContext without worrying about culture flowing issues. | ||
| #else | ||
| using System; | ||
| using System.Diagnostics; | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using System.Globalization; | ||
| using System.Threading; | ||
|
|
||
| namespace Avalonia.Threading; | ||
|
|
||
| /// <summary> | ||
| /// An ExecutionContext that preserves culture information across async operations. | ||
| /// This is a modernized version that removes legacy compatibility switches and | ||
| /// includes nullable reference type annotations. | ||
| /// </summary> | ||
| internal sealed class CulturePreservingExecutionContext | ||
| { | ||
| private readonly ExecutionContext _context; | ||
| private CultureAndContext? _cultureAndContext; | ||
|
|
||
| private CulturePreservingExecutionContext(ExecutionContext context) | ||
| { | ||
| _context = context; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Captures the current ExecutionContext and culture information. | ||
| /// </summary> | ||
| /// <returns>A new CulturePreservingExecutionContext instance, or null if no context needs to be captured.</returns> | ||
| public static CulturePreservingExecutionContext? Capture() | ||
| { | ||
| // ExecutionContext.SuppressFlow had been called. | ||
| // We expect ExecutionContext.Capture() to return null, so match that behavior and return null. | ||
| if (ExecutionContext.IsFlowSuppressed()) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var context = ExecutionContext.Capture(); | ||
| if (context == null) | ||
| return null; | ||
|
|
||
| return new CulturePreservingExecutionContext(context); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Runs the specified callback in the captured execution context while preserving culture information. | ||
| /// This method is used for .NET Framework and earlier .NET versions. | ||
| /// </summary> | ||
| /// <param name="executionContext">The execution context to run in.</param> | ||
| /// <param name="callback">The callback to execute.</param> | ||
| /// <param name="state">The state to pass to the callback.</param> | ||
| public static void Run(CulturePreservingExecutionContext executionContext, ContextCallback callback, object? state) | ||
| { | ||
| // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract | ||
| if (callback == null) | ||
| return; | ||
|
|
||
| // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract | ||
| if (executionContext == null) | ||
| ThrowNullContext(); | ||
|
|
||
| // Save culture information - we will need this to restore just before | ||
| // the callback is actually invoked from CallbackWrapper. | ||
| executionContext._cultureAndContext = CultureAndContext.Initialize(callback, state); | ||
|
|
||
| try | ||
| { | ||
| ExecutionContext.Run( | ||
| executionContext._context, | ||
| s_callbackWrapperDelegate, | ||
| executionContext._cultureAndContext); | ||
| } | ||
| finally | ||
| { | ||
| // Restore culture information - it might have been modified during callback execution. | ||
| executionContext._cultureAndContext.RestoreCultureInfos(); | ||
| } | ||
| } | ||
|
|
||
| [DoesNotReturn] | ||
| private static void ThrowNullContext() | ||
| { | ||
| throw new InvalidOperationException("ExecutionContext cannot be null."); | ||
| } | ||
|
|
||
| private static readonly ContextCallback s_callbackWrapperDelegate = CallbackWrapper; | ||
|
|
||
| /// <summary> | ||
| /// Executes the callback and saves culture values immediately afterwards. | ||
| /// </summary> | ||
| /// <param name="obj">Contains the actual callback and state.</param> | ||
| private static void CallbackWrapper(object? obj) | ||
| { | ||
| var cultureAndContext = (CultureAndContext)obj!; | ||
|
|
||
| // Restore culture information saved during Run() | ||
| cultureAndContext.RestoreCultureInfos(); | ||
|
|
||
| try | ||
| { | ||
| // Execute the actual callback | ||
| cultureAndContext.Callback(cultureAndContext.State); | ||
| } | ||
| finally | ||
| { | ||
| // Save any culture changes that might have occurred during callback execution | ||
| cultureAndContext.CaptureCultureInfos(); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Helper class to manage culture information across execution contexts. | ||
| /// </summary> | ||
| private sealed class CultureAndContext | ||
| { | ||
| public ContextCallback Callback { get; } | ||
| public object? State { get; } | ||
|
|
||
| private CultureInfo? _culture; | ||
| private CultureInfo? _uiCulture; | ||
|
|
||
| private CultureAndContext(ContextCallback callback, object? state) | ||
| { | ||
| Callback = callback; | ||
| State = state; | ||
| CaptureCultureInfos(); | ||
| } | ||
|
|
||
| public static CultureAndContext Initialize(ContextCallback callback, object? state) | ||
| { | ||
| return new CultureAndContext(callback, state); | ||
| } | ||
|
|
||
| public void CaptureCultureInfos() | ||
| { | ||
| _culture = Thread.CurrentThread.CurrentCulture; | ||
| _uiCulture = Thread.CurrentThread.CurrentUICulture; | ||
| } | ||
|
|
||
| public void RestoreCultureInfos() | ||
| { | ||
| if (_culture != null) | ||
| Thread.CurrentThread.CurrentCulture = _culture; | ||
|
|
||
| if (_uiCulture != null) | ||
| Thread.CurrentThread.CurrentUICulture = _uiCulture; | ||
| } | ||
| } | ||
| } | ||
| #endif | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.