Browse Source
* Capture ExecutionContext for Dispatcher.InvokeAsync (cherry picked from commit e41c9272ac45afc31007a8fe26f9f56291b063ef) * Implement CulturePreservingExecutionContext * Add IsFlowSuppressed checking * Add NET6_0_OR_GREATER because only the Restore need it. * Use `ExecutionContext.Run` instead of `ExecutionContext.Restore`. * Pass this to avoid lambda capture. * Use ExecutionContext directly on NET6_0_OR_GREATER * on NET6_0_OR_GREATER, use Restore so we can get a simple stack trace. * Add unit tests. * All test code must run inside Task.Run to avoid interfering with the test * First, test Task.Run to ensure that the preceding validation always passes, serving as a baseline for the subsequent Invoke/InvokeAsync tests. This way, if a later test fails, we have the .NET framework's baseline behavior for reference.pull/19469/head
committed by
GitHub
3 changed files with 403 additions and 52 deletions
@ -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
|
||||
Loading…
Reference in new issue