From 2d57d0a16417f3879aeca1265f99f7b8f82b88f6 Mon Sep 17 00:00:00 2001 From: Petr Onderka Date: Tue, 12 May 2026 14:11:35 +0200 Subject: [PATCH 1/3] Avoid recursive FileSystemWatcher when not required in PhysicalFilesWatcher PhysicalFilesWatcher previously set IncludeSubdirectories=true unconditionally on the wrapped FileSystemWatcher. On Linux this causes inotify watches to be added for every descendant directory under the root, which can be very expensive (or hit inotify limits) when the root is something like / -- as happens with applications launched by systemd with no explicit WorkingDirectory. Now IncludeSubdirectories is enabled only when at least one registered token's pattern actually references a subdirectory (file path contains '/', or wildcard contains '/' or '**'), or when the wrapped FSW watches a strict ancestor of root (where every event is in a subdirectory from the FSW's perspective). The state is maintained via a counter updated when tokens are added or removed, and re-evaluated each time the FSW is enabled. Fixes #126708 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/PhysicalFilesWatcher.cs | 83 ++++++++++-- .../tests/PhysicalFilesWatcherTests.cs | 119 ++++++++++++++++++ 2 files changed, 194 insertions(+), 8 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs b/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs index 477bd8ca2045c8..299c02248ae636 100644 --- a/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs +++ b/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs @@ -34,6 +34,13 @@ public class PhysicalFilesWatcher : IDisposable private readonly object _fileWatcherLock = new(); private readonly string _root; private readonly ExclusionFilters _filters; + // True when the FileSystemWatcher watches a strict ancestor of _root (rather than _root + // itself). In that case IncludeSubdirectories must always be true so that events occurring + // inside _root (which is below the FSW's watched path) are observed. + private readonly bool _fileWatcherIsAboveRoot; + // Number of currently registered tokens whose pattern requires watching subdirectories. + // Maintained as tokens are added and removed so we don't iterate the lookups. + private int _subdirectoryRequiringTokenCount; // A single non-recursive watcher used when _root does not exist. // Watches for _root to be created, then enables the main FileSystemWatcher. @@ -111,10 +118,18 @@ public PhysicalFilesWatcher( { throw new ArgumentException(SR.Format(SR.FileSystemWatcherPathError, watcherFullPath, _root), nameof(fileSystemWatcher)); } + + // If the FSW watches an ancestor of _root, every event of interest occurs + // in a subdirectory from the FSW's perspective, so subdirectory watching + // is required to observe any of them. + _fileWatcherIsAboveRoot = !watcherFullPath.Equals(_root, StringComparison.OrdinalIgnoreCase) && + _root.StartsWith(watcherFullPath, StringComparison.OrdinalIgnoreCase); } _fileWatcher = fileSystemWatcher; - _fileWatcher.IncludeSubdirectories = true; + // IncludeSubdirectories is set in TryEnableFileSystemWatcher based on whether + // any registered token's pattern actually requires watching subdirectories. + _fileWatcher.IncludeSubdirectories = false; _fileWatcher.Created += OnChanged; _fileWatcher.Changed += OnChanged; _fileWatcher.Renamed += OnRenamed; @@ -189,8 +204,16 @@ internal IChangeToken GetOrAddFilePathChangeToken(string filePath) { var cancellationTokenSource = new CancellationTokenSource(); var cancellationChangeToken = new CancellationChangeToken(cancellationTokenSource.Token); - tokenInfo = new ChangeTokenInfo(cancellationTokenSource, cancellationChangeToken); - tokenInfo = _filePathTokenLookup.GetOrAdd(filePath, tokenInfo); + var newTokenInfo = new ChangeTokenInfo(cancellationTokenSource, cancellationChangeToken); + tokenInfo = _filePathTokenLookup.GetOrAdd(filePath, newTokenInfo); + + // GetOrAdd may not have actually added our entry if another thread won the race. + // Compare by reference to detect whether our entry was the one stored. + if (ReferenceEquals(tokenInfo.TokenSource, cancellationTokenSource) && + FilePathRequiresSubdirectories(filePath)) + { + Interlocked.Increment(ref _subdirectoryRequiringTokenCount); + } } IChangeToken changeToken = tokenInfo.ChangeToken; @@ -227,8 +250,14 @@ internal IChangeToken GetOrAddWildcardChangeToken(string pattern) var cancellationChangeToken = new CancellationChangeToken(cancellationTokenSource.Token); var matcher = new Matcher(StringComparison.OrdinalIgnoreCase); matcher.AddInclude(pattern); - tokenInfo = new ChangeTokenInfo(cancellationTokenSource, cancellationChangeToken, matcher); - tokenInfo = _wildcardTokenLookup.GetOrAdd(pattern, tokenInfo); + var newTokenInfo = new ChangeTokenInfo(cancellationTokenSource, cancellationChangeToken, matcher); + tokenInfo = _wildcardTokenLookup.GetOrAdd(pattern, newTokenInfo); + + if (ReferenceEquals(tokenInfo.TokenSource, cancellationTokenSource) && + WildcardRequiresSubdirectories(pattern)) + { + Interlocked.Increment(ref _subdirectoryRequiringTokenCount); + } } IChangeToken changeToken = tokenInfo.ChangeToken; @@ -347,17 +376,22 @@ private void OnChanged(object sender, FileSystemEventArgs e) private void OnError(object sender, ErrorEventArgs e) { // Notify all cache entries on error. - CancelAll(_filePathTokenLookup); - CancelAll(_wildcardTokenLookup); + CancelAll(_filePathTokenLookup, FilePathRequiresSubdirectories); + CancelAll(_wildcardTokenLookup, WildcardRequiresSubdirectories); TryDisableFileSystemWatcher(); - static void CancelAll(ConcurrentDictionary tokens) + void CancelAll(ConcurrentDictionary tokens, Func requiresSubdirectories) { foreach (KeyValuePair entry in tokens) { if (tokens.TryRemove(entry.Key, out ChangeTokenInfo matchInfo)) { + if (requiresSubdirectories(entry.Key)) + { + Interlocked.Decrement(ref _subdirectoryRequiringTokenCount); + } + CancelToken(matchInfo); } } @@ -418,6 +452,11 @@ private void ReportChangeForMatchedEntries(string path) bool matched = false; if (_filePathTokenLookup.TryRemove(path, out ChangeTokenInfo matchInfo)) { + if (FilePathRequiresSubdirectories(path)) + { + Interlocked.Decrement(ref _subdirectoryRequiringTokenCount); + } + CancelToken(matchInfo); matched = true; } @@ -428,6 +467,11 @@ private void ReportChangeForMatchedEntries(string path) if (matchResult.HasMatches && _wildcardTokenLookup.TryRemove(wildCardEntry.Key, out matchInfo)) { + if (WildcardRequiresSubdirectories(wildCardEntry.Key)) + { + Interlocked.Decrement(ref _subdirectoryRequiringTokenCount); + } + CancelToken(matchInfo); matched = true; } @@ -529,6 +573,17 @@ private void TryEnableFileSystemWatcher() { bool rootExists = Directory.Exists(_root); + // Only enable recursive subdirectory watching when at least one registered + // pattern actually references a subdirectory. This avoids creating an inotify + // watch on every descendant directory on Linux when only root-level files + // (e.g. appsettings.json) are being monitored. + bool needsSubdirectories = _fileWatcherIsAboveRoot || + Volatile.Read(ref _subdirectoryRequiringTokenCount) > 0; + if (_fileWatcher.IncludeSubdirectories != needsSubdirectories) + { + _fileWatcher.IncludeSubdirectories = needsSubdirectories; + } + // On some platforms (e.g., Linux), FileSystemWatcher currently does not // invoke OnError when the watched directory is deleted, so we don't disable // the FSW and start root watcher at that point. @@ -655,6 +710,18 @@ private void EnsureRootCreationWatcher() } } + // Patterns are normalized to use forward slashes. A file path token references a single + // file, so it requires subdirectory watching only when the path is in a subdirectory + // (i.e. contains '/'). + private static bool FilePathRequiresSubdirectories(string normalizedFilePath) => + normalizedFilePath.Contains('/'); + + // A wildcard pattern requires subdirectory watching when it explicitly references a + // subdirectory (contains '/') or uses the recursive globbing wildcard '**'. A simple + // wildcard like '*' or '*.json' only matches entries directly in the root directory. + private static bool WildcardRequiresSubdirectories(string normalizedPattern) => + normalizedPattern.Contains('/') || normalizedPattern.Contains("**"); + private static string NormalizePath(string filter) => filter.Replace('\\', '/'); private static bool IsDirectoryPath(string path) diff --git a/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFilesWatcherTests.cs b/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFilesWatcherTests.cs index 7bee9f6a45f3a9..c188836f9c6d33 100644 --- a/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFilesWatcherTests.cs +++ b/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFilesWatcherTests.cs @@ -69,6 +69,125 @@ private static async Task WhenChanged(IChangeToken token, bool withTimeout = tru } } + [Theory] + [InlineData(new[] { "appsettings.json" }, false)] + [InlineData(new[] { "appsettings.json", "appsettings.Production.json" }, false)] + [InlineData(new[] { "*.json" }, false)] + [InlineData(new[] { "*" }, false)] + [InlineData(new[] { "sub/file.json" }, true)] + [InlineData(new[] { "sub/*.json" }, true)] + [InlineData(new[] { "**/appsettings.json" }, true)] + [InlineData(new[] { "**" }, true)] + [InlineData(new[] { "sub/" }, true)] + [InlineData(new[] { "appsettings.json", "sub/file.json" }, true)] + [SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.iOS | TestPlatforms.tvOS, "System.IO.FileSystem.Watcher is not supported on Browser/iOS/tvOS")] + public void IncludeSubdirectories_OnlyEnabledWhenAPatternRequiresIt(string[] patterns, bool expectedIncludeSubdirectories) + { + using var root = new TempDirectory(GetTestFilePath()); + using var fileSystemWatcher = new MockFileSystemWatcher(root.Path); + using var physicalFilesWatcher = new PhysicalFilesWatcher(root.Path, fileSystemWatcher, pollForChanges: false); + + foreach (string pattern in patterns) + { + physicalFilesWatcher.CreateFileChangeToken(pattern); + } + + Assert.Equal(expectedIncludeSubdirectories, fileSystemWatcher.IncludeSubdirectories); + Assert.True(fileSystemWatcher.EnableRaisingEvents); + } + + [Fact] + [SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.iOS | TestPlatforms.tvOS, "System.IO.FileSystem.Watcher is not supported on Browser/iOS/tvOS")] + public void IncludeSubdirectories_UpgradedWhenSubdirectoryPatternAddedLater() + { + using var root = new TempDirectory(GetTestFilePath()); + using var fileSystemWatcher = new MockFileSystemWatcher(root.Path); + using var physicalFilesWatcher = new PhysicalFilesWatcher(root.Path, fileSystemWatcher, pollForChanges: false); + + physicalFilesWatcher.CreateFileChangeToken("appsettings.json"); + Assert.False(fileSystemWatcher.IncludeSubdirectories); + + physicalFilesWatcher.CreateFileChangeToken("sub/file.json"); + Assert.True(fileSystemWatcher.IncludeSubdirectories); + } + + [Fact] + [SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.iOS | TestPlatforms.tvOS, "System.IO.FileSystem.Watcher is not supported on Browser/iOS/tvOS")] + public void IncludeSubdirectories_DowngradedWhenSubdirectoryPatternRemoved() + { + using var root = new TempDirectory(GetTestFilePath()); + using var fileSystemWatcher = new MockFileSystemWatcher(root.Path); + using var physicalFilesWatcher = new PhysicalFilesWatcher(root.Path, fileSystemWatcher, pollForChanges: false); + + physicalFilesWatcher.CreateFileChangeToken("sub/file.json"); + Assert.True(fileSystemWatcher.IncludeSubdirectories); + + // Fire an event matching the subdirectory token to remove it from the lookup. + fileSystemWatcher.CallOnChanged(new FileSystemEventArgs(WatcherChangeTypes.Changed, root.Path, "sub/file.json")); + + // Adding a new root-only token re-evaluates IncludeSubdirectories. + physicalFilesWatcher.CreateFileChangeToken("appsettings.json"); + Assert.False(fileSystemWatcher.IncludeSubdirectories); + } + + [Fact] + [SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.iOS | TestPlatforms.tvOS, "System.IO.FileSystem.Watcher is not supported on Browser/iOS/tvOS")] + public void IncludeSubdirectories_NotDowngradedWhileSubdirectoryPatternRemains() + { + using var root = new TempDirectory(GetTestFilePath()); + using var fileSystemWatcher = new MockFileSystemWatcher(root.Path); + using var physicalFilesWatcher = new PhysicalFilesWatcher(root.Path, fileSystemWatcher, pollForChanges: false); + + physicalFilesWatcher.CreateFileChangeToken("sub/file.json"); + physicalFilesWatcher.CreateFileChangeToken("appsettings.json"); + Assert.True(fileSystemWatcher.IncludeSubdirectories); + + // Remove only the root-level token; the subdirectory token must keep subdirectories enabled. + fileSystemWatcher.CallOnChanged(new FileSystemEventArgs(WatcherChangeTypes.Changed, root.Path, "appsettings.json")); + Assert.True(fileSystemWatcher.IncludeSubdirectories); + + // Trigger re-evaluation by adding another root-only token. + physicalFilesWatcher.CreateFileChangeToken("other.json"); + Assert.True(fileSystemWatcher.IncludeSubdirectories); + } + + [Fact] + [SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.iOS | TestPlatforms.tvOS, "System.IO.FileSystem.Watcher is not supported on Browser/iOS/tvOS")] + public void IncludeSubdirectories_NotDowngradedWhenWildcardSubdirectoryPatternRemains() + { + using var root = new TempDirectory(GetTestFilePath()); + using var fileSystemWatcher = new MockFileSystemWatcher(root.Path); + using var physicalFilesWatcher = new PhysicalFilesWatcher(root.Path, fileSystemWatcher, pollForChanges: false); + + physicalFilesWatcher.CreateFileChangeToken("sub/*.json"); + physicalFilesWatcher.CreateFileChangeToken("appsettings.json"); + Assert.True(fileSystemWatcher.IncludeSubdirectories); + + // Remove the root-level token; the recursive wildcard token must keep subdirectories enabled. + fileSystemWatcher.CallOnChanged(new FileSystemEventArgs(WatcherChangeTypes.Changed, root.Path, "appsettings.json")); + Assert.True(fileSystemWatcher.IncludeSubdirectories); + + physicalFilesWatcher.CreateFileChangeToken("other.json"); + Assert.True(fileSystemWatcher.IncludeSubdirectories); + } + + [Fact] + [SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.iOS | TestPlatforms.tvOS, "System.IO.FileSystem.Watcher is not supported on Browser/iOS/tvOS")] + public void IncludeSubdirectories_AlwaysTrueWhenWatcherIsAboveRoot() + { + using var tempDir = new TempDirectory(GetTestFilePath()); + string rootDir = Path.Combine(tempDir.Path, "rootDir"); + Directory.CreateDirectory(rootDir); + + using var fsw = new FileSystemWatcher(tempDir.Path); + using var physicalFilesWatcher = new PhysicalFilesWatcher(rootDir, fsw, pollForChanges: false); + + // Even with only a root-only pattern, IncludeSubdirectories must be true because + // the FSW watches an ancestor of _root. + physicalFilesWatcher.CreateFileChangeToken("appsettings.json"); + Assert.True(fsw.IncludeSubdirectories); + } + [Fact] [SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.iOS | TestPlatforms.tvOS, "System.IO.FileSystem.Watcher is not supported on Browser/iOS/tvOS")] public void CreateFileChangeToken_DoesNotAllowPathsAboveRoot() From ad3da868e067a33b436863a668575ef4aabef156 Mon Sep 17 00:00:00 2001 From: Petr Onderka Date: Wed, 13 May 2026 16:09:22 +0200 Subject: [PATCH 2/3] Addressed Copilot review comments --- .../src/PhysicalFilesWatcher.cs | 23 ++++++++---- .../tests/PhysicalFilesWatcherTests.cs | 35 +++++++++++++------ 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs b/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs index 299c02248ae636..50d9dbfe773319 100644 --- a/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs +++ b/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs @@ -39,7 +39,8 @@ public class PhysicalFilesWatcher : IDisposable // inside _root (which is below the FSW's watched path) are observed. private readonly bool _fileWatcherIsAboveRoot; // Number of currently registered tokens whose pattern requires watching subdirectories. - // Maintained as tokens are added and removed so we don't iterate the lookups. + // Maintained as tokens are added and removed so we don't iterate the lookups when + // re-evaluating IncludeSubdirectories. private int _subdirectoryRequiringTokenCount; // A single non-recursive watcher used when _root does not exist. @@ -127,9 +128,6 @@ public PhysicalFilesWatcher( } _fileWatcher = fileSystemWatcher; - // IncludeSubdirectories is set in TryEnableFileSystemWatcher based on whether - // any registered token's pattern actually requires watching subdirectories. - _fileWatcher.IncludeSubdirectories = false; _fileWatcher.Created += OnChanged; _fileWatcher.Changed += OnChanged; _fileWatcher.Renamed += OnRenamed; @@ -389,7 +387,8 @@ void CancelAll(ConcurrentDictionary tokens, Func= 0, "Subdirectory-requiring token counter went negative."); } CancelToken(matchInfo); @@ -454,7 +453,8 @@ private void ReportChangeForMatchedEntries(string path) { if (FilePathRequiresSubdirectories(path)) { - Interlocked.Decrement(ref _subdirectoryRequiringTokenCount); + int newCount = Interlocked.Decrement(ref _subdirectoryRequiringTokenCount); + Debug.Assert(newCount >= 0, "Subdirectory-requiring token counter went negative."); } CancelToken(matchInfo); @@ -469,7 +469,8 @@ private void ReportChangeForMatchedEntries(string path) { if (WildcardRequiresSubdirectories(wildCardEntry.Key)) { - Interlocked.Decrement(ref _subdirectoryRequiringTokenCount); + int newCount = Interlocked.Decrement(ref _subdirectoryRequiringTokenCount); + Debug.Assert(newCount >= 0, "Subdirectory-requiring token counter went negative."); } CancelToken(matchInfo); @@ -535,6 +536,14 @@ private void TryDisableFileSystemWatcher() // Perf: Turn off the file monitoring if no files or directories to monitor. _fileWatcher.EnableRaisingEvents = false; } + else if (_fileWatcher.IncludeSubdirectories && + !_fileWatcherIsAboveRoot && + Volatile.Read(ref _subdirectoryRequiringTokenCount) == 0) + { + // Perf: Some tokens were removed and none of the remaining ones require + // subdirectory watching, so we can stop recursing. + _fileWatcher.IncludeSubdirectories = false; + } } } diff --git a/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFilesWatcherTests.cs b/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFilesWatcherTests.cs index c188836f9c6d33..4581188c71ab44 100644 --- a/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFilesWatcherTests.cs +++ b/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFilesWatcherTests.cs @@ -119,14 +119,13 @@ public void IncludeSubdirectories_DowngradedWhenSubdirectoryPatternRemoved() using var fileSystemWatcher = new MockFileSystemWatcher(root.Path); using var physicalFilesWatcher = new PhysicalFilesWatcher(root.Path, fileSystemWatcher, pollForChanges: false); + physicalFilesWatcher.CreateFileChangeToken("appsettings.json"); physicalFilesWatcher.CreateFileChangeToken("sub/file.json"); Assert.True(fileSystemWatcher.IncludeSubdirectories); // Fire an event matching the subdirectory token to remove it from the lookup. + // The remaining root-only token does not require subdirectory watching, IncludeSubdirectories should be downgraded. fileSystemWatcher.CallOnChanged(new FileSystemEventArgs(WatcherChangeTypes.Changed, root.Path, "sub/file.json")); - - // Adding a new root-only token re-evaluates IncludeSubdirectories. - physicalFilesWatcher.CreateFileChangeToken("appsettings.json"); Assert.False(fileSystemWatcher.IncludeSubdirectories); } @@ -145,10 +144,6 @@ public void IncludeSubdirectories_NotDowngradedWhileSubdirectoryPatternRemains() // Remove only the root-level token; the subdirectory token must keep subdirectories enabled. fileSystemWatcher.CallOnChanged(new FileSystemEventArgs(WatcherChangeTypes.Changed, root.Path, "appsettings.json")); Assert.True(fileSystemWatcher.IncludeSubdirectories); - - // Trigger re-evaluation by adding another root-only token. - physicalFilesWatcher.CreateFileChangeToken("other.json"); - Assert.True(fileSystemWatcher.IncludeSubdirectories); } [Fact] @@ -166,9 +161,6 @@ public void IncludeSubdirectories_NotDowngradedWhenWildcardSubdirectoryPatternRe // Remove the root-level token; the recursive wildcard token must keep subdirectories enabled. fileSystemWatcher.CallOnChanged(new FileSystemEventArgs(WatcherChangeTypes.Changed, root.Path, "appsettings.json")); Assert.True(fileSystemWatcher.IncludeSubdirectories); - - physicalFilesWatcher.CreateFileChangeToken("other.json"); - Assert.True(fileSystemWatcher.IncludeSubdirectories); } [Fact] @@ -188,6 +180,29 @@ public void IncludeSubdirectories_AlwaysTrueWhenWatcherIsAboveRoot() Assert.True(fsw.IncludeSubdirectories); } + [Fact] + [SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.iOS | TestPlatforms.tvOS, "System.IO.FileSystem.Watcher is not supported on Browser/iOS/tvOS")] + public async Task IncludeSubdirectories_StarPatternIsNotRecursive_DoesNotMatchSubdirectoryFile() + { + using var root = new TempDirectory(GetTestFilePath()); + using var fileSystemWatcher = new MockFileSystemWatcher(root.Path); + using var physicalFilesWatcher = new PhysicalFilesWatcher(root.Path, fileSystemWatcher, pollForChanges: false); + + IChangeToken token = physicalFilesWatcher.CreateFileChangeToken("*.json"); + Assert.False(fileSystemWatcher.IncludeSubdirectories); + + Task changed = WhenChanged(token); + + // A '*' pattern should not match files in subdirectories. + fileSystemWatcher.CallOnChanged(new FileSystemEventArgs(WatcherChangeTypes.Changed, root.Path, "sub/foo.json")); + await Task.Delay(WaitTimeForTokenToFire); + Assert.False(changed.IsCompleted, "Token must not fire for an event in a subdirectory."); + + // Sanity check: a root-level event does fire the token. + fileSystemWatcher.CallOnChanged(new FileSystemEventArgs(WatcherChangeTypes.Changed, root.Path, "foo.json")); + await changed; + } + [Fact] [SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.iOS | TestPlatforms.tvOS, "System.IO.FileSystem.Watcher is not supported on Browser/iOS/tvOS")] public void CreateFileChangeToken_DoesNotAllowPathsAboveRoot() From 1c38dc78d63d71656653f943114429c2e0ca1588 Mon Sep 17 00:00:00 2001 From: Petr Onderka Date: Wed, 13 May 2026 16:25:13 +0200 Subject: [PATCH 3/3] Remove incorrect asserts --- .../src/PhysicalFilesWatcher.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs b/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs index 50d9dbfe773319..344369cf53c676 100644 --- a/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs +++ b/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs @@ -387,8 +387,7 @@ void CancelAll(ConcurrentDictionary tokens, Func= 0, "Subdirectory-requiring token counter went negative."); + Interlocked.Decrement(ref _subdirectoryRequiringTokenCount); } CancelToken(matchInfo); @@ -453,8 +452,7 @@ private void ReportChangeForMatchedEntries(string path) { if (FilePathRequiresSubdirectories(path)) { - int newCount = Interlocked.Decrement(ref _subdirectoryRequiringTokenCount); - Debug.Assert(newCount >= 0, "Subdirectory-requiring token counter went negative."); + Interlocked.Decrement(ref _subdirectoryRequiringTokenCount); } CancelToken(matchInfo); @@ -469,8 +467,7 @@ private void ReportChangeForMatchedEntries(string path) { if (WildcardRequiresSubdirectories(wildCardEntry.Key)) { - int newCount = Interlocked.Decrement(ref _subdirectoryRequiringTokenCount); - Debug.Assert(newCount >= 0, "Subdirectory-requiring token counter went negative."); + Interlocked.Decrement(ref _subdirectoryRequiringTokenCount); } CancelToken(matchInfo);