254 自己署名証明書を使っているWebサーバーにHTTPSでアクセスしたい
AuthenticationExceptionを独自に検証する方法
HTTPSでは接続の開始時にサーバーの提示したサーバー証明書の正当性が検証されます。このときにサーバーが提示した証明書の正当性を検証できない場合(つまり、そのサーバーが正しい接続先だと認められない場合)、WebException(内容はAuthenticationException)がスローされて通信に失敗します。
System.Net.ServicePointManagerクラスのServerCertificateValidationCallback静的イベントにコールバックを登録すると.NET Frameworkによる検証後に独自の検証処理を追加できます。
using System;
using System.IO;
using System.Net;
using System.Net.Security;
class VerifyTestCert
{
// 既知のテスト証明書のフィンガープリント
const string ExpectedFingerprint = "4F-F6-5E-E3-CE-B0-99-BD-68-6D-ADBA-EC-16-B8-D1-5C-30-75-6A";
// 静的コンストラクターでServicePointMangerのコールバックを設定する
static VerifyTestCert()
{ // ServerCertificateValidationCallbackは.NET Frameworkによる検証後に
// 呼び出されて、検証結果をオーバーライドできる
ServicePointManager.ServerCertificateValidationCallback =
// certはX509Certifacteクラスのオブジェクト
// chainはX509Chainクラスのオブジェクト
// sslPolicyErrosはSslPolicyErrors列挙体
// trueを返すと接続処理が続行される。falseを返すと
// WebExceptionがスローされる
(sender, cert, chain, sslPolicyErros) => {
if (sslPolicyErros == SslPolicyErrors.None)
{ // .NET Frameworkによってポリシーエラーが検出されていなければOK
return true;
}
// エラーであっても証明書のハッシュが既知のフィンガープリントと
// 一致していればOKとする
return ExpectedFingerprint == BitConverter.ToString(
cert.GetCertHash());
};
}
static void Main(string[] args)
{ // 引数で接続先を変える
var request = WebRequest.CreateHttp(args[0]);
request.UserAgent = "sample web client";
request.Accept = "*/*";
using (WebResponse response = request.GetResponse())
using (var reader = new StreamReader(response.GetResponseStream()))
{
Console.WriteLine(reader.ReadToEnd());
}
}
}
「テストを行なう際、このレシピを知っておくと意外と便利です」
第16章 プロセスとスレッド
286 指定した時間処理を停止したい
Task.Delay静的メソッドを利用する
連続して処理を実行することに問題があるため、一時的に処理を停止したいことがあります。
たとえば、ウェブスクレイピングを行う場合には、連続してデータを取得するとサービス拒否攻撃とみなされる可能性があります。このため、人間がブラウジングするときと同様に数ページアクセスしたところで数秒の間隔を空けます。
このような場合には、Task.Delay(ミリ秒)を利用します。Task.Delayは.NETFramework 4以降で有効なので、それより前のバージョンではThread.Sleep(ミリ秒)を利用して休止します。この2つのAPIの差は、Thread.Sleepが文字通り呼び出したスレッドそのものを休眠状態にするのに対し、Task.Delayは実行しているスレッドを別の処理に割り当て可能な状態とすることです。したがって、Task.Delayのほうがスレッドを占有しない分効率的です。
static async Task ReadFromWeb(string root)
{
using (var client = new HttpClient())
{
foreach (var uri in GetLinks(root, await client.GetStringAsync(root)))
{
Console.Write(uri);
// Webサーバーからuriを取得する
var response = await client.GetAsync(uri);
Console.WriteLine($"...{response.StatusCode}");
await Task.Delay(1000); // 最低1秒間このスレッドを開放する
}
}
}
「この方法を知ってから、Thread.Sleepを書くときに手が震えます。これはリソースの無駄遣いなんだ、と(笑)」
第17章 例外処理
293 特定のプロパティ値を持つ例外だけをキャッチしたい(例外フィルターを使いたい)
catchとwhenを利用する
「catch (例外型 変数名)」に続けて「when (条件)」を記述すると、catch節で指定した例外の型で、かつwhen節で指定した条件に合致したcatchブロックが実行されます。
try
{
cmd.ExecuteNonQuery(); // insert tableの実行
return InsertSucceeded;
}
catch (SQLException e) when (e.Number == 2601)
{
return RetryByUpdate; // 呼び出したメソッドにupdate文を用意させる
}
// Numberプロパティが2601以外のSQLExceptionは上位へ伝播される
「C# 7から型に応じて処理を分岐するパターンマッチングが導入される予定と聞いたので、Elixirのパターンマッチングの書き方に似ていることもあり、catch whenの技術を流用して似たような構文で実現するのではないかと予測していたらちょっと外れてしまいました。でもcatchを使っている人ならすでにパターンマッチングの予習はできているので、安心してC# 7のパターンマッチングを使いこなせますね」
第18章 メタプログラミング
299 フィールド名を指定してフィールドにアクセスしたい
FieldInfoオブジェクトを取得し、GetValue/SetValueメソッドを利用する
オブジェクトのフィールドを取得/設定するには、オブジェクトの型(Typeクラスのオブジェクト)のGetFieldメソッドを呼び出して得たFieldInfoオブジェクトのGetValue/SetValueメソッドを利用します。
class Test
{
public int counter = 3;
internal string name = "abc";
public static readonly string classDesc = "xyz";
}
...
var test = new Test();
// publicフィールドはType.GetFieldにフィールド名を与える
var counter = typeof(Test).GetField("counter");
// 非publicフィールドは第2引数にBindingFlags.NonPublic | BindingFlags.Instance
// を与える
var name = typeof(Test).GetField("name",
BindingFlags.NonPublic | BindingFlags.Instance);
// フィールド値を取得するには、FieldInfo.GetValueに対象のオブジェクトを与える
Console.WriteLine($"{counter.GetValue(test)}, {name.GetValue(test)}");
// フィールドに値を設定するには、FieldInfo.SetValueに対象のオブジェクトと値を与える
counter.SetValue(test, 48);
name.SetValue(test, "ABC");
Console.WriteLine($"{test.counter}, {test.name}");
// publicフィールドはType.GetFieldにフィールド名を与える(静的フィールドかは問わない)
var classdesc = typeof(Test).GetField("classDesc");
// 静的フィールドのFieldInfo.GetValue / SetValueの第1引数はnullを与える
classdesc.SetValue(null, "XYZ");
// readonlyかどうかは関係しない
Console.WriteLine(Test.classDesc); // => XYZ (readonlyはコンパイル時の制約なので影響しない)
「前のレシピ(026)で、readonlyで修飾したフィールドは再代入ができないので安全だと言いながら、このレシピではreadonlyでも書き換えられると言っています(笑)。自分で自分の頭でも脚でも撃ち抜ける仕組みを持っている言語はプログラマを信頼している気持ちのよい言語ですね。それができない言語は気分的にも実用的にもいまいちです」
第19章 プログラム開発支援
306 デバッグ(トレース)出力をファイルに書き出したい
Listenersプロパティにリスナーを設定する
Debugクラス、TraceクラスのWriteLine、WriteLineIfなどのメソッドの出力先は、各クラスのListenersプロパティに追加したリスナーによって決まります。
既定のリスナーにはWindows APIのOutputDebugString関数を呼び出すDefaultTraceListenerが設定されています。
この設定は、構成ファイル(.exe.config)で変更できます。
<configuration>
<system.diagnostics>
<!-- 要素名はtraceだが、Debugクラスのリスナーも設定される -->
<trace autoflush="false">
<listeners>
<remove name="Default">
<!-- C:\temp¥traceOut.logに出力するTextWriterTraceListenerを設定
name属性はなんでも良い(リスナーのNameプロパティに設定される) -->
<add initializedata="c:¥temp¥traceOut.log" name="fileListener" type="System.Diagnostics.TextWriterTraceListener">
<!-- コンソール(標準出力)へトレース出力するには以下の設定を利用する
<add name="consoleListener" type="System.Diagnostics.ConsoleTraceListener"/>
-->
</add>
</trace>
</system.diagnostics>
「今回、かなりしっかり調べた結果が反映されているのがこのレシピです。こうやればいいのかと喜んでもらえる方が多いのではないでしょうか。私自身、これを見つけたときとても嬉しかったですね」
第20章 Windows環境
315 イベントログへ書き込みたい
イベントログへ出力する手順
イベントログへ出力するには次の手順を取ります。
- 第1引数にアプリケーション名、第2引数にログファイル名を指定してCreateEventSource静的メソッドを呼び出してプログラムを登録します。第2引数をnullとすると、Applicationログへの出力となります。
- EventLogをnewしてオブジェクトを作成します。
- (2)で作成したEventLogオブジェクトのSourceプロパティに(1)で指定したアプリケーション名を設定します。
- (2)で作成したEventLogオブジェクトのWriteEntryメソッドを呼び出してログを出力します。
イベントログ出力手順についての注意点
EventLog.CreateEventSourceおよび未登録時のSourceExistsの実行には管理者権限が必要です。これはアクセスに管理者権限が必要となるSecurityログを含むすべてのログを検索して、同じ名前が出現していないか確認するためです。そこで、ユーザー権限で実行するプログラムについては、上記の(1)の処理は他の処理とは切り離してインストーラーなどで提供したほうが良いでしょう。
一度アプリケーション名を登録すると、以降はSecurityログ以外についてはユーザー権限で書き込みが可能です。
// using System.Diagnostics; が必要
const int IOErrorEvent = 101; // EventID (任意の整数)
const short CatOperationFailed = 10;// カテゴリ(任意の整数)
...
if (!EventLog.SourceExists("EvLogTest")) // アプリケーション名が未登録ならば登録する
{ // Applicationログを利用する場合、第2引数はnullで良い。それ以外の場合はログ名を指定する
EventLog.CreateEventSource("EvLogtest", null);
// CreateEventSource静的メソッドは管理者権限でなければ実行できない
}
// ログの書き込み エラー発生はレアケースという前提でEventLogはusingで囲む
using (var log = new EventLog ( ))
{
log.Source = "EvLogTest"; // CreateEventSourceで登録したアプリケーション名を設定する
// 文字列のみを与えると情報レベルの書き込みとなる
log.WriteEntry($"IO Error {errorNumber}");
// EventLogEntryType列挙体で文字列とイベントログ種(エラー、警告、情報)を指定する
log.WriteEntry($"IO Error {errorNumber}", EventLogEntryType.Error);
// int型の第3引数でアプリケーション定義のイベント識別子を書き込める
// 2引数以下の呼び出しでは0となる
log.WriteEntry($"IO Error {errorNumber}", EventLogEntryType.Error,
IOErrorEvent);
// short型の第4引数でアプリケーション定義のログのカテゴリを指定できる
// 3引数以下の呼び出しでは「なし」となる
log.WriteEntry("start EvLogTest app", EventLogEntryType.Information,
IOErrorEvent,CatOperationFailed);
}
「Win32APIを使ってイベントログを出力するのは意外と面倒なんです。メッセージのテンプレートを格納したDLLを用意して、あらかじめこれを使用するように登録しておかなければなりません。でないと、イベントログに『メッセージの詳細が見つかりません』というエラーメッセージが出てきます。ところが.NET Frameworkにはデフォルトのテンプレートが用意されているんですよ。これはすごく助かります。Win32APIの面倒さからイベントログを敬遠していると、.NET Frameworkではよろしくやってくれるようになっていると気づかないんですね」
便利になっていくC#をキャッチアップし続けることが大事
「C#はどんどん書きやすくなっているので、変化をキャッチアップしていってください。C# 1のときに覚えたもの以外は認めないというのは馬鹿らしいし、キャッチアップしなくてもきちんと動くからOKというのも違います。もし仕事で開発をしているなら、新しく入ってきた人の書き方についていけなくなるか、社内で決められている古い書き方をルールとして強制することになります。どちらも困りますよね。
せめてC# 4までの書き方、つまりラムダ式を利用した書き方は理解して利用できるようになりましょう。非同期処理ならいきなりC# 5のasync/awaitから入るのもありですし、最新のC# 6はさらに細かく改善されています。きっとC# 7はさらによくなるでしょう。もちろん常に正しいわけではなく、指摘しようと思えばC# 2の匿名メソッドやC# 3のSQL構文のようにろくでもないものもあります。しかし、そういうものは匿名メソッドに対するラムダ式のように次のバージョンでちゃんと代替案が出てくるか、LINQのメソッド形式+ラムダ式のように最初からあるべきものも用意されています。どちらもラムダ式ですね。同じように本書でTupleが便利だと思って覚えても、C# 7が登場したときTupleが更新されていれば本書で得た知識は古いバージョン専用として切り捨てる、そういう考え方ができるようになってもらいたいですね」


