Podczas korzystania z WCF najprostszą drogą do wywołania metody udostępnianej przez jakąś usługę jest pozwolenie Visual Studio na wygenerowanie odpowiedniego proxy, stworzenie jego instancji… i już – mamy metody usługi do dyspozycji. Bez wysiłku, bez kodu… bez sensu?
O tym, dlaczego takie podejście NIE jest wyborem słusznym, rozpisywać się nie będę. Zainteresowanych argumentami “przeciw” odsyłam do fajnego artykułu autorstwa Miguela Castro na Code-Magazine: “WCF the Manual Way… the Right Way“. Propozycji alternatyw jest w internecie sporo i mają dużą wartość edukacyjną. Jednak gdy przyszło do prawdziwej zabawy z WCF, rozszerzyłem trochę te rozwiązania. Całość kodu wykorzystującego usługi WCF rozbiłem na dwie części: właściwe Proxy oraz klasę udostępniającą odpowiednie Proxy wedle naszego zapotrzebowania.
Pierwsza część, czyli klasa Proxy eliminująca kod generowany przez Visual Studio, przedstawia się następująco:
1: public class ServiceProxy<T> : ClientBase<T>, IDisposable where T : class 2: { 3: // empty ctor for default config if only one available 4: public ServiceProxy() 5: { 6: } 7: 8: // ctor with config name; other ctors not available to enable ChannelFactory caching 9: public ServiceProxy(string endpointConfigurationName) 10: : base(endpointConfigurationName) 11: { 12: } 13: 14: public T GetChannel() 15: { 16: return base.Channel; 17: } 18: 19: public void Dispose() 20: { 21: try 22: { 23: if (base.Channel != null) 24: { 25: if (base.State != CommunicationState.Faulted) 26: { 27: base.Close(); 28: } 29: else 30: { 31: base.Abort(); 32: } 33: } 34: } 35: catch (CommunicationException) 36: { 37: base.Abort(); 38: } 39: catch (TimeoutException) 40: { 41: base.Abort(); 42: } 43: catch (Exception) 44: { 45: base.Abort(); 46: throw; 47: } 48: } 49: }
Wielkiego odkrycia nie ma tu żadnego. Klasa ta ma właściwie dwa zadania: dać nam dostęp do kanału implementującego komunikację z żądanym serwisem oraz odpowiednią obsługę Dispose(). O problemach z Dispose() można poczytać na MSDN w artykule “Avoiding Problems with the Using Statement“.
Wprowadziłem dość istotną modyfikację w stosunku do fruwających po necie przykładów: ograniczyłem liczbę konstruktorów. Moja klasa Proxy udostępnia tylko dwa konstruktory – domyślny oraz przyjmujący nazwę wpisu z konfiguracji. Powód jest bardzo prosty, choć niekoniecznie każdy musi o takim fakcie wiedzieć: te konstruktory umożliwiają cache’owanie przez WCF raz utworzonych obiektów ChannelFactory. Dzięki temu dalsze instancjonowanie samych Proxy jest bardzo lekkim procesem. Więcej o tym na blogu Wenlong Dong: “Performance Improvement for WCF Client Proxy Creation in .NET 3.5 and Best Practices“.
Przedstawiona powyżej klasa może znajdować się w jakimś współdzielonym assembly, dostępna dla każdej aplikacji klienckiej.
Ciągłe pisanie takiego kodu nie do końca mi się jednak podobało:
1: using (var proxy = new ServiceProxy<IMyService>()) 2: { 3: proxy.GetChannel().MyMethod(); 4: }
Dlatego też bezpośrednio w aplikacji klienckiej umieszczam małego helpera, który pozwala skrócić te instrukcje do takich wywołań:
1: ServiceProxyProvider<IMyService>.Invoke(x => x.MyAction()); 2: string result = ServiceProxyProvider<IMyService>.Invoke(x => x.MyFunction());
Oprócz oczywistej korzyści, jaką jest mniejsza ilość kodu (choć można by dyskutować czy w tym konkretnym przypadku to faktycznie tak wielka korzyść), otrzymujemy jeszcze jeden bonus: możemy w jednym miejscu zarządzać każdym wywołaniem zdalnej usługi. Błyskawicznie przychodzącym na myśl wykorzystaniem tej zalety jest wrzucenie tu ustawiania parametrów uwierzytelniania (jakie fancy określenie na login i hasło:) ). W poniższej implementacji, pochodzącej z aplikacji WinForms, zrobiłem jednak coś innego:
1: public class ServiceProxyProvider<TService> where TService : class 2: { 3: public static void Invoke(Action<TService> operation) 4: { 5: using (var proxy = new ServiceProxy<TService>()) 6: { 7: WaitingCursor(() => 8: operation(proxy.GetChannel()) 9: ); 10: } 11: } 12: 13: public static TResult Invoke<TResult>(Func<TService, TResult> operation) 14: { 15: TResult result = default(TResult); 16: 17: Invoke(x => 18: { 19: result = operation(x); 20: }); 21: 22: return result; 23: } 24: 25: private static void WaitingCursor(Action operation) 26: { 27: Form activeForm = Form.ActiveForm; 28: 29: if (activeForm != null) 30: { 31: activeForm.Cursor = Cursors.WaitCursor; 32: } 33: 34: try 35: { 36: operation(); 37: } 38: finally 39: { 40: if (activeForm != null) 41: activeForm.Cursor = Cursors.Default; 42: } 43: } 44: }
Każde wywołanie skutkuje zmianą kursora na klepsydrę bądź Viściano-Siódemkowe Błękitne Koło Zagłady, dzięki czemu użytkownik wie, że “coś się dzieje i ma być cierpliwy“.
Macie jakieś ciekawe doświadczenia w tym zakresie? Uwagi, sugestie, dotyczące przedstawionego rozwiązania?
Wkrótce powinien pojawić się dłuższy post traktujący o WCF, gdzie na żywym zarodku aplikacji będzie można zobaczyć to w praktyce. W tymczasem – borem lasem!
Dobry kawałek kodu. Zmienił bym tylko mały drobiazg: if (base.Channel == null) { return; } i wyrzucił bym to poza blok try/catch.