Néha kliens alkalmazásokban látni a következő WCF hívási mintát:
using (var client = new FooServiceReference.FooServiceClient("myBinding"))
{
var f = client.GetFoo();
}
A probléma a fenti kóddal az, hogy a kliens minden alkalommal újraépíti a WCF kommunikációt a szerver felé, ami több erőforrásba kerül, mintha egy már megnyitott proxy-n hívná meg a szervert.
Fontos az is, hogy a proxy objektum Fault állapotba kerülhet akkor, ha olyan katasztrofális hiba keletkezett a kliens-szerver kommunikáció közben, melynél nem folytatható tovább a kommunikáció.
Példa a Fault állapot bekövetkeztére:
- reliable session használatakor a hívás timeout-ra fut,
- a szerver SOAP Fault üzenetet küld vissza (kivételt dobott a szerver),
- a kommunikáció iniciliazálásakor az ICommunicationObject.Open() hívás nem sikerül,
- session alapú kapcsolatnál olyan protokoll vagy szerver hiba keletkezik, amely miatt a session érvénytelen lesz.
Ha a proxy példány Fault állapotba kerül, akkor nem lehet tovább használni, nem szabad Close()-t hívni rajta (kivételt dob), és az Abort() meghívásával kell felszabadítani az általa lefoglalt erőforrásokat.
A fenti minta így hibásan viselkedik a Fault állapot bekövetkeztekor, mert a proxy-n hívott Dispose() a Close()-t hívja meg, ami pedig Fault állapotban a Dispose() elszállását fogja okozni, tehát egy kommunikációs kivétel feldolgozása közben keletkezni fog egy másik, az eredeti hibát elfedő kivétel. Erre a hibára remek körbebástyázós megoldást találtak ki Redmondban:
Avoiding Problems with the Using Statement
AnswerJuly CTP – Guidance on factory close and message faults
Csak a try-catch beágyazást ipari méretben alkalmazva spagetti kódot fog eredményezni.
A hatékonyság miatt az is kívánatos lenne, ha az alkalmazás az induláskor hozná létre és inicializálná a proxy objektumokot, ezt a meglévő proxy-t használná az egész élete alatt, és ha az Faulted állapotva kerülne, akkor automatikusa zárná a proxy-t, és nyitná az újat.
A problémát elég jól részletezi a következő két cikk:
Performance Improvement for WCF Client Proxy Creation in .NET 3.5 and Best Practices
WCF Guidance for WPF Developers
Ezek után mindenki szépen neki is foghat a saját barkács proxy kezelőjének megírásához:
1) WCF Client Proxy IDisposable – Generic WCF Service Proxy
A megoldás a Fault állapotba kerülést jól kezeli, de minden hívásnál új Channel-t és ChannelFactory-t hoz létre, ami viszont a sebességnek nem tesz jót.
2. Exception Handling WCF Proxy Generator
Egy Visual Studio extension, melyet Michele Leroux Bustamante fejlesztett ki.
Jellemzők:
- Visual Studio 2008 alá készült az extension
- az extension a ExceptionHandlingProxyBase ősből leszármaztatott proxy osztályokat gyárt. A ExceptionHandlingProxyBase a hölgy saját fejlesztése, egy általános kliens, gyakorlatilag egy alapoktól újraírt ClientBase-nek felel meg.
A megoldás működik, precíz, és látszik a befektetett munka, de mostanában a csapból is a kódgenerátorok folynak, és a plusz bonyolultságtól meg a plusz macerától idegbajt kapok.
3. Going Proxy-less – The WCF Proxy Factory
Nem tűnik elterjedt, vagy kipróbált dolognak.
4. Christian Weyer elődása és példaprogramjai
TechDays Sweden 2009: Presentation slides and demos for ‘WCF Tips & Tricks” session
Az előadás fóliái jók, viszont a példaprogramok a szerző Thinktecture.ServiceModel.dll osztálykönyvtárát használják, melynek nincs forráskódja, és maga az assembly is csak demonstrációs célból van publikálva.
5. WCF Contrib
A WCF Contrib osztálykönyvtárat Amir Zuker készítette, gyakorlatilag ez is egy saját WCF kliens implementáció, de vannak szerver oldali kiegészítései is. Az előző mellett ez tűnik a legátfogóbb megoldásnak, példaprogamok vannak, viszont dokumentáció kevés. Támogatja az aszinkron hívásokat is.
6. Csak szinkron hívásokra egy egyszerűbb megoldás
Először is a ClientBase-t meg kell patkolni egy kicsit:
namespace WcfTools
{
using System.ServiceModel;
internal class ClientBaseEx<TInterface> : ClientBase<TInterface>
where TInterface : class
{
public ClientBaseEx(string endpointConfigurationName)
: base(endpointConfigurationName)
{
}
public ClientBaseEx(string endpointConfigurationName, string remoteAddress)
: base(endpointConfigurationName, remoteAddress)
{
}
/// <summary>
/// Protected field published with internal access
/// </summary>
internal TInterface Proxy
{
get { return Channel; }
}
protected override TInterface CreateChannel()
{
return base.CreateChannel();
}
}
}
Utána készül egy wrapper, mely a ClientBaseEx segítségével hívja meg a szervert:
namespace WcfTools
{
using System;
using System.Runtime.InteropServices;
using System.Security;
using System.ServiceModel;
using log4net;
public class SafeClient<TInterface> where TInterface : class
{
protected static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
#region Static singleton instance
private static SafeClient<TInterface> instance = new SafeClient<TInterface>();
public static SafeClient<TInterface> Instance
{
get { return SafeClient<TInterface>.instance; }
}
#endregion
#region Private variables
private ClientBaseEx<TInterface> clientProxy = null;
private object lockObj = new object();
/// <summary>
/// Csak akkor ad a Proxy mező értéket, ha Connect() meghívásra kerül.
/// </summary>
private bool isConnected = false;
#endregion
#region Public properties
public bool BasicAuthenticationEnabled { get; set; }
public string BasicAuthenticationUserName { get; set; }
public SecureString BasicAuthenticationPassword { get; set; }
public string EndpointConfigurationName { get; set; }
public string RemoteAddressDirectoryUrl { get; set; }
public string RemoteAddressServiceFile { get; set; }
/// <summary>
/// Webszolgáltatás interfésze
/// </summary>
public TInterface Proxy
{
get
{
if (log.IsDebugEnabled) log.Debug("Start");
TInterface result = null;
lock (lockObj)
{
if (isConnected)
{
CheckClientIsOpened();
result = clientProxy.Proxy;
}
else
{
if (log.IsDebugEnabled) log.Debug("Not connected!");
}
}
if (log.IsDebugEnabled) log.Debug("End");
return result;
}
}
#endregion
#region Public functions
/// <summary>
/// Kapcsolódás
/// </summary>
public void Connect()
{
if (log.IsDebugEnabled) log.Debug("Start");
lock (lockObj)
{
// a proxy ne a régi legyen, mert az autentikációs adatok lehet változtak
DisconnectInternal();
CheckClientIsOpened();
isConnected = true;
}
if (log.IsDebugEnabled) log.Debug("End");
}
/// <summary>
/// Kapcsolat bontása
/// </summary>
public void Disconnect()
{
if (log.IsDebugEnabled) log.Debug("Start");
lock (lockObj)
{
DisconnectInternal();
}
if (log.IsDebugEnabled) log.Debug("End");
}
#endregion
#region Private functions
/// <summary>
/// Kapcsolat ellenőrzése: szükség esetén bontás, és újranyitás
/// </summary>
private void CheckClientIsOpened()
{
if (clientProxy != null)
{
if (clientProxy.State == CommunicationState.Faulted)
{
if (log.IsDebugEnabled) log.Debug("State == CommunicationState.Faulted, aborting...");
try
{
clientProxy.Abort();
}
finally
{
clientProxy = null;
}
}
else if (clientProxy.State == CommunicationState.Closed)
{
if (log.IsDebugEnabled) log.Debug("State == CommunicationState.Closed.");
clientProxy = null;
}
}
if (clientProxy == null)
{
if (log.IsDebugEnabled) log.Debug("Creating WCF proxy....");
// a könyvtár jelet a végére tesszük, ha nincs ott, mert különben az sikertelen lesz az összefűzés
this.RemoteAddressDirectoryUrl = CheckTailSlash(this.RemoteAddressDirectoryUrl);
Uri uriMain = new Uri(this.RemoteAddressDirectoryUrl);
Uri uriChild = new Uri(uriMain, this.RemoteAddressServiceFile);
if (log.IsDebugEnabled) log.DebugFormat("Url: '{0}'", uriChild);
clientProxy = new ClientBaseEx<TInterface>(this.EndpointConfigurationName, uriChild.ToString());
if (BasicAuthenticationEnabled)
{
clientProxy.ClientCredentials.UserName.UserName = this.BasicAuthenticationUserName;
clientProxy.ClientCredentials.UserName.Password = SecureStringToString(this.BasicAuthenticationPassword);
}
clientProxy.Open();
if (log.IsDebugEnabled) log.Debug("Proxy OK.");
}
}
private static string CheckTailSlash(string data)
{
string result = data.Trim();
if (result.Length > 0)
{
char last = result[result.Length - 1];
if (last != '/')
{
result += "/";
}
}
return result;
}
private void DisconnectInternal()
{
try
{
if (clientProxy != null)
{
try
{
if (clientProxy.State == CommunicationState.Faulted)
{
clientProxy.Abort();
}
else
{
clientProxy.Close();
}
}
finally
{
clientProxy = null;
}
}
}
finally
{ // mindegy mi történt, a kapcsolat bontva van
isConnected = false;
}
}
private static string SecureStringToString(SecureString value)
{
IntPtr bstr = Marshal.SecureStringToBSTR(value);
try
{
return Marshal.PtrToStringBSTR(bstr);
}
finally
{
Marshal.FreeBSTR(bstr);
}
}
#endregion
}
}
Példa a használatra:
// starting fat client
var client = new SafeClient<FooServiceReference.IFooService>();
client.EndpointConfigurationName = "myBinding";
client.RemoteAddressDirectoryUrl = "http://localhost:52008/";
client.RemoteAddressServiceFile = "FooService.svc";
client.Connect();
...
// fat client using same SafeClient<T> instance
client.Proxy.GetFoo();
try
{
client.Proxy.GetFooTimeout();
}
catch (Exception ex)
{
log.Error(ex);
}
try
{
client.Proxy.GetFooFaulted();
}
catch (Exception ex)
{
log.Error(ex);
}
client.Proxy.GetFoo();
...
// fat client stopping
client.Disconnect();
A vastag kliensnek annyi dolga van, hogy induláskor építse fel a kapcsolatot, és utána a SafeClient példányt őrizgesse, és rajta keresztül hívja a szervert.
A SafeClient rendelkezik egy Instance nevű singleton példánnyal is, tehát akár saját példány nélkül is lehet használni.
Mikor érdemes külön példányokat használni:
- ha ugyanazt az interfészt más-más autentikációs beállításokkal kell hívni, mert ilyenkor nem lenne szerencsés, ha keverednének a szerveren az azonosítási információk.
- többszálú működésnél, ha két szál egyidőben hívja a szervert, és az egyik Fault állapotba billenti a proxy-t, akkor az a másik, még ugyanazzal a proxy példánnyal dolgozó szálra is hatással lesz. Ha ez zavaró, akkor szálanként kell példányokat használni, és valami pool-t kell képezni.