はじめに 「ファイル監視」ができると“自動処理”の世界が一気に広がる
業務システムを書いていると、こんな要望がよく出てきます。
フォルダにファイルが置かれたら、自動で取り込んで処理したい。
設定ファイルが書き換えられたら、アプリを再起動せずに反映したい。
ログフォルダに異常なファイルが増えたら、すぐに検知したい。
こういう「ファイルやフォルダの変化をトリガーにして何かしたい」ときに使えるのが、FileSystemWatcher です。
これは .NET が標準で提供している「ファイルシステムの変化を監視するクラス」で、
指定したフォルダに対して「作成」「変更」「削除」「リネーム」などのイベントを受け取ることができます。
ここでは、プログラミング初心者向けに、FileSystemWatcher の基本から、
実務でよくある「監視→自動処理」のパターン、そしてハマりどころまで、かみ砕いて解説していきます。
FileSystemWatcher の基本を理解する
何をしてくれるクラスなのか
FileSystemWatcher は、ざっくり言うと「OS にフォルダを見張ってもらって、変化があったらイベントで教えてもらう」ためのクラスです。
自分で定期的にフォルダをスキャンして差分を取る必要はなく、
OS 側の通知を受け取る形になるので、効率が良く、リアルタイム性も高いです。
監視できる主なイベントは次のようなものです。
Created(ファイル・ディレクトリが作成された)
Changed(内容や属性が変更された)
Deleted(削除された)
Renamed(名前が変更された)
さらに、「どのフォルダを」「どの拡張子を」「サブディレクトリも含めるか」などを細かく指定できます。
最小構成のサンプルコードを先に見る
まずは、動くイメージをつかむために、最小限のサンプルを見てみます。
using System;
using System.IO;
class Program
{
static void Main()
{
string path = @"C:\watch";
using var watcher = new FileSystemWatcher(path);
watcher.IncludeSubdirectories = false;
watcher.Filter = "*.*";
watcher.NotifyFilter =
NotifyFilters.FileName |
NotifyFilters.DirectoryName |
NotifyFilters.LastWrite;
watcher.Created += OnCreated;
watcher.Changed += OnChanged;
watcher.Deleted += OnDeleted;
watcher.Renamed += OnRenamed;
watcher.EnableRaisingEvents = true;
Console.WriteLine("監視中です。終了するには Enter を押してください。");
Console.ReadLine();
}
private static void OnCreated(object sender, FileSystemEventArgs e)
{
Console.WriteLine($"Created: {e.FullPath}");
}
private static void OnChanged(object sender, FileSystemEventArgs e)
{
Console.WriteLine($"Changed: {e.FullPath}");
}
private static void OnDeleted(object sender, FileSystemEventArgs e)
{
Console.WriteLine($"Deleted: {e.FullPath}");
}
private static void OnRenamed(object sender, RenamedEventArgs e)
{
Console.WriteLine($"Renamed: {e.OldFullPath} -> {e.FullPath}");
}
}
C#このプログラムを動かして C:\watch フォルダでファイルを作ったり消したりすると、
コンソールにイベントが表示されるはずです。
ここから、重要なポイントを一つずつ深掘りしていきます。
監視対象の指定とフィルタリング
Path と Filter と IncludeSubdirectories
FileSystemWatcher のコンストラクタには、監視対象のパスを渡します。
using var watcher = new FileSystemWatcher(path);
C#ここで指定するのは「フォルダ」です。
個別のファイルではなく、「このフォルダの中で起きる変化」を監視します。
Filter プロパティで、監視するファイル名のパターンを指定できます。
watcher.Filter = "*.csv";
C#こうすると、.csv ファイルだけが対象になります。"*.*" にしておけば、すべてのファイルが対象です。
IncludeSubdirectories を true にすると、サブフォルダも含めて再帰的に監視します。
watcher.IncludeSubdirectories = true;
C#業務でよくあるのは、「特定のフォルダ直下だけを監視する」か「サブフォルダも含めて全部監視する」かの二択です。
どちらにするかは、運用イメージから逆算して決めます。
NotifyFilter で「何の変化を監視するか」を絞る
NotifyFilter は、「どの種類の変化を検知するか」をビットフラグで指定するプロパティです。
watcher.NotifyFilter =
NotifyFilters.FileName |
NotifyFilters.DirectoryName |
NotifyFilters.LastWrite;
C#ここでは、「ファイル名の変更」「ディレクトリ名の変更」「最終更新日時の変更」を監視しています。
あまり細かく考えずに、最初はこの組み合わせで十分です。
Changed イベントは、LastWrite(最終更新)や Size(サイズ)などが変わったときに発生します。
「内容が変わったかどうか」を知りたい場合は、LastWrite を含めておくのが基本です。
イベントハンドラの書き方と注意点
FileSystemEventArgs と RenamedEventArgs の違い
Created、Changed、Deleted は、どれも FileSystemEventHandler という同じ型のイベントで、
ハンドラの引数は FileSystemEventArgs です。
private static void OnCreated(object sender, FileSystemEventArgs e)
{
Console.WriteLine($"Created: {e.FullPath}");
}
C#FileSystemEventArgs からは、主に次の情報が取れます。
FullPath(フルパス)Name(ファイル名のみ)ChangeType(Created / Changed / Deleted など)
一方、Renamed イベントだけは、RenamedEventHandler という別の型で、
引数は RenamedEventArgs になります。
private static void OnRenamed(object sender, RenamedEventArgs e)
{
Console.WriteLine($"Renamed: {e.OldFullPath} -> {e.FullPath}");
}
C#RenamedEventArgs では、OldFullPath や OldName が取れるのがポイントです。
「どのファイルがどの名前に変わったか」をログに残したいときに便利です。
イベントは「複数回」飛んでくることがある
初心者が最初にハマりやすいのが、「Changed イベントが何度も飛んでくる」問題です。
ファイルを保存するとき、アプリケーションによっては「一時ファイルを作ってから入れ替える」「複数回書き込む」といった動きをするため、
Changed が連続して発生することがあります。
つまり、「Changed が 1 回来たら 1 回だけ処理される」とは限りません。
実務で使うときは、次のような工夫をよくします。
一定時間内に同じファイルに対するイベントが連続したら、まとめて 1 回だけ処理する。
Created と Changed の両方が飛んでくる前提で、「どちらが来ても同じ処理をする」。
このあたりは少し高度な話になるので、まずは「イベントは必ずしも 1 回とは限らない」という感覚だけ持っておくと十分です。
典型パターン① 受信フォルダに置かれたファイルを自動処理する
イメージ:受信ボックスフォルダを監視して、CSV が来たら取り込む
業務でよくあるのが、「あるフォルダに CSV を置いてもらったら、自動で取り込んで DB に登録する」といったパターンです。
これを FileSystemWatcher で実現してみます。
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
public sealed class ImportWatcher : IDisposable
{
private readonly FileSystemWatcher _watcher;
public ImportWatcher(string watchDirectory)
{
_watcher = new FileSystemWatcher(watchDirectory)
{
Filter = "*.csv",
IncludeSubdirectories = false,
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite
};
_watcher.Created += OnCreated;
_watcher.Changed += OnChanged;
_watcher.EnableRaisingEvents = true;
}
private void OnCreated(object sender, FileSystemEventArgs e)
{
HandleFileAsync(e.FullPath);
}
private void OnChanged(object sender, FileSystemEventArgs e)
{
HandleFileAsync(e.FullPath);
}
private void HandleFileAsync(string path)
{
Task.Run(async () =>
{
await WaitFileReadyAsync(path);
Console.WriteLine($"取り込み開始: {path}");
try
{
string text = File.ReadAllText(path, Encoding.UTF8);
Console.WriteLine($"ファイル長: {text.Length}");
}
catch (Exception ex)
{
Console.WriteLine($"取り込み失敗: {path} {ex.Message}");
}
});
}
private static async Task WaitFileReadyAsync(string path)
{
for (int i = 0; i < 10; i++)
{
try
{
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None);
return;
}
catch (IOException)
{
}
await Task.Delay(500);
}
}
public void Dispose()
{
_watcher.Dispose();
}
}
C#このクラスを使う側は、例えばこうです。
class Program
{
static void Main()
{
using var watcher = new ImportWatcher(@"C:\import");
Console.WriteLine("監視中です。Enter で終了します。");
Console.ReadLine();
}
}
C#ここでの重要ポイントを整理していきます。
重要ポイント① 書き込み中のファイルをすぐに開こうとしない
ファイルが「作成された直後」や「書き込み途中」のタイミングで処理を始めると、
まだ書き込みが終わっていなくて IOException が出たり、中途半端な内容を読んでしまったりします。
そこで WaitFileReadyAsync のようなメソッドを用意して、
「一定時間、ファイルが単独で開けるようになるまで待つ」という工夫をしています。
FileShare.None で開けるということは、「他のプロセスがそのファイルを掴んでいない」状態です。
これを目安に、「もう書き込みは終わった」と判断しています。
重要ポイント② イベントハンドラの中で重い処理をしない
HandleFileAsync の中で Task.Run を使っているのは、
イベントハンドラの中で重い処理(ファイル読み込みや DB 書き込みなど)を直接やらないためです。
イベントハンドラは、FileSystemWatcher の内部スレッドから呼ばれます。
ここで時間のかかる処理をしてしまうと、他のイベント処理が詰まったり、アプリ全体のレスポンスに影響したりします。
実務では、「イベントハンドラでは最低限の情報をキューに積むだけにして、実際の処理は別スレッドで行う」という設計がよく使われます。
ここでは簡易的に Task.Run で別タスクに逃がしています。
典型パターン② 設定ファイルの変更を検知して再読み込みする
イメージ:config.json が書き換えられたら設定を再読み込み
もう一つよくあるのが、「アプリを再起動せずに設定を変えたい」という要望です。
設定ファイル(例えば config.json)を監視して、変更されたら再読み込みする、というパターンです。
using System;
using System.IO;
using System.Text.Json;
public sealed class ConfigWatcher<TConfig> : IDisposable where TConfig : class
{
private readonly string _configPath;
private readonly FileSystemWatcher _watcher;
public TConfig Current { get; private set; }
public ConfigWatcher(string configPath)
{
_configPath = configPath;
Current = LoadConfig();
string? dir = Path.GetDirectoryName(configPath)
?? throw new ArgumentException("パスが不正です。", nameof(configPath));
string fileName = Path.GetFileName(configPath);
_watcher = new FileSystemWatcher(dir)
{
Filter = fileName,
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size
};
_watcher.Changed += OnChanged;
_watcher.EnableRaisingEvents = true;
}
private void OnChanged(object sender, FileSystemEventArgs e)
{
try
{
System.Threading.Thread.Sleep(200);
Current = LoadConfig();
Console.WriteLine("設定を再読み込みしました。");
}
catch (Exception ex)
{
Console.WriteLine($"設定の再読み込みに失敗しました: {ex.Message}");
}
}
private TConfig LoadConfig()
{
string json = File.ReadAllText(_configPath);
return JsonSerializer.Deserialize<TConfig>(json)
?? throw new InvalidOperationException("設定ファイルの読み込みに失敗しました。");
}
public void Dispose()
{
_watcher.Dispose();
}
}
C#使い方のイメージです。
public sealed class AppConfig
{
public string ConnectionString { get; set; } = "";
public int MaxRetry { get; set; }
}
class Program
{
static void Main()
{
using var watcher = new ConfigWatcher<AppConfig>(@"C:\app\config.json");
Console.WriteLine("現在の設定:");
Console.WriteLine(watcher.Current.ConnectionString);
Console.WriteLine("config.json を編集して保存すると、再読み込みされます。");
Console.ReadLine();
}
}
C#ここでのポイントは、「特定のファイル名だけを Filter で監視している」ことと、
「Changed イベントで再読み込みしている」ことです。
FileSystemWatcher のハマりどころと注意点
バッファオーバーフロー(イベント取りこぼし)の可能性
FileSystemWatcher は内部にイベント用のバッファを持っていて、
短時間に大量のファイル操作が行われると、このバッファがあふれてイベントを取りこぼすことがあります。
例えば、「数万ファイルを一気にコピーする」ような操作を監視していると、
全部の Created イベントを受け取れないことがあります。
この問題に対しては、次のような考え方が必要です。
「イベントは“ヒント”であって、絶対ではない」と割り切る。
重要な処理では、定期的にフォルダ全体をスキャンして整合性を取る。
初心者の段階では、「FileSystemWatcher は完璧ではない」という認識だけ持っておけば十分です。
プロセス終了時に Dispose を忘れない
FileSystemWatcher は OS のリソースを使うので、
使い終わったら Dispose するのが基本です。
using を使うか、クラスに持たせる場合は IDisposable を実装して、
アプリ終了時に確実に解放するようにしておきましょう。
UI スレッドとの関係(WinForms / WPF の場合)
ここまでの例はコンソールアプリ前提でしたが、
WinForms や WPF で使う場合、イベントハンドラは UI スレッド以外から呼ばれます。
そのため、イベントハンドラの中で直接 UI を触ると、「別スレッドから UI を触った」例外が出ます。
この場合は、Invoke や Dispatcher.Invoke を使って、UI スレッドに処理を投げる必要があります。
これは「スレッドと UI」の話になるので、
FileSystemWatcher に限らず、非同期処理全般で意識しておくべきポイントです。
まとめ 「ファイル監視ユーティリティ」で業務フローを自動化する
FileSystemWatcher を使いこなせるようになると、「人がフォルダを見に行く」世界から「ファイルが来たら勝手に動く」世界に一歩進めます。
業務フローの自動化や、外部システムとのゆるい連携にとても相性が良い機能です。
押さえておきたいポイントを整理すると、こうなります。
監視対象のフォルダ、フィルタ(拡張子)、サブディレクトリの有無をきちんと設計する。
Created / Changed / Deleted / Renamed のイベントを使い分けるが、「イベントは複数回飛ぶこともある」と理解しておく。
書き込み中のファイルをすぐに処理しようとせず、「開けるようになるまで待つ」工夫を入れる。
イベントハンドラの中で重い処理をせず、別スレッドやキューに逃がす設計を意識する。
FileSystemWatcher は完璧ではないので、「取りこぼしの可能性がある」前提で重要な処理を設計する。
ここまで理解できれば、
「受信フォルダ監視」「設定ファイルのホットリロード」「ログフォルダの異常検知」など、
実務でよくある“ファイルをトリガーにした自動処理”を、自分の手で組み立てられるようになります。
