はじめに
パート1では、.NET Service Managerとはいったい何をするものか、そしてそれの下で動作する被管理サービスのインストールと設定はどうすればよいか、という点を説明しました。パート2では、.NET Service Managerの働きの仕組みを詳細に見ていきます。つまり、ドラッグアンドドロップ配備などの便利な働きがなぜ可能なのか、ということです。こうした機能を背後で支えている諸概念には、他の.NETプロジェクトにも応用できるものが少なくありません。
.NET Service Managerは、いくつかの中核的技術の上に成り立っています。たとえば、.NET Remoting、AppDomain、Reflection、シャドウコピー、そしてクラスやインタフェースといった標準的なオブジェクト指向技術です。そのうち、アセンブリの動的ロードとアンロードでの中心となる技術がRemotingです。Remotingを利用すると、コードのロード、ホスト、実行を隔離されたメモリ領域(アプリケーションドメイン、もしくはAppDomainと呼び出されます)で行い、外見上は標準のWindowsプロセスとなんら変わりなく動作させることができます。
AppDomainとRemotingの関係
AppDomainは、小型のプロセスのようなもので、信頼性と安全性の確保に必要な分離性を実現していますが、プロセッサやメモリのオーバーヘッドは完全なWin32プロセスよりも小さく抑えられています。1つの実プロセスの内部でいくつものAppDomainが実行されます。同じプロセス内にあるAppDomain間の通信は、当然、プロセス間通信より高速であると期待できますが、やはりRemoting境界を越えた往来が必要となるため、パフォーマンスには多少の影響が出ます。
リモート通信に必要なオーバーヘッドを理解するために、1つの比喩を考えましょう。いま、2人の人間が同じオフィスビル内の、異なる2つの部門にいるとします。オフィスビルがプロセス、部門がAppDomainに相当します。2人は社内便を使って、メッセージや荷物をやり取りします。そこで必要とされる時間や費用は、さほど大きなものではありません。
次に、同じ2人が国内の別地域に住んでいるとしましょう。この場合は、同じメッセージや荷物を送るにもかなりの郵便代がかかりますし、それを郵便局に持っていくための時間もかかります。時間や費用がかかることが、すなわちオーバーヘッドの増大です。Win32プロセス間の通信でも、同じことが言えます。
最後に、1人がアメリカ、もう1人がイギリスに住んでいる場合を考えましょう。メッセージや荷物を送るのにかかる時間は大幅に長くなります。また、それほどではないにせよ、費用も増大します。この状況は、2台のマシンをつなぐネットワーク経由の通信に似ています。
さて、この2人が同じ室内にいたらどうでしょう。通信はずっと速く、ずっと安上がりになります。とにかく、面と向かって話し合い、必要なものを手渡せばいいのですから、封筒も包装もいりません。AppDomain内通信もこれと同じことです。2つのオブジェクト間における最も効率の良い通信方法が、これであることは明らかでしょう。
ここまでの比喩は、主としてパフォーマンスだけを考えたものでした。今回の記事で実装する.NET Service ManagerではRemotingの別の機能――他のAppDomain内にオブジェクトのインスタンスを作り、それにStart(開始)やStop(停止)などのキーメッセージを送信するという働き――が重要な意味を持ちます。この機能には、MarshalByRef
クラスを継承するオブジェクトを使用します。この継承によって、あるAppDomain内にオブジェクトを作成し、それを別のAppDomainから制御することが可能になります。この方法では、参照(アドレス)だけをパッケージ(プロキシ)の形でAppDomain境界の向こうへ送信します。MarshalByRef(参照によるマーシャリング)という名前は、そこから来ています。マーシャリングという言葉でわかりにくければ、「エスコート」と言い替えてもよいでしょう。
Remoting境界を越えてオブジェクトをマーシャリングする場合、既定では値単位でマーシャリングが実施されます。つまり、データ(値)の完全コピーがシリアル化され、送信されます。ただ、これをRemotingで正しく(つまり、適切に)行おうとすれば、実際にはクラスにSerializableAttribute
を追加しなければなりません。上で「既定では」と言いましたが、「リモートデータを扱うには、ほとんどの場合、そうすることが望ましい」と言い替えたほうがいいかもしれません(理由はありますが、ここでは取り上げません)。ただ、今回のケースではMarshalByRef
クラスを使用する必要がありますし、扱うのはプロセス内におけるAppDomain間通信ですから、MarshalByRef
の使用による負の影響はそれほど大きくありません。
とにかく、Remotingを使って、ある型のインスタンスを別のAppDomain内に作成します。コードでは、この型をRemoteServiceHandler
と呼んでいます。なぜこの方法を使うかと言うと、型情報がAppDomainにロードされることを避けなければならないからです。これを許すと、アセンブリ全体がメインのAppDomainにロードされ、動的なアンロードができなくなります。今回のアプリケーションにおける我々の目標の1つは、被管理サービス(アセンブリ)の動的なロード/アンロードによって「ホット更新」ができるようにしようということです。そのためには、私の知る限り、個々の一意な被管理サービスアセンブリごとにAppDomainを作成し、その被管理サービスのIService
実装をRemotingによって作成・制御する以外に方法がありません(IService
や被管理サービスの意味がよくわからないという方は、パート1を参照してください)。
各アセンブリをそれぞれ別個のAppDomainにロードしておくと、それをAppDomain.Unload
メソッドで完全にメモリからアンロードできますし、必要なら更新済みの新しいコピーのロードも、.NET Service Managerのメインプロセスを再始動せずに実行できます。ここでも理屈は同じで、やはり個別のAppDomainを使い、個々の被管理サービスのアセンブリ情報を他から分離することで、これが可能になります。型をメインのAppDomainへ直接ロードしたのでは、そのメインAppDomainを再始動しない限り(したがって、プロセス全体を再始動しない限り)アンロードと更新ができません。既に述べたとおり、アセンブリの型情報をロードしてしまうと、アセンブリ全体がそのAppDomainにロードされます。図1に、この分離化の様子を示します。
それぞれの概念の実装
関係する概念と技術を高みから概観したところで、今度は具体的な実装を詳しく見ていくことにしましょう。このアプリケーションのエンジンはServiceBrokerアセンブリの中にあります。パート1で述べたとおり、ここには必要なServiceEntryPoint
属性とIService
インタフェースの定義が含まれています。また、被管理サービスのロード、開始、停止、アンロードを行うコードも、子AppDomain中の型とのリモート対話に使われるRemoteServiceHandler
型もここに含まれています。
エンジンの中核はServiceBroker
クラスです。サービス状態そのものや監視対象のディレクトリに変化があったときは、このクラスが「.NET Service Manager」Windowsサービスによって呼び出されます。ServiceBroker
クラスの冒頭部分(リスト1)には、HybridDictionary
の静的/共用インスタンスの宣言がいくつかあります。これは、データと被管理サービス参照のキャッシュに用いられるインスタンスです。HybridDictionary
型を選択したのは、一般には小さくて、ときに大きくもなりうるコレクションを扱う際に最もすぐれたパフォーマンスを示す型だからです。
private HybridDictionary serviceNames = new HybridDictionary(10); private HybridDictionary serviceAppDomains = new HybridDictionary(10); private HybridDictionary services = new HybridDictionary(10); private HybridDictionary serviceLastModified = new HybridDictionary(10);
serviceNames
ディクショナリとserviceLastModified
ディクショナリの役割は、ロードされているサービスについてのデータをキャッシュすることです。serviceNames
は、オリジナルアセンブリの所在へのパスをキーとし、その被管理サービスのServiceEntryPointAttribute.ServiceName
値を値として使用します。serviceLastModified
は、ServiceName
をキーとし、被管理サービスのアセンブリの最終更新日時(DateTime
)を値として使用します。他の2つのディクショナリには、重要オブジェクトへの参照が格納されます。まず、serviceAppDomains
には、動的に作成されたAppDomainへの参照がServiceNameをキーとして格納されます。そしてservices
には、被管理サービスのリモート制御(それぞれのAppDomainにある被管理サービスの制御)に使われるRemoteServiceHandler
への参照が格納されます。
サービスのロードと開始
次のリスト2に示すのは、アプリケーション機能の中核を成すServiceBroker.StartService
メソッドの一部です。パラメータはfilePath
の1つだけで、これに基づいて目的のDLLを探します。まず、パスからファイル名を取り出して、.NET Service Managerのディレクトリに常に存在する2つのファイルと比較します。これは、これらのファイルを不必要に処理しないための用心です。
public void StartService(string filePath) { string serviceName; string fileName = filePath.Substring( filePath.LastIndexOf("\\") + 1); if (fileName.IndexOf("Microsoft.ApplicationBlocks") != -1 || fileName == "ServiceBroker.dll") return; try { AssemblyName asmName = null; try { asmName = AssemblyName.GetAssemblyName(filePath); } catch (Exception ex) { Logger.WriteToLog( String.Format("Could not get assembly name " +"from '{0}'; bypassing that file.",filePath) + " Exception Details: " + ex.ToString() ,System.Diagnostics.EventLogEntryType.Warning); return; } ...
次にロード手順を開始します。まず、ReflectionのAssemblyName.GetAssemblyName
メソッドによって2つのことを行います。1つは、これが.NETアセンブリなのか、それ以外のDLLなのかを確認することです。このメソッドから例外がスローされたら、おそらく.NETアセンブリではありません。AssemblyName
情報の一部は、後でまた必要になります。AssemblyName
をロードしても、AssemblyName
のプロパティに値を埋めるのに必要なメタデータが読み込まれるだけで、アセンブリ自体はメモリにロードされないことに注意してください。
このメソッドの次のコードブロックをリスト3に示します。この部分の目的は、問題のアセンブリの最新バージョンがロード済みでないかどうかを確かめることです。ロード済みなら、これ以後の実行の一部を省略できます。確かめるには、まず、serviceNames
コレクションにサービス名が含まれていないかどうかを調べます。含まれていれば、現在ディレクトリにあるファイルからサービス名と最終変更時刻を取り出し、それをキャッシュされている最終変更時刻と比較します。これで、新しいバージョンがディレクトリにドロップされたかどうかがわかります。
// check if assembly service already loaded if (this.serviceNames.Contains(filePath)) { serviceName = this.serviceNames[filePath].ToString(); DateTime curTime = File.GetLastWriteTime(filePath); // only add the service if it is not already running latest copy if (curTime.Ticks <= ((DateTime)this.serviceLastModified[serviceName]).Ticks) { Logger.WriteToLog( String.Format("Skipping '{0}' because it is already loaded.", serviceName), System.Diagnostics.EventLogEntryType.Information); return; } else// new copy, so stop current and clear out cache { // stop/unload service this.UnloadService(serviceName); // remove from file path cache this.serviceNames.Remove(filePath); } }
現在ディレクトリ中にあるバージョンがいまロードされているバージョンと同じか、それより古い場合は、再ロードは不要です。ロードを省略したことをログに記入して、関数を抜けます。それ以外の場合は、新しいバージョンがディレクトリにドロップされているので、現在のバージョンをアンロードし、キャッシュから取り除かなければなりません。
UnloadService
メソッド(リスト4)は、被管理サービスを停止してアンロードし、それへの参照を静的キャッシュから取り除きます。このメソッドは、どこでも必要な場所で使用できます。
private void UnloadService(string serviceName) { RemoteServiceHandler service = this.services[serviceName] as RemoteServiceHandler; if (service != null) { try { service.StopService(); } catch (Exception ex) { Logger.LogException(ex); } } this.services.Remove(serviceName); AppDomain svcDomain = this.serviceAppDomains[serviceName] as AppDomain; if (svcDomain != null) { try { AppDomain.Unload(svcDomain); } catch (Exception ex) { Logger.LogException(ex); } } this.serviceAppDomains.Remove(serviceName); this.serviceLastModified.Remove(serviceName); }
UnloadService
メソッドでは、まずサービス名に基づいて被管理サービスのRemoteServiceHandler
への参照を取得し、その参照からIService.StopService
実装を呼んでシャットダウンを試みます(RemoteServiceHandler.StopService
は、現在処理中のどのサービスに対してもIService.StopService
実装を呼び出します)。RemoteServiceHandler
については後で詳しく説明します。このメソッドは、次に被管理サービスのAppDomainへの参照を取得し、AppDomain.Unloadでのアンロードを試みたのち、最後に各キャッシュからその他の参照を取り除きます。ただし、serviceNames
キャッシュだけは、リスト3の最終行で見たとおり、呼び出しを行ったコードの中でクリアしなければなりません。
被管理サービスの新バージョンをロードする必要があるかどうか(既にロードされているときは、アンロードが必要かどうか)を調べた後で、AppDomainの作成と被管理サービスのリモートロードに進みます。
AppDomain svcDomain = null; try { AppDomainSetup setup = new AppDomainSetup(); setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory; setup.PrivateBinPath = setup.ApplicationBase; setup.ApplicationName = asmName.FullName; setup.ShadowCopyDirectories = setup.ApplicationBase; setup.ShadowCopyFiles = "true"; svcDomain = AppDomain.CreateDomain(asmName.FullName, null, setup); } catch (Exception ex) { Logger.LogException( new ApplicationException( String.Format("Could not create an AppDomain for '{0}'; " + "bypassing that assembly.", asmName.FullName), ex)); return; }
これは、被管理サービスの実行に使う新しいAppDomainをセットアップするコードです。AppDomainSetup
オブジェクトを使用しているのは、AppDomain.CreateDomain
のオーバーロードにはないオプションがあるためです。ここでは、ディレクトリプロパティを、現アプリケーションディレクトリを指し示すように設定することと、ファイルのシャドウコピーを作成するよう指示することが重要です。シャドウコピーを作成することによって、アセンブリがアプリケーションディレクトリでロックされることを防止できます。ここで例外がスローされることはまずないと思われますが、それでも、何が起こったかをログに記録するためと、スレッドを例外で終わらせないために、例外を正確にキャッチするようにしました。アプリケーションにユーザインタフェースがないときは、こうやって例外をキャッチし、ログに記録することをお勧めします。
さてこれで、被管理サービスを実行するための専用のAppDomainが作成できました。では、新しいAppDomainに被管理サービスをロードしましょう。型データをメインのAppDomainにロードせずにこれを行うには、上で触れたRemoteServiceHandler
を使用します。この型が何をするかと言えば、既知の型(被管理サービスアセンブリでなく、ServiceBrokerアセンブリで宣言されたもの)を返すことです。その型を使って子AppDomain内にインスタンスを作成できます。これはMarshalByRef
を継承しているので、インスタンスは実際に子ドメインの中に置かれており、我々はそれに向けてメッセージを送信することになります。
RemoteServiceHandler svc = null; try { svc = (RemoteServiceHandler) svcDomain.CreateInstanceFromAndUnwrap( svcDomain.BaseDirectory + "\\ServiceBroker.dll", "ServiceBroker.RemoteServiceHandler"); } catch (Exception ex) { AppDomain.Unload(svcDomain); Logger.LogException( new AssemblyLoadException( "Could not load ServiceBroker remote service handler," + " bypassing that file.", asmName.FullName, ex)); return; }
リスト6のコードでは、被管理サービスのドメインに置かれるRemoteServiceHandler
インスタンスを作成しています。具体的には、そのAppDomainインスタンスに対してCreateInstanceFromAndUnwrap
メソッドを呼び出します。このメソッドは、いわば便利なヘルパであり、実質的には、AppDomain.CreateInstanceFrom
とObjectHandle.UnWrap
という2つのメソッドを併せた働きをします。CreateInstanceFrom
メソッドからObjectHandle
が返されるので、そのObjectHandle
に対してUnWrap
メソッドを呼び出し、被管理サービスドメイン内にあるRemoteServiceHandler
への透過プロキシを作るという手順です。
前出の比喩を使って表現すれば、この時点で、相手が近くのオフィスにいることを知っていて、その人宛てのオフィス間郵便のアドレスも知っている状態になります。これまでは、情報のやり取りに関わる人全員が同じ部屋の中にいました。ここで初めて、離れた場所にいる人を相手にできるようになります。
ここの例外ハンドラでは、まずAppDomain.Unload
を呼び出していることに注意してください。既にロード済みのAppDomainがある場合、何か不測の事態が起こって、そのAppDomainがもはや不要になったとき、それをいつまでもメモリにとどめたくはないからです。
RemoteServiceHandler
について、Remoting機能との関連でいくつか考えておかなければならないことがあります。まず、MarshalByRef
からの継承を指定したことです。これについては既に説明したので、ここでは繰り返しません。次に、InitializeLifetimeService
をオーバーライドして、nullを返させることです。なぜそうするかと言えば、プロキシが期限切れになって、リモートオブジェクトが回収されてしまうことを防ぐためです。このメソッドからnullを返すと、インスタンスの寿命管理が実質的に禁止されます。今回の例では、作成する被管理サービスとの間に有効なリンクを維持しておく必要があります。そうしないと、将来(数週間後、あるいは数か月後)、その被管理サービスに対してStopService
メソッドを呼び出すことができないからです。
被管理サービスのAppDomain内にあるRemoteServiceHandler
インスタンスへの透過的なプロキシが得られたら、さらにもう1歩進んで、そのドメイン中でサービスを開始します。そのためには、リスト7に示すとおり、RemoteServiceHandler
のLoadService
メソッドを呼び出します。これにより、他のAppDomain内のインスタンスにメッセージが送られ、これこれという名前のサービスをロードするように伝達されます(ちょうど、別室にいる同僚にメモを送り、あることで手助けをしてくれるよう頼む感じでしょうか)。プロキシで何かをするということは、相手インスタンスに対して直接何かをするのではなく、メッセージのやり取りで済ませることだと覚えておいてください。
try { if (!svc.LoadService(asmName.FullName)) { AppDomain.Unload(svcDomain); Logger.WriteToLog( String.Format("No ServiceEntryPointAttribute was " + "found for assembly '{0}'.", asmName.FullName), System.Diagnostics.EventLogEntryType.Warning); return; } } catch (Exception ex) { AppDomain.Unload(svcDomain); Logger.LogException(ex); return; }
LoadService
には3つの重要な働きがあります。まず、目的のアセンブリをロードすることです。今回のロードには、先に.NETアセンブリかどうかを確かめるときに取り出しておいたAssemblyName.FullName
プロパティを使用します。被管理サービスアセンブリのロードが済むと、今度はReflectionのAssembly.GetCustomAttributes
メソッドを用いて、そのアセンブリのServiceEntryPointAttribute
を見つけようとします。ロードされたアセンブリにServiceEntryPoint
属性が適用されていれば、LoadService
はサービスエントリポイントの型名とフレンドリ名を取り出します。最後に、その属性から得られた情報に基づいて、IService
を実装する型インスタンスを作成します。具体的には、そのアセンブリに対してCreateInstance
を呼び出し、サービスエントリポイントの型名を引き渡します。次のリスト8を見てください。
try { this.service = (IService)assembly.CreateInstance( this.serviceEntryPointType, true); } catch (Exception ex) { throw new TypeInitializationException( this.serviceEntryPointType, ex); }
例外の発生もなくすべてが進めば、このメソッドからはtrueが返され(メッセージがServiceBroker
に送り返され)、指定されたアセンブリの被管理サービスが正しくロードされたことが伝達されます。この時点で、ServiceBroker
の魔法はほぼ完成です。つまり、新しいAppDomainが作成され、そこへ目的の被管理サービスがリモートロードされています。あと残るのは、その被管理サービスを開始すること、そしてのちにサービスの停止とアンロードが必要になったとき使用できるよう、それへの参照をキャッシュに記憶しておくことです。リスト9を参照してください。
svc.StartService(); serviceName = svc.ServiceName; this.serviceNames.Add(filePath, serviceName); this.services.Add(serviceName, svc); this.serviceLastModified.Add( serviceName, File.GetLastWriteTime(filePath)); this.serviceAppDomains.Add(serviceName, svcDomain);
リスト9に示すコードの実行が終わった時点で、サービスは既に開始されています。つまり、当該被管理サービスの作者がIService.StartService
実装に含めておいたコードが、既に実行されています。パート1で被管理サービスのセットアップ方法を示した際、私がSampleServiceでやったことは、イベントログにメッセージを書き込むことだけでした。しかし、一般の被管理サービスでは、タイマを始動させるなどして、定期的に何らかのタスクを実行させるのが普通でしょう。
サービスの停止
もちろん、これまでやってきたことの裏返しとして、被管理サービスを停止させるための作業も必要です。しかし、困難な仕事はすべて終わっていて、停止にともなう作業はずっと簡単ですから安心してください。被管理サービスアセンブリが削除され、あるいはServicesアプレット中で.NET Service Managerが停止されると、ServiceBroker
のStopService
メソッド(リスト10)が呼び出されます。見てわかるとおり、このメソッドは停止すべき被管理サービスのファイルパスを取り出し、それを使ってサービス名キャッシュ内で当該サービスを探します。そして見つかれば、UnloadService
メソッド(前出のリスト4)を呼び出し、それをサービス名キャッシュから取り除きます。万事これほど簡単に運ぶといいのですが……。
public void StopService(string filePath) { if (this.serviceNames.Contains(filePath)) { this.UnloadService(Convert.ToString( this.serviceNames[filePath])); this.serviceNames.Remove(filePath); } }
以上で、.NET Service Managerの魔法の源泉をあらかた見終わりました。もちろん、そのほとんど(特に、AppDomain、Remoting、Reflectionを取り巻く諸概念)は、どのようなアプリケーションでも便利に使用でき、アセンブリの動的なロード、更新、アンロードといった便利な機能を実現できます。
残務整理
.NET Service Managerとは切っても切り離せないのに、説明が長くなるために本稿では取り上げなかった事柄がいくつかあります。その第一は、Windowsサービスコードそのものです。これについては既に多くの資料(.NETでWindowsサービスを構築する方法などを扱った記事の類)が出回っていて、私から価値ある何かを付け加えられるとは思えないという事情もありました。あえて言うならば、実際のWindowsサービスでは、ServiceBroker
インスタンスを作成しなければならず、FileSystemWatcher
を使ってアプリケーションディレクトリ内で新しいDLLを探すことも必要である、ということだけ指摘しておきたいと思います。ディレクトリに新しいDLLが追加されたり、既存のDLLが変更されたりすると、そのDLLのServiceBroker.StartService
が呼び出されます。また、あるDLLが削除されると、そのServiceBroker.StopService
が呼び出されます。Windowsの[サービス]アプレットを使って開始または停止した場合や、コンソールからコマンドを発行した場合は、ディレクトリ中のすべてのDLLが順繰りに調べられ、目的のDLLのServiceBroker
インスタンスに対してStartService
メソッドもしくはStopService
メソッドが呼び出されます。
上記のとおりディレクトリを監視していて、1つまずいことに気がつきました。それは、ファイルをディレクトリにドロップしたとき、同じファイルに対してFileSystemWatcher
が複数のイベントを発生させることがあるという点です。そこで、一度のファイル変更が複数回処理されるのを防ぐため、変更から変更まで2秒間待つというチェックを入れてみました。残念ながら、この方法では複数のDLLを同時にドロップしたときにエラーが起こるという別のバグが発生したため、チェックを手直しして、変更されたファイルが前回変更されたファイルと別物かどうかを調べるようにしました。つまり、適用後2秒以上経過したファイル変更か、別ファイルへの変更だけを処理するという規則です。
もう1つ簡単に触れておきたい事柄に、Config
クラスがあります。パート1を読んだ方はご記憶でしょうが、これは被管理サービスごとに別個の設定ファイルを持てるようにするためのクラスです。つまり、このクラスのインスタンスは、被管理サービスアセンブリ名に「.config」を付加したファイルを探すようにFileSystemWatcher
をセットアップします(.NETが.EXEアプリケーションを探すのと同様です)。具体的には、Assembly.GetCallingAssembly().GetName(false).CodeBase
を呼び出して、URLに似た構文を標準のWindows構文に置き換えて末尾に「.config」を付加し、用意された文字列インデクサによる設定要求があるまでロードを遅らせます。
.NET Service Managerは、既に私と仲間たちにとっては開発に役立つ有用なサービスとなっています。読者にとっても同様であることを願っています。このアプリケーション全体の課題として、なかなか解決が難しいだろうと思われる問題が1つあることを申し上げておきます。それは、従属DLLを持つ被管理サービスを配備するときには、参照されるDLLを先に配備しておかなければならないということです。そうしないと、被管理サービスに対してStartService
を呼び出したとき、参照すべきアセンブリが見つからないというエラーになります。一定時間待ってからアセンブリの処理を開始するよう、何らかの時限式キューを作ることも考えましたが、完全には信頼できるものになりません。おそらく、配備手順の中で記録しておいて、忘れないようにするのが最良の解決策だと思います。
参考資料
- MSDN Library 『.NET Remoting Overview』
- MSDN Library 『Programming with AppDomains』
- MSDN Library 『AppDomains and Dynamic Loading』 Eric Gunnerson 著、2002年5月
- MSDN Library 『Reflection Overview』
- MSDN Library 『FileSystemWatcher Class』
- Suzanne Cook's .NET CLR Loader Notes