The start of this issue was during an upgrade of NLog (see issue).
Summary: I have a legacy application that uses FileSystemWatcher
and I wanted to upgrade NLog from version 4 to 5. Internally this includes a switch from CallContext
to AsyncLocal
. If I try to access the value of the AsyncLocal
inside the raised event of the watcher it is not available on .NET Framework 4.8 but on .NET 6+.
Here is a repro test (xUnit) that fails on .NET Framework 4.8
private readonly static AsyncLocal<string> s_test = new AsyncLocal<string>();
[Fact]
public void AsyncLocalTest()
{
const string EXPECTED_VALUE = "YES";
s_test.Value = EXPECTED_VALUE;
string basePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
try
{
Directory.CreateDirectory(basePath);
FileSystemWatcher fileSystemWatcher = new FileSystemWatcher(basePath) {
EnableRaisingEvents = true
};
string GetProperty() => s_test.Value ?? "";
using (fileSystemWatcher)
{
Assert.Equal(EXPECTED_VALUE, GetProperty());
using (ManualResetEvent resetEvent = new ManualResetEvent(false))
{
string result = "";
fileSystemWatcher.Created += (object sender, FileSystemEventArgs e) => {
result = GetProperty();
resetEvent.Set();
};
string filePath = Path.Combine(basePath, "abc.txt");
// trigger the event
File.WriteAllText(filePath, "YES");
Assert.True(resetEvent.WaitOne(5000));
Assert.Equal(EXPECTED_VALUE, result); // <!-- fails here because result is "" and not "YES"
}
}
}
finally
{
Directory.Delete(basePath, true);
}
}
I read the source code of the FileSystemWatcher
and found that the SynchronizingObject
may do the job for me. So I wrote a basic implementation that captures the ExecutionContext
(AsyncLocal
uses that internally).
private sealed class FileSystemWatcherSynchronizingObject : ISynchronizeInvoke
{
private readonly ExecutionContext? _executionContext = ExecutionContext.Capture();
private object? Run(Delegate method, object?[]? args)
{
if (_executionContext != null)
{
object? result = null;
ExecutionContext.Run(_executionContext, _ => result = method.DynamicInvoke(args), null);
return result;
}
else
{
return method.DynamicInvoke(args);
}
}
public bool InvokeRequired => true;
public IAsyncResult BeginInvoke(Delegate method, object?[]? args) => Task.Run(() => Run(method, args));
public object? EndInvoke(IAsyncResult result) => ((Task<object?>)result).Result;
public object? Invoke(Delegate method, object?[]? args) => Run(method, args);
}
Now we just have to attach this to the FileSystemWatcher
if we are on .NET Framework 4.8 and the test succeeds:
FileSystemWatcher fileSystemWatcher = new FileSystemWatcher(basePath) {
EnableRaisingEvents = true,
#if NET48_OR_GREATER
SynchronizingObject = new FileSystemWatcherSynchronizingObject()
#endif
};
As I do not have any deep knowledge about these things, I wanted to know if there is any better approach or any better implementation of FileSystemWatcherSynchronizingObject
. I assume that there is a better way than Task.Run(...)
in BeginInvoke
.