|
| » myCSharp.de Diskussionsforum |
|
|
|
|
Autor
 |
|
gfoidl
myCSharp.de-Team (Moderation)
Dabei seit: 07.06.2009
Beiträge: 5.362
Entwicklungsumgebung: VS 2010 sup{Editionen} Herkunft: Waidring / Tirol
|
|
Voraussetzung:
.net 4.0 oder Kompatible
Beschreibung:
Die Basisklasse ProxyBase<TChannel> stellt eine generische Basisimplementierung bereit, die vewendet werden kann um WCF-Clientobjekte zu erstellen, welche WCF-Dienste aufrufen können.
Es gibt zwar in .net die ClientBase(TChannel)-Klasse diese hat aber beim cachen von ChannelFactories Einschränkungen. Für detailliertere Information sei auf Performance Improvement for WCF Client Proxy Creation in .NET 3.5 and Best Practices verwiesen. Die hier vorgestellte Basisklasse kann natürlich angepasst werden um auf eigenen spezielle Bedürfnisse besser zu passen. ZB kann das Caching angepasst werden indem eine andere IFactoryCache-Instanz gesetzt wird.
Hier werden ChannelFactories auf AppDomain-Ebene gecached. Dies verkürzt die Erstellungszeit eines Proxies immens. Zusätzlich wird das Proxy lazy erzeugt, also erst beim tatsächlichen Zugriff.
Im Standardcache der dabei ist werden zwei ChannelFactories als gleich erachtet wenn die Endpunkt-Adresse und das Binding übereinstimmen sind.
Ich finde außerdem dass sich mit meiner Basisklasse transparenter arbeiten lässt - im Vergleich zu ClientBase<TChannel> :-)
Die ClientBase<TChannel> wird übrigens bei der Erstellung eines Clients mittels svcutil bzw. Add Service Reference verwendet.
Nachfolgend eine kurze Demonstration der Verwendung für folgenden Service-Contract:
C#-Code (ServiceContract): |
[ServiceContract]
public interface IAnswerService
{
[OperationContract]
[FaultContract(typeof(Exception))]
string GetUltimativeAnswer(string query);
}
|
Somit kann die Client-Klasse für diesen Vertrag wie folgt definiert werden:
C#-Code (Client): |
public interface IGreetingsServiceClient : IAnswerService, ICommunicationObject { }
public class AnswerServiceClient : ProxyBase<IGreetingsServiceClient>, IAnswerService
{
public AnswerServiceClient(string endpointAddress)
: base(endpointAddress) { }
public AnswerServiceClient(string endpointAddress, Bindings bindings)
: base(endpointAddress, bindings) { }
#region IAnswerService Members
public string GetUltimativeAnswer(string query)
{
try
{
return this.Proxy.GetUltimativeAnswer(query);
}
catch (FaultException<Exception> ex)
{
throw ex.FromFaultException();
}
}
#endregion
}
|
Das ist der ganze Aufwand der nötig ist um ein WCF-Client-Objekt zu erstellen. Für das komfortablere Exception-Handling sind in der Assembly auch Erweiterungsmethoden vorhanden - im obigen Beispiel die FromFaultException-Erweiterung. Das "Gegenstück" ist "ToFaultException". Für nähere Infos zum Thema Exception-Handling in WCF siehe Specifying and Handling Faults in Contracts and Services.
Dieser Client kann dann in einem Programm ganz ".net like" verwendet werden:
C#-Code (Verwendung): |
using (AnswerServiceClient client = new AnswerServiceClient(
"net.pipe:
Bindings.NamedPipes))
{
string query = "What's the answer?";
string answer = client.GetUltimativeAnswer(query);
Console.WriteLine(answer);
}
|
Die Enum Bindings ist im Projekt enthalten.
Anhang: Die DLL sowie das VS 2010 Projekt für den gesamten Code (inkl. Demo). XML-Kommentare sind keine vorhanden, aber mehr als Konstruktorüberladungen und zwei Eigenschaften (Proxy und ChannelFactoryCache) gibt es nicht - somit sollte die Verwendung auch ohne Kommentare klar und möglich sein ;-)
Wenn jemand Änderungen, Ergänzungen, Anpassungen, etc. hat wäre es nett wenn diese hier als Antwort veröffentlicht werden könnten.
Eine leichte Änderung empfehle ich jedoch jedem: Anstatt der Enum Bindings kann das verwendete Bindung auch per Konvention ermittelt/erstellt werden. D.h. aus der EndpointAdress kann die Binding abgeleitet werden und somit erübrigt sich das Enum.
mfG Gü
Schlagwörter: WCF, Proxy, Client, WCF-Client
|
|
17.06.2011 17:58
|
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
| Zwischen diesen beiden Beiträgen liegen mehr als 3 Monate. |
gfoidl
myCSharp.de-Team (Moderation)
Dabei seit: 07.06.2009
Beiträge: 5.362
Entwicklungsumgebung: VS 2010 sup{Editionen} Herkunft: Waidring / Tirol
Themenstarter
|
|
Hallo zusammen,
die Proxies sind nur für das Erstellen der Channel und der ICommunication-Objekte zuständig. D.h. es lassen sich auch asynchrone Vorgänge damit bewerkstelligen, da dies eben unabhängig vom Proxy an sich ist.
Mit .net 4.0 und speziell mit den Tasks lassen sich asynchrone Methoden für WCF sehr einfach bewerkstelligen, da Task die Schnittstelle IAsyncResult implementiert. Auf eine allgemeine Übersicht über asynchrone Vorgänge in WCF verzichte ich und verweise auf Synchronous and Asynchronous Operations. Hier weise ich nur darauf hin, dass es verschiedene Möglichkeiten der Asynchronizität in WCF gibt: - Server asynchron und Client asynchron
- Server asynchron und Client synchron
- Server synchron und Client asynchron
Der Server sollte vorzugsweise dann asynchron laufen, wenn dieser als "Zwischenschicht" dient, z.B. wenn die Servicemethode einen weiteren Service, etc. aufruft. Dies erhöht den Durchsatzt und die Skalierbarkeit.
Der Client sollte dann asynchron laufen wenn z.B. [FAQ] Warum blockiert mein GUI? ein Problem sein könnte.
Die Implementierung eines asynchronen Vorgangs zeige ich an einem Beispiel. Angenommen wir haben folgenden ServiceContract:
C#-Code (ServiceContract): |
using System;
using System.ServiceModel;
namespace Contracts
{
[ServiceContract]
public interface IMyService
{
[OperationContract]
int Add(int a, int b);
[OperationContract(AsyncPattern = true)]
IAsyncResult BeginAddAsync(int a, int b, AsyncCallback callback, object state);
int EndAddAsync(IAsyncResult ar);
}
}
|
Der Service kann dann wie folgt implementiert werden.
C#-Code (Service): |
using System;
using System.Threading.Tasks;
using Contracts;
using gfoidl.Service.Utils;
namespace Service
{
public class MyService : IMyService
{
#region IMyService Members
public int Add(int a, int b)
{
return a + b;
}
public IAsyncResult BeginAddAsync(int a, int b, AsyncCallback callback, object state)
{
var task = Task.Factory.StartNew(() => this.Add(a, b));
task.ContinueWith(
t => Console.WriteLine(t.Exception),
TaskContinuationOptions.OnlyOnFaulted);
return task.ToApm(callback, state);
}
public int EndAddAsync(IAsyncResult ar)
{
return ar.End<int>();
}
#endregion
}
}
|
Und der Client schaut so aus:
C#-Code (Client): |
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Contracts;
using System.ServiceModel;
using gfoidl.Service.Utils;
using System.Threading.Tasks;
namespace Client
{
public interface IMyServiceClient : IMyService
{
Task<int> AddAsync(int a, int b);
}
public interface IMyServiceClientChannel : IMyServiceClient, ICommunicationObject { }
public class MyServiceClient : ProxyBase<IMyServiceClientChannel>, IMyServiceClient
{
public MyServiceClient(string endpointAddress)
: base(endpointAddress) { }
public MyServiceClient(string endpointAddress, Bindings bindings)
: base(endpointAddress, bindings) { }
#region IMyService Members
public int Add(int a, int b)
{
return this.Proxy.Add(a, b);
}
IAsyncResult IMyService.BeginAddAsync(int a, int b, AsyncCallback callback, object state)
{
return this.Proxy.BeginAddAsync(a, b, callback, state);
}
int IMyService.EndAddAsync(IAsyncResult ar)
{
return this.Proxy.EndAddAsync(ar);
}
public Task<int> AddAsync(int a, int b)
{
return TaskHelper.FromAsync(
(this as IMyService).BeginAddAsync,
(this as IMyService).EndAddAsync,
a, b,
this.Proxy);
}
#endregion
}
}
|
Damit das Ganze elegant funktioniert sind ein paar Erweiterungsmethoden notwendig, die dem obigen Projekt hinzugefügt werden können. Ich poste hier die wiederverwendbaren Klassen, so wie ich sie (od. fast so) produktiv verwende, und diese beinhalten auch Überladungen für die gängigen Anwendungsfälle.
Zuerst werden Erweiterungsmethoden für Tasks benötigt, um das asynchrone Pattern (Begin/End, auch als APM...asynchronous programming model bezeichnet) mit den Task kompatibel zu machen:
C#-Code: |
using System;
using System.Diagnostics.Contracts;
using System.Threading.Tasks;
namespace System.Threading.Tasks
{
public static partial class TaskExtensions
{
public static Task ToApm(
this Task task,
AsyncCallback callback,
object state)
{
Contract.Requires(task != null);
Contract.Requires(callback != null);
Contract.Ensures(Contract.Result<Task>() != null);
if (task.AsyncState == state)
{
if (callback != null)
task.ContinueWith(
t => callback(t),
TaskContinuationOptions.ExecuteSynchronously);
return task;
}
var tcs = new TaskCompletionSource<object>(state);
task.ContinueWith(t =>
{
if (t.IsFaulted) tcs.TrySetException(t.Exception.InnerExceptions);
else if (t.IsCanceled) tcs.TrySetCanceled();
else tcs.TrySetResult(null);
if (callback != null) callback(tcs.Task);
}, TaskContinuationOptions.ExecuteSynchronously);
return tcs.Task;
}
public static Task<TResult> ToApm<TResult>(
this Task<TResult> task,
AsyncCallback callback,
object state)
{
Contract.Requires(task != null);
Contract.Requires(callback != null);
Contract.Ensures(Contract.Result<Task<TResult>>() != null);
if (task.AsyncState == state)
{
if (callback != null)
task.ContinueWith(
t => callback(t),
TaskContinuationOptions.ExecuteSynchronously);
return task;
}
var tcs = new TaskCompletionSource<TResult>(state);
task.ContinueWith(t =>
{
if (t.IsFaulted) tcs.TrySetException(t.Exception.InnerExceptions);
else if (t.IsCanceled) tcs.TrySetCanceled();
else tcs.TrySetResult(t.Result);
if (callback != null) callback(tcs.Task);
}, TaskContinuationOptions.ExecuteSynchronously);
return tcs.Task;
}
}
}
|
Genauso gibts eine Erweiterungsmethode um vom Task wieder zum APM zu gelangen:
C#-Code: |
using System;
using System.Diagnostics.Contracts;
using System.ServiceModel;
using System.Threading.Tasks;
namespace gfoidl.Service.Utils
{
public static class IAsyncResulstExtensions
{
public static T End<T>(this IAsyncResult ar)
{
if (!(ar is Task<T>)) throw new InvalidOperationException("Only Task<T> can be handeld.");
Contract.EndContractBlock();
var task = ar as Task<T>;
try
{
return task.Result;
}
catch (AggregateException aex)
{
throw EnsureFaultException(aex);
}
}
private static FaultException EnsureFaultException(AggregateException aex)
{
Exception ex = aex.GetRootException();
FaultException fex = ex as FaultException;
if (fex == null)
{
Type genericType = typeof(FaultException<>);
Type[] typeArgs = { ex.GetType() };
Type type = genericType.MakeGenericType(typeArgs);
fex = Activator.CreateInstance(type, ex, ex.Message) as FaultException;
}
return fex;
}
}
}
|
Hierzu ist es auch notwendig die ExceptionExtensions, die im obigen Projekt dabei sind, zu erweitern. Der komplette Code für diese Klasse ist nun wie folgt:
C#-Code: |
using System;
using System.Diagnostics.Contracts;
using System.ServiceModel;
using System.Threading.Tasks;
namespace gfoidl.Service.Utils
{
public static class ExceptionExtensions
{
public static FaultException<T> ToFaultException<T>(this T ex)
where T : Exception
{
Contract.Requires(ex != null);
Contract.Ensures(Contract.Result<FaultException<T>>() != null);
return new FaultException<T>(ex, ex.Message);
}
public static FaultException<T> ToFaultException<T>(this Task task)
where T : Exception
{
Contract.Requires(task != null);
Contract.Ensures(Contract.Result<FaultException<T>>() != null);
return (task.Exception.InnerException as T).ToFaultException();
}
public static T FromFaultException<T>(this FaultException<T> ex)
where T : Exception
{
Contract.Requires(ex != null);
Contract.Ensures(Contract.Result<T>() != null);
return ex.Detail;
}
public static Exception GetRootException(this Exception ex)
{
Contract.Requires(ex != null);
return GetRootExceptionCore(ex);
}
private static Exception GetRootExceptionCore(Exception ex)
{
if (ex is AggregateException) return GetRootExceptionCore(ex.InnerException);
return ex;
}
}
}
|
Für den Client brauchen wir auch noch Hilfs-Methoden um aus dem APM einen Task zu erzeugen, damit die Client-Verwendung ganz transparent wird. Dies geschieht in folgender Hilfsklasse:
C#-Code: |
using System;
using System.Diagnostics.Contracts;
using System.ServiceModel;
using System.Threading.Tasks;
namespace gfoidl.Service.Utils
{
public static class TaskHelper
{
public static Task<TResult> FromAsync<TArg1, TResult>(
Func<TArg1, AsyncCallback, object, IAsyncResult> beginMethod,
Func<IAsyncResult, TResult> endMethod,
TArg1 arg1,
object state)
{
Contract.Requires(beginMethod != null);
Contract.Requires(endMethod != null);
TaskCompletionSource<TResult> tcs = new TaskCompletionSource<TResult>(state);
AsyncCallback callback = ar => FromAsyncCore(tcs, ar, endMethod);
beginMethod(arg1, callback, state);
return tcs.Task;
}
public static Task<TResult> FromAsync<TArg1, TArg2, TResult>(
Func<TArg1, TArg2, AsyncCallback, object, IAsyncResult> beginMethod,
Func<IAsyncResult, TResult> endMethod,
TArg1 arg1,
TArg2 arg2,
object state)
{
Contract.Requires(beginMethod != null);
Contract.Requires(endMethod != null);
TaskCompletionSource<TResult> tcs = new TaskCompletionSource<TResult>(state);
AsyncCallback callback = ar => FromAsyncCore(tcs, ar, endMethod);
beginMethod(arg1, arg2, callback, state);
return tcs.Task;
}
public static Task<TResult> FromAsync<TArg1, TArg2, TArg3, TResult>(
Func<TArg1, TArg2, TArg3, AsyncCallback, object, IAsyncResult> beginMethod,
Func<IAsyncResult, TResult> endMethod,
TArg1 arg1,
TArg2 arg2,
TArg3 arg3,
object state)
{
Contract.Requires(beginMethod != null);
Contract.Requires(endMethod != null);
TaskCompletionSource<TResult> tcs = new TaskCompletionSource<TResult>(state);
AsyncCallback callback = ar => FromAsyncCore(tcs, ar, endMethod);
beginMethod(arg1, arg2, arg3, callback, state);
return tcs.Task;
}
private static void FromAsyncCore<TResult>(
TaskCompletionSource<TResult> tcs,
IAsyncResult ar,
Func<IAsyncResult, TResult> endMethod)
{
try
{
tcs.SetResult(endMethod(ar));
}
catch (FaultException fex)
{
Exception ex = EnsureException(fex);
tcs.SetException(ex);
}
catch (Exception ex)
{
tcs.SetException(ex);
}
}
private static Exception EnsureException(FaultException fex)
{
Type exType = fex.GetType();
object ex = exType.GetProperty("Detail").GetValue(fex, null);
return ex as Exception;
}
}
}
|
Im obigen Beispiel ist die Methodenpaarung Server - Client so, dass nur synchron-synchron od. nur asynchron-asynchron gearbeitet werden kann. Dieses Verhalten kann geändert werden, siehe eingangs erwähnte Kombinationen, indem die im OperationContract mit der Action- und ReplyAction-Eigenschaft die jeweilige Adresse für die Methode angegeben wird. Dies kann sowohl beim Client als auch beim Server erfolgen. Aber dies ist mMn eher ein Sonderfall.
mfG Gü
|
|
07.10.2011 16:58
|
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
| Zwischen diesen beiden Beiträgen liegt mehr als ein Monat. |
Campy
myCSharp.de-Mitglied
Dabei seit: 02.04.2008
Beiträge: 319
|
|
Hallo gfoidl,
was mir beim Arbeiten mit dem Proxy noch aufgefallen ist:
Die Methoden für Open() und Close() fehlen beim Proxy komplett => Wenn das Programm lange läuft und der Timeout für den Proxy abgelaufen ist, dann bekomm ich ne TimeOutException. Kann ich die Methoden noch irgendwie "einbauen" oder wie machst du das?
|
|
01.12.2011 15:14
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
gfoidl
myCSharp.de-Team (Moderation)
Dabei seit: 07.06.2009
Beiträge: 5.362
Entwicklungsumgebung: VS 2010 sup{Editionen} Herkunft: Waidring / Tirol
Themenstarter
|
|
Hallo Campy,
ich hoffe dass ich jetzt diese Implementierung richtig im Kopf habe, da ich eine leicht modifiziert verwende, aber das Grundgerüst ist dennoch das Gleiche.
| Zitat: |
| Die Methoden für Open() und Close() fehlen beim Proxy komplett |
Als explizite Methoden sind diese (bewusst) nicht vorhanden. Open und Close wird intern aufgerufen. Für die Verwendung halte ich es so, dass der Proxy für jede Verwendung neu erstellt wird und dann gleich wieder verworfen wird. Daher eignet sich eine Verwendung im using-Block besonders gut.
Laufzeittechnisch hat das keine Nachteile, denn alles schwergewichtige wird gecached, etnweder von der WCF selbst od. von der Basisklasse (und deren Helfer).
Die Verwendung ist also
C#-Code: |
using (MyProxy proxy = new MyProxy(...))
{
int sum = proxy.Add(40, 2);
}
MyProxy proxy = new MyProxy(...);
Task<int> sum = proxy.AddAsync(40, 2);
sum.ContinueWith(t => proxy.Dispose());
sum.ContinueWith(t => _logger.LogException(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
sum.ContinueWith(t => UseResult(t.Result), TaskContinuationOptions.OnlyOnRunToCompletion);
|
Ein ständiges offenhalten des Proxies ist nicht gedacht. Wenn du das jedoch willst, so müsstest du im Konstruktor des Proxies den Lazy-Init-Teil weglassen und stattdessen dies in einer zu hinzufügenden Open-Methode implementieren.
mfG Gü
|
|
01.12.2011 15:26
|
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
Campy
myCSharp.de-Mitglied
Dabei seit: 02.04.2008
Beiträge: 319
|
|
Alles klar, das ist auch wahrscheinlich das sauberste und wenn die wichtigsten Dinge eh gecached werden ist das auch kein Problem (außer bissl Umbauarbeit für mich).
Bei mir sieht das dann folgendermaßen aus:
C#-Code: |
using (XXXServiceClient customerService = new XXXServiceClient("NetTcpBinding_IXXService"))
{
...
}
|
Hast du noch eine Idee wie ich mir das mit dem Bindingnamen vereinfachen könnte? Sonst muss ich das an jeder Stelle der Verwendung übergeben.
Vielen vielen Dank!
|
|
01.12.2011 15:35
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
gfoidl
myCSharp.de-Team (Moderation)
Dabei seit: 07.06.2009
Beiträge: 5.362
Entwicklungsumgebung: VS 2010 sup{Editionen} Herkunft: Waidring / Tirol
Themenstarter
|
|
Hallo Campy,
noch als eher allg. Anmerkung zum Snippet:
Für die Bindungsart hab ich das Enum rausgeworfen, denn anhand der Adresse kann diese ja leicht ermittelt werden. http://... -> BasisHttpBinding, net.tcp://... -> NetTcpBinding, usw. Das ist alles nach Konvention.
| Zitat: |
| Hast du noch eine Idee wie ich mir das mit dem Bindingnamen vereinfachen könnte? |
Mach dir einfach eine Factory, welche den Proxy erstellt:
C#-Code: |
public class MyProxyFactory
{
private readonly string _bindingName;
public MyProxyFactory(string bindingName)
{
if (string.IsNullOrWhitespace(bindingName)) throw new ArgumentNullException("bindingName");
_bindingName = bindingName;
}
public MyProxy Create()
{
}
}
public class Foo
{
private readonly MyProxyFactory _proxyFactory;
using (MyProxy proxy = _proxyFactory.Create())
{
}
}
|
Wobei dei Factory besser die Schnittstelle mit dem ServiceContract zurückgibt, denn somit ist alles gleich testbarer.
mfG Gü
|
|
01.12.2011 15:43
|
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
|
|