gRPC という超クールなものが大人気な昨今、WCF なんてひと昔もふた昔も前のもの...と鼻で笑っているあなた!古き良き (?) WCF はまだまだ現役の世界線もあるのですよ!
ゴンドアの谷の歌にあるもの。
— じんぐる (@xin9le) 2019年12月24日
「WCF に根を下ろし、SOAP と共に生きよう。XML と共に Web を越え、REST と共に API を歌おう」
どんなに恐ろしいプロトコルを持っても、たくさんの可哀想なライブラリを操っても、WCF から離れては生きられないのよ!
背景 / 前話
ということで僕は仕事で EC サービスの開発/運営をやっているのですが、とある WCF を採用している外部 API と連携する必要が業務上の必須要件としてあるのです。そして、その外部 API はアクセス元を IP 制限しています。僕たちが運用している EC サービスは Azure Web Apps にホストされているので、IP を固定するためにプロキシサーバーを経由するようにしています。そしてこのプロキシサーバーはいわゆる「ユーザー名 / パスワード」での認証が必要になっています。誰でも自由に踏み台にできてしまったら困りますからね。
...と、ここまではかなりオーソドックスなアプローチかと思うのですが、認証があることによって WCF はメチャクチャ面倒が増えます。そういった部分に対応していくための方法をご紹介します。
Case.1 : 認証なし [.NET Core / .NET Framework]
認証がない場合は非常にシンプルです。Binding インスタンスにこれだけ設定すれば OK です。内部的に WebProxy
インスタンスを生成して利用してくれます。
var binding = new BasicHttpsBinding(); binding.ProxyAddress = new Uri($"http://{host}:{port}"); binding.UseDefaultWebProxy = false;
Case.2 : 認証あり - グローバル対応 [.NET Framework]
Case.1 の書き方は簡単なのですが認証情報を設定する方法がありません。ここで一気に躓くのですが、WCF が内部で WebRequest
型を利用して通信しているのを利用すれば以下のように書くことができます。
// WebRequest による通信をプロキシ経由にしてしまおう!(雑 var proxy = new WebProxy(host, port); proxy.Credentials = new NetworkCredential("UserName", "Password"); WebRequest.DefaultWebProxy = proxy; var binding = new BasicHttpsBinding(); binding.UseDefaultWebProxy = false; // システム既定のプロキシを使うかどうかの設定なので false
これはこれで OK なのですが、WebRequest
の既定値を変更しているので大半がプロキシを経由することになってしまうでしょう。それが許されるなら良いのですが、そんな雑過ぎる暴挙は本来良くない!
しかもこれは .NET Framework でのみ有効で .NET Core では動きません。「なんでやねん!」と思うかもしれないのですが、.NET Core 版の WCF は WebRequest
ベースから HttpClient
ベースに実装が書き換わっているからです。一筋縄では行かなさそうな嫌な空気感が徐々に出てきましたね。
Case.3 : 認証あり - 個別対応 [.NET Framework]
Case.2 だと WebRequest
を利用した他の通信にまでグローバルに影響を与えてしまうのがイヤなので、なんとかして WCF の通信だけにプロキシを適用したいものです。と、ここで意地になって .NET Framework の実装を Reference Source で読み漁ってみると...?なんと裏口入学的に Proxy を差す口があるんですねー。
// こんな独自の Binding 型を用意する public class WebProxyBinding : BasicHttpsBinding { private WebProxy Proxy { get; } public WebProxyBinding(WebProxy proxy) { this.Proxy = proxy; this.UseDefaultWebProxy = false; } public override BindingElementCollection CreateBindingElements() { // リフレクションを利用して無理やり internal な 'Proxy' プロパティに WebProxy を差し込む // 'ProxyAddress' プロパティなどよりも優先される隠しプロパティ var elements = base.CreateBindingElements(); var element = elements.Find<HttpsTransportBindingElement>(); var flags = BindingFlags.Instance | BindingFlags.NonPublic; var propertyInfo = element.GetType().GetProperty("Proxy", flags); if (propertyInfo != null) propertyInfo.SetValue(element, this.Proxy); return elements; } }
// BasicHttpsBinding の代わりに独自型を利用 var binding = new WebProxyBinding();
internal なプロパティに無理やり差し込むという完全な力業で神回避!最初から Proxy プロパティを公開していてくれれば何も困ることはなかったのに...、と思わなくはないのですが、何か意図があったんでしょうね。ちなみに CreateBindingElements
メソッドは WCF のライブラリ側から相当回数コールされます。リフレクションを使っている手前、かなり実行パフォーマンスが低下することが予想されるのでご注意ください。
また、このアプローチは .NET Framework だけで有効です。.NET Core 版の WCF 実装は Proxy プロパティが丸ごと消え去っているので、リフレクションですら手出しができません。上記の実装例だと .NET Core では propertyInfo
が null
で返ってきます。なので、過信してそのまま .NET Framework を .NET Core に移植するとハマります。
Case.4 : 認証あり [.NET Core]
.NET Core で Case.2 も Case.3 も利用できないとなると、.NET Core で WCF + 認証 Proxy を利用している方は本気で頭を抱えると思います。僕もそうでした。とは言え業務。そう易々と諦めることはできません。なんとかしなければと意地になって GitHub にある WCF の .NET Core 実装を読みまくりました。その結果、隠し機能のような差し込み口を発見しました。
// こんな感じの型を用意 public class WebProxyBehavior : IEndpointBehavior { private WebProxy Proxy { get; } public WebProxyBehavior(WebProxy proxy) => this.Proxy = proxy; #region IEndpointBehavior implementations public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { // このデリゲート型 (固定) を突っ込むとコールバックしてくれる!(知らんがな // ちょっとでもパフォーマンスを求めるならデリゲートをキャッシュすると善き Func<HttpClientHandler, HttpMessageHandler> callback = handler => { // ここで設定すると内部で通信する HttpClient に渡される handler.Proxy = this.Proxy; return handler; }; bindingParameters.Add(callback); } public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { } public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { } public void Validate(ServiceEndpoint endpoint) { } #endregion }
// プロキシを作って var proxy = new WebProxy(host, port); proxy.Credentials = new NetworkCredential("UserName", "Password"); var behavior = new WebProxyBehavior(proxy); // 差し込む var binding = new BasicHttpsBinding(); var endpoint = "https://example.com/foo.svc"; var client = new SampleClient(binding, endpoint); client.Endpoint.EndpointBehaviors.Add(behavior);
IEndpointBehavior
を実装した型で Func<HttpClientHandler, HttpMessageHandler>
デリゲートを BindingParameterCollection
に突っ込むという隠し機能!見つけたときはメチャメチャ歓喜しましたw ちなみにこのあたりに実装があります。
この拡張ポイントは WCF が通信をするときに内部で利用する HttpClient
の HttpMessageHandler
をカスタマイズするためにあります。今回は Proxy を設定するために利用していますが、通信にかかった時間を測定したりログを出力するなど、通信の前後をフックしていろんなことができるようになっています。全然まともにドキュメント化されていないのが残念ですが、.NET Framework 時代よりも汎用性の高い拡張ポイントを提供していると思います。
ちなみに、この拡張ポイントは .NET Core で新たに追加されたものなので .NET Framework では動作しません。具体的には bindingParameters.Add(callback)
でコールバックを登録していてもコールバックされません。無視されます。
Case.5 : 認証あり [.NET Standard]
最後に .NET Standard で WCF の通信をラップするようなライブラリを作る場合です。ここまで読んできてお分かりかと思いますが、 Case.3 と Case.4 を組み合わせれば OK です。お互いの実装がちょうどよく無視されるので、特に干渉することなく動作します。めでたし。