{"id":5730,"date":"2020-02-10T15:00:04","date_gmt":"2020-02-10T15:00:04","guid":{"rendered":"http:\/\/writeasync.net\/?p=5730"},"modified":"2020-02-10T04:30:00","modified_gmt":"2020-02-10T04:30:00","slug":"watching-a-directory-basic-intro","status":"publish","type":"post","link":"http:\/\/writeasync.net\/?p=5730","title":{"rendered":"Watching a directory: basic intro"},"content":{"rendered":"<p>Let&#8217;s say we want to write a .NET Core class that watches for file changes in a single directory. The implementation should only notify for explicitly subscribed files. For example, if <code>DIR<\/code> is being watched and <code>DIR\\file1.txt<\/code> is subscribed, we should not receive any events for <code>DIR\\file2.txt<\/code>, nor for <code>DIR\\INNER\\file1.txt<\/code>. Further, let&#8217;s presume that any time after subscribing, the caller could decide to unsubscribe. Finally, the entire watch procedure should be able to be stopped in one shot, so <code>IDisposable<\/code> support is a must.<\/p>\n<p>Clearly, we would be wise to start from <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/api\/system.io.filesystemwatcher?view=netcore-3.1\">FileSystemWatcher<\/a>. This gives us 90% of the solution already (basic file and folder change detection) and <a href=\"https:\/\/github.com\/dotnet\/corefx\/blob\/master\/src\/System.IO.FileSystem.Watcher\/src\/System\/IO\/FileSystemWatcher.Linux.cs\">cross-platform<\/a> <a href=\"https:\/\/github.com\/dotnet\/corefx\/blob\/master\/src\/System.IO.FileSystem.Watcher\/src\/System\/IO\/FileSystemWatcher.OSX.cs\">support<\/a> to boot! Given that FileSystemWatcher allows many complicated patterns with subdirectories, custom filters, different event types, and so on, we would rather <a href=\"https:\/\/stackoverflow.com\/questions\/5242429\/what-is-the-facade-design-pattern\">create a fa\u00e7ade<\/a> &#8212; we&#8217;ll call it <code>DirectoryWatcher<\/code>.<\/p>\n<p>Here is a <a href=\"https:\/\/github.com\/brian-dot-net\/writeasync\/blob\/c4a41973dcd400b164fee551357b0526296ffe11\/projects\/DirectoryWatcherSample\/DirectoryWatcherSample.Core\/DirectoryWatcher.cs\">basic implementation of DirectoryWatcher<\/a> that should support the requirements:<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\r\n    public sealed class DirectoryWatcher : IDisposable\r\n    {\r\n        private readonly string path;\r\n        private readonly ConcurrentDictionary&lt;string, Subscription&gt; subscriptions;\r\n        private readonly FileSystemWatcher watcher;\r\n\r\n        public DirectoryWatcher(DirectoryInfo path)\r\n        {\r\n            this.path = path.FullName;\r\n            this.subscriptions = new ConcurrentDictionary&lt;string, Subscription&gt;(StringComparer.OrdinalIgnoreCase);\r\n            this.watcher = new FileSystemWatcher(this.path);\r\n            this.watcher.NotifyFilter =\r\n                NotifyFilters.FileName |\r\n                NotifyFilters.LastWrite |\r\n                NotifyFilters.CreationTime;\r\n            this.watcher.Changed += (o, e) =&gt; this.OnUpdated(e.FullPath);\r\n            this.watcher.Created += (o, e) =&gt; this.OnUpdated(e.FullPath);\r\n            this.watcher.Renamed += (o, e) =&gt; this.OnUpdated(e.FullPath);\r\n            this.watcher.EnableRaisingEvents = true;\r\n        }\r\n\r\n        public void Dispose() =&gt; this.watcher.Dispose();\r\n\r\n        public IDisposable Subscribe(string file, Action&lt;FileInfo&gt; onUpdate)\r\n        {\r\n            FileInfo fullPath = new FileInfo(Path.Combine(this.path, file));\r\n            if (!string.Equals(fullPath.DirectoryName, this.path, StringComparison.OrdinalIgnoreCase))\r\n            {\r\n                throw new ArgumentException(\r\n                    &quot;The file '{file}' is not directly within directory '{path}'.&quot;,\r\n                    nameof(file));\r\n            }\r\n\r\n            string key = fullPath.FullName;\r\n            Subscription subscription = this.subscriptions.GetOrAdd(\r\n                key,\r\n                k =&gt; new Subscription(fullPath, onUpdate, () =&gt; this.subscriptions.TryRemove(k, out _)));\r\n            if (!object.ReferenceEquals(subscription.FullPath, fullPath))\r\n            {\r\n                throw new InvalidOperationException($&quot;A subscription for '{key}' already exists.&quot;);\r\n            }\r\n\r\n            return subscription;\r\n        }\r\n\r\n        private void OnUpdated(string fullPath)\r\n        {\r\n            if (this.subscriptions.TryGetValue(fullPath, out Subscription subscription))\r\n            {\r\n                subscription.Invoke();\r\n            }\r\n        }\r\n\r\n        private sealed class Subscription : IDisposable\r\n        {\r\n            private readonly Action&lt;FileInfo&gt; callback;\r\n            private readonly Action onDispose;\r\n\r\n            public Subscription(FileInfo fullPath, Action&lt;FileInfo&gt; callback, Action onDispose)\r\n            {\r\n                this.FullPath = fullPath;\r\n                this.callback = callback;\r\n                this.onDispose = onDispose;\r\n            }\r\n\r\n            public FileInfo FullPath { get; }\r\n\r\n            public void Invoke() =&gt; this.callback(this.FullPath);\r\n\r\n            public void Dispose() =&gt; this.onDispose();\r\n        }\r\n    }\r\n<\/pre>\n<p>To show it in action, we can write a <a href=\"https:\/\/github.com\/brian-dot-net\/writeasync\/blob\/c4a41973dcd400b164fee551357b0526296ffe11\/projects\/DirectoryWatcherSample\/DirectoryWatcherSample.App\/Program.cs\">sample app<\/a>; here is the relevant calling code for using the <code>DirectoryWatcher<\/code>:<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\r\nprivate static void RunWatcher(string&#x5B;] files)\r\n{\r\n    using DirectoryWatcher watcher = new DirectoryWatcher(new DirectoryInfo(&quot;.&quot;));\r\n\r\n    Action&lt;FileInfo&gt; onUpdated = f =&gt; Log($&quot;Got an update for '{f.Name}'&quot;);\r\n\r\n    IDisposable lastFile = null;\r\n    foreach (string file in files)\r\n    {\r\n        Console.WriteLine($&quot;Subscribing to '{file}'&quot;);\r\n        lastFile = watcher.Subscribe(file, onUpdated);\r\n    }\r\n\r\n    Console.WriteLine(&quot;Press ENTER unsubscribe last file.&quot;);\r\n    Console.ReadLine();\r\n\r\n    using (lastFile)\r\n    {\r\n    }\r\n\r\n    Console.WriteLine(&quot;Press ENTER to dispose.&quot;);\r\n    Console.ReadLine();\r\n}\r\n<\/pre>\n<p>Running the sample app gives us plausibly correct output:<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\nSubscribing to 'file1.txt'\r\nSubscribing to 'file2.txt'\r\nPress ENTER unsubscribe last file.\r\n&#x5B;000.000\/T5] ** delete 'file2.txt'\r\n&#x5B;000.577\/T5] ** append 'file2.txt'\r\n&#x5B;000.579\/T4] Got an update for 'file2.txt'\r\n&#x5B;000.584\/T4] Got an update for 'file2.txt'\r\n&#x5B;001.171\/T5] ** delete 'file2.txt'\r\n&#x5B;001.744\/T5] ** delete 'file1.txt'\r\n&#x5B;001.824\/T5] ** delete 'file2.txt'\r\n&#x5B;002.678\/T5] ** move to 'file1.txt'\r\n&#x5B;002.686\/T4] Got an update for 'file1.txt'\r\n&#x5B;002.687\/T6] Got an update for 'file1.txt'\r\n&#x5B;002.711\/T5] ** move to 'file2.txt'\r\n&#x5B;002.717\/T6] Got an update for 'file2.txt'\r\n&#x5B;002.717\/T4] Got an update for 'file2.txt'\r\n&#x5B;002.868\/T5] ** overwrite 'file2.txt'\r\n&#x5B;002.868\/T4] Got an update for 'file2.txt'\r\n&#x5B;002.873\/T4] Got an update for 'file2.txt'\r\n&#x5B;003.806\/T5] ** overwrite 'file2.txt'\r\n&#x5B;003.807\/T4] Got an update for 'file2.txt'\r\n&#x5B;003.812\/T4] Got an update for 'file2.txt'\r\n\r\nPress ENTER to dispose.\r\n&#x5B;004.053\/T5] ** append 'file1.txt'\r\n&#x5B;004.058\/T4] Got an update for 'file1.txt'\r\n&#x5B;004.165\/T5] ** append 'file1.txt'\r\n&#x5B;004.170\/T4] Got an update for 'file1.txt'\r\n&#x5B;004.975\/T5] ** overwrite 'file2.txt'\r\n&#x5B;005.288\/T5] ** overwrite 'file2.txt'\r\n&#x5B;005.899\/T5] ** move to 'file2.txt'\r\n&#x5B;006.557\/T7] ** append 'file2.txt'\r\n&#x5B;006.974\/T5] ** move to 'file2.txt'\r\n&#x5B;007.795\/T5] ** move to 'file2.txt'\r\n&#x5B;008.741\/T7] ** delete 'file2.txt'\r\n&#x5B;008.927\/T5] ** overwrite 'file1.txt'\r\n&#x5B;008.928\/T4] Got an update for 'file1.txt'\r\n&#x5B;008.932\/T4] Got an update for 'file1.txt'\r\n\r\nPress ENTER to quit.\r\n&#x5B;009.461\/T5] ** overwrite 'file2.txt'\r\n&#x5B;010.100\/T5] ** append 'file1.txt'\r\n&#x5B;010.293\/T7] ** append 'file1.txt'\r\n&#x5B;010.438\/T7] ** overwrite 'file1.txt'\r\n\r\n\r\nDirectoryWatcherSample\\DirectoryWatcherSample.App\\bin\\Debug\\netcoreapp3.1\\DirectoryWatcherSample.App.exe (process 13048) exited with code 0.\r\nPress any key to close this window . . .\r\n<\/pre>\n<p>There are just a couple issues here. The first is evident in the program output, where we get more than one event for certain types of updates on a single file. Of course, this is well documented over the years by <a href=\"https:\/\/stackoverflow.com\/questions\/1764809\/filesystemwatcher-changed-event-is-raised-twice\">vexed users<\/a> and <a href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20140507-00\/?p=1053\">insiders alike<\/a>. The other issue is more of a self-inflicted design problem: the code is not easily testable since it has direct file system dependencies, and there is a lot of logic in there which has no platform dependencies that we want to be sure we got right.<\/p>\n<p>Stay tuned for a few more posts which delve deeper into the design choices and address some of the issues raised. Until next time&#8230;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Let&#8217;s say we want to write a .NET Core class that watches for file changes in a single directory. The implementation should only notify for explicitly subscribed files. For example, if DIR is being watched and DIR\\file1.txt is subscribed, we&hellip; <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[61,91],"tags":[],"class_list":["post-5730","post","type-post","status-publish","format-standard","hentry","category-concurrency","category-design"],"_links":{"self":[{"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts\/5730","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=5730"}],"version-history":[{"count":1,"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts\/5730\/revisions"}],"predecessor-version":[{"id":5731,"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts\/5730\/revisions\/5731"}],"wp:attachment":[{"href":"http:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=5730"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=5730"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=5730"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}