WCF呼び出しクラスで失敗した接続をトラップする場所は?
-
05-07-2019 - |
質問
WCF呼び出しをカプセル化するクラスを作成しようとしています(重要な場合、クライアントはSilverlightです)。すべてが順調に動作しますが、サーバーが応答しないかのように、接続障害をトラップする方法がわかりません。 ChannelFactoryの結果のコードのどこかで少し作業が行われているように見えますが、よくわかりません。一般的なコードレビューも歓迎します。 :)
チャネルの作成を囲む一番下の行、またはtry / catchの開始または非同期の結果デリゲートは、失敗した接続をトラップしません。そのキャッチにServiceCallErrorイベントを実行させたいのです。
public class ServiceCaller : IDisposable
{
private IFeedService _channel;
public ServiceCaller()
{
var elements = new List<BindingElement>();
elements.Add(new BinaryMessageEncodingBindingElement());
elements.Add(new HttpTransportBindingElement());
var binding = new CustomBinding(elements);
var endpointAddress = new EndpointAddress(App.GetRootUrl() + "Feed.svc");
_channel = new ChannelFactory<IFeedService>(binding, endpointAddress).CreateChannel();
}
public void MakeCall(DateTime lastTime, Dictionary<string, string> context)
{
AsyncCallback asyncCallBack = delegate(IAsyncResult result)
{
var items = ((IFeedService)result.AsyncState).EndGet(result);
if (ItemsRetrieved != null)
ItemsRetrieved(this, new ServiceCallerEventArgs(items));
};
_channel.BeginGet(lastTime, context, asyncCallBack, _channel);
}
public event ItemsRetrievedEventHandler ItemsRetrieved;
public event ServiceCallErrorHandler ServiceCallError;
public delegate void ItemsRetrievedEventHandler(object sender, ServiceCallerEventArgs e);
public delegate void ServiceCallErrorHandler(object sender, ServiceCallErrorEventArgs e);
public void Dispose()
{
_channel.Close();
_channel.Dispose();
}
}
好奇心those盛な人向けのスタックトレースは次のとおりです。
An AsyncCallback threw an exception.
at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously, Exception exception)
at System.ServiceModel.Channels.HttpChannelFactory.HttpRequestChannel.HttpChannelAsyncRequest.OnGetResponse(IAsyncResult result)
at System.Net.Browser.BrowserHttpWebRequest.<>c__DisplayClassd.<InvokeGetResponseCallback>b__b(Object state2)
at System.Threading._ThreadPoolWaitCallback.WaitCallback_Context(Object state)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading._ThreadPoolWaitCallback.PerformWaitCallbackInternal(_ThreadPoolWaitCallback tpWaitCallBack)
at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback(Object state)
これを実現するには、ブラウザーでアプリを起動し、Visual StudioからWebサーバープロセスを強制終了します。テスト環境では、クライアントシステムのネットワーク接続を切断することで同じことが得られます。
完全な例外のToString()は次のとおりです。
System.Exception: An AsyncCallback threw an exception. ---> System.Exception: An AsyncCallback threw an exception. ---> System.ServiceModel.CommunicationException: The remote server returned an error: NotFound. ---> System.Net.WebException: The remote server returned an error: NotFound. ---> System.Net.WebException: The remote server returned an error: NotFound.
at System.Net.Browser.BrowserHttpWebRequest.InternalEndGetResponse(IAsyncResult asyncResult)
at System.Net.Browser.BrowserHttpWebRequest.<>c__DisplayClass5.<EndGetResponse>b__4(Object sendState)
at System.Net.Browser.AsyncHelper.<>c__DisplayClass2.<BeginOnUI>b__0(Object sendState)
--- End of inner exception stack trace ---
at System.Net.Browser.AsyncHelper.BeginOnUI(SendOrPostCallback beginMethod, Object state)
at System.Net.Browser.BrowserHttpWebRequest.EndGetResponse(IAsyncResult asyncResult)
at System.ServiceModel.Channels.HttpChannelFactory.HttpRequestChannel.HttpChannelAsyncRequest.CompleteGetResponse(IAsyncResult result)
--- End of inner exception stack trace ---
at System.ServiceModel.Channels.Remoting.RealProxy.Invoke(Object[] args)
at proxy_2.EndGet(IAsyncResult )
at CoasterBuzz.Feed.Client.ServiceCaller.<MakeCall>b__0(IAsyncResult result)
at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
--- End of inner exception stack trace ---
at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously, Exception exception)
at System.ServiceModel.Channels.ServiceChannel.SendAsyncResult.CallComplete(Boolean completedSynchronously, Exception exception)
at System.ServiceModel.Channels.ServiceChannel.SendAsyncResult.FinishSend(IAsyncResult result, Boolean completedSynchronously)
at System.ServiceModel.Channels.ServiceChannel.SendAsyncResult.SendCallback(IAsyncResult result)
at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
--- End of inner exception stack trace ---
at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously, Exception exception)
at System.ServiceModel.Channels.HttpChannelFactory.HttpRequestChannel.HttpChannelAsyncRequest.OnGetResponse(IAsyncResult result)
at System.Net.Browser.BrowserHttpWebRequest.<>c__DisplayClassd.<InvokeGetResponseCallback>b__b(Object state2)
at System.Threading._ThreadPoolWaitCallback.WaitCallback_Context(Object state)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading._ThreadPoolWaitCallback.PerformWaitCallbackInternal(_ThreadPoolWaitCallback tpWaitCallBack)
at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback(Object state)
ところで、これをキャッチできる唯一の方法は、SLクライアントでアプリレベルのUnhandledExceptionイベントを使用することです。
解決
ジェフ、接続の失敗をどのようにシミュレートしているのか、あなたの質問からは完全にはわかりません。 「サーバーが応答しないかのように」と仮定しています。何らかのタイムアウトエラーを探していることを意味します。サーバー側でThread.Sleep()を使用して、サービス呼び出しからの応答を遅くすることにより、応答しないサーバーをシミュレートしている可能性があります。右?十分近いですか?
同様のプロジェクトがあったため、これをテストし、タイムアウトやその他の例外を正常にトラップすることができました。カスタムバインディングのデフォルトタイムアウトが非常に長く、十分な時間待機していない可能性があるため、これらの例外は表示されなかったと思われます。
説明すると、WCFタイムアウトはバインディング構成によって制御されます。コードを見ると、次のような独自のカスタムバインディングを作成しています。
var elements = new List<BindingElement>();
elements.Add(new BinaryMessageEncodingBindingElement());
elements.Add(new HttpTransportBindingElement());
var binding = new CustomBinding(elements);
そこでブレークポイントを設定し、作成したカスタムバインディングを検査すると、1分間の SendTimeout はデフォルトで。長い間待たなかったか、サービスのシミュレートされたタイムアウトを十分に長く設定していないため、タイムアウト例外をキャッチできなかったと思われます。
ここに、いくつかのコード変更を示します。まず、バインディングにタイムアウトを設定します:
var binding = new CustomBinding(elements);
//Set the timeout to something reasonable for your service
//This will fail very quickly
binding.SendTimeout = TimeSpan.FromSeconds(1);
最後に、例外をキャッチして正しいイベントを発生させるために、次のようにAsyncCallbackデリゲートを更新できます。 EndGet()が呼び出されると、例外がスローされます。
AsyncCallback asyncCallBack = delegate(IAsyncResult result)
{
IFeedService service = ((IFeedService)result.AsyncState);
try
{
var items = service.EndGet(result);
ItemsRetrieved(this, EventArgs.Empty);
}
catch (System.TimeoutException ex)
{
//Handles timeout
ServiceCallError(this, EventArgs.Empty);
}
catch (System.ServiceModel.CommunicationException ex)
{
//Handles a number of failures:
// Lack of cross-domain policy on target site
// Exception thrown by a service
ServiceCallError(this, EventArgs.Empty);
}
catch (System.Exception ex)
{
//Handles any other errors here
ServiceCallError(this, EventArgs.Empty);
}
};
一般的なコードレビューに関しては、バインディング構成のハードコーディングを避けることをお勧めします。代わりに、ServiceReferences.ClientConfigファイルでエンドポイント構成(妥当なタイムアウトを含む)を宣言し、次のようなエンドポイント名でチャネルファクトリを更新できます。
new ChannelFactory<IFeedService>("feedServiceEndpoint");
これにより、時間の経過とともにアプリのメンテナンス性が向上します。
これが役立つことを願っています。
ジェリー
更新:
ジェフ、
ここで何が起きているかの詳細な説明については、このコードとその中のコメントをご覧ください。 MakeCall()メソッドは、BeginGet()メソッドに渡される関数(デリゲート)を作成しています。その関数は、サービスが応答するときに後で実行されます。
提案された変更を行い、&quot; AsyncCallback asyncCallback&quot;で始まる行にブレークポイントを設定してください。および「var items」。デバッガーを介した最初のパスは、サービスが応答するときに実行するコード(関数/デリゲート)を宣言するだけで、2番目のパスはデリゲート宣言の内部から始まるその応答の実際の処理です。これは、サービスコールの応答が処理されるときに、外側のtry / catchがスコープ内にないことを意味します。
public void MakeCall(DateTime lastTime, Dictionary<string, string> context)
{
try
{
AsyncCallback asyncCallBack = delegate(IAsyncResult result)
{
try
{
var items = ((IFeedService)result.AsyncState).EndGet(result);
if (ItemsRetrieved != null)
ItemsRetrieved(this, new ServiceCallerEventArgs(items));
}
catch (Exception ex)
{
//This will catch errors from the service call
}
};
_channel.BeginGet(lastTime, context, asyncCallBack, _channel);
}
catch(Exception ex)
{
//This will not catch an error coming back from the service. It will
//catch only errors in the act of calling the service asynchronously.
//The communication with the service and response is handled on a different
//thread, so this try/catch will be out of scope when that executes.
//So, at this point in the execution, you have declared a delegate
//(actually an anonymous delegate the compiler will turn into a hidden class)
//which describes some code that will be executed when the service responds.
//You can see this in action by setting a breakpoint just inside the delegate
//at the line starting with "var items". It will not be hit until the service
// responds (or doesn't respond in a timeout situation).
}
}
ジェリー
他のヒント
以前にも同様の問題が発生しました。主な問題は、IDisposableがプロキシ/チャネルのインスタンスに実装される方法にあります。私がそれを解決した方法は、以下のコードに示されています。 IDirector
は私のサービス契約です:
public class ProxyWrapper : IDisposable
{
private IDirector proxy;
private ChannelFactory<IDirector> factory;
int callCount = 0;
public ProxyWrapper()
{
factory = new ChannelFactory<IDirector>();
proxy = factory.CreateChannel();
}
public IDirector Proxy
{
get
{
if (callCount > 0)
throw new InvalidOperationException("The proxy can only be accessed once for every ProxyWrapper instance. You must create a new ProxyWrapper instance for each service request.");
// only allow the proxy/channel to be used for one call.
callCount++;
return proxy;
}
}
public void Dispose()
{
IClientChannel channel = (IClientChannel)proxy;
try
{
if (channel.State != CommunicationState.Faulted)
{
channel.Close();
}
else
{
channel.Abort();
}
}
catch (CommunicationException)
{
channel.Abort();
}
catch (TimeoutException)
{
channel.Abort();
}
catch (Exception)
{
channel.Abort();
throw;
}
finally
{
channel = null;
proxy = null;
}
}
}
上記のクラスを使用する方法は次のとおりです。
public static void Login(string userName, string password)
{
using (ProxyWrapper wrapper = new ProxyWrapper())
{
currentSession = wrapper.Proxy.Login(userName, password);
}
}
using
内で ProxyWrapper
クラスのインスタンスを使用する場合、 ProxyWrapper
クラスは IDisposable
を実装するためブロックでは、例外がスローされた場合でも、 Dispose()
メソッドが呼び出されることが保証されています。 Dispose()
メソッドのコードは、プロキシ/チャネルのすべてのケースと状態を処理します。その後、このメソッドにエラー処理/ロギング/イベントデリゲートコードを追加できます。
詳細および上記のコードのより一般的なバージョンについては、次のブログエントリを参照してください。 http://bloggingabout.net/blogs/erwyn/archive/2006/12/09/WCF-Service-Proxy-Helper.aspx
サーバーとの通信エラーをキャッチするには、 .CreateChannel()
への呼び出しをtry ... catchに入れ、CommunicationExceptionやTimeoutExceptionsなどを処理することをお勧めします。
また、一般的なアドバイスの1つとして、ChannelFactoryの作成は非常に高価な操作であるため、これを個別に行い、ChannelFactoryオブジェクトへの参照をどこかに保存することをお勧めします。
ChannelFactoryからチャネルを作成し、場合によっては再作成する必要がある場合は、ChannelFactoryの保存/キャッシュされたインスタンスを何度も何度も使用することなく使用できます。
しかし、それ以外は、ここでかなりうまくやっていると思います!
マーク
ChannelFactoryインスタンスは&quot; Aborted&quot;になります。このタイムアウトが発生したときの状態。例外は、呼び出しで例外が発生したときではなく、_channel.Dispose()を呼び出したときにスローされることがわかります。
どういうわけか、この例外を防ぐ必要があります。最も簡単な方法は、disposeメソッドの内容をtry / catchすることですが、閉じるか破棄する前に_channelインスタンスの状態を確認した方が良いでしょう。
これが、まだ例外を受け取っている理由です-ここでは処理されていません。
更新:
スタックトレースに基づいて、フレームワークがコールバックデリゲートを実行しようとしており、そこで例外を取得しているようです。他の場所で、デリゲートのコンテンツを試行/キャッチしても問題が解決しないと回答しました...内部例外はどうですか? TheException.ToString()の完全な出力は何ですか?
.NET Frameworkをトロールしましたが、実際には(少なくともここにあるスタックトレースで)スレッドプールに戻ることが許可されているため、未処理の例外が発生し、スレッドが停止します。悪化した場合は、いつでもスレッドを引き継ぐことができます...組み込みの非同期パターンを使用するのではなく、独自のスレッドを起動し、内部で同期的にサービスを呼び出します(これはWCF Duplexタイプの通信ではない限り、 tだと思う)。
まだ(もし何かあれば)完全な例外情報が役立つと思う。
ジェフ、EndGet呼び出しの周りにtry / catchブロックを配置しようとするときに、あなたのコードを見せてください。それが例外をキャッチしないと信じるのは難しいです、そして、それを見ることは私が信じるのを助けるでしょう。
また、この実験の一環として、UnhandledExceptionイベントハンドラーを取り除きます。 BrowserHttpRequestが404を認識するとすぐにこの例外をキャッチすると思います。UnhandledExceptionイベントがなければ、EndGetのtry / catchが例外をキャッチします(ある場合)。例外処理プロセスの中間状態を取り上げていると思います。
また、EndGet呼び出しの直後にSystem.Debugger.Breakまたは何かを配置したいのですが。
using
ブロックで呼び出すことができるように ServiceCaller
クラスを設計したと思いますか?
はいの場合、それが問題です。 WCFチャネルクラスは、考えられるすべてのエラー状態に対応する完全に一般的な破棄コードを書くことが非常に困難になるように設計されています。単純な場合は、チャネルクラス自体を破棄しても安全であり、ラッパークラスは必要ありません。
これまでに発見した唯一の安全な方法は、破棄ロジックではなく、呼び出しシーケンス全体をカプセル化することです。
public static void Invoke<TContract>(ChannelFactory<TContract> factory, Action<TContract> action) where TContract : class {
var proxy = (IClientChannel) factory.CreateChannel();
bool success = false;
try {
action((TContract) proxy);
proxy.Close();
success = true;
} finally {
if(!success) {
proxy.Abort();
}
}
}
これは同期呼び出しであることは知っていますが、原理は同じです。 Close
と Abort
のどちらを呼び出すべきかを推測する代わりに、倒れるまでにどの程度の通話を取得できたかに基づいて事前に決定します。
注-実際のチャネルのみがこの特別な処理を必要とします。好きな方法で工場を処分できます。
問題は、サービス呼び出し元を設計した方法にあると思います。
サービス呼び出し元が作成されると、チャネルが開きます。これは、チャネルがタイムアウトする可能性があり、コードにタイムアウトから回復できるものがないことを意味します。
チャンネルの作成と終了をmake callメソッドに移動します。
begin getおよびコールバックのコンテンツの周りでtry catchを実行できます。