WCF客户端“using”块问题的最佳解决方法是什么?

423

我喜欢在using块内实例化我的WCF服务客户端,因为这基本上是使用实现IDisposable资源的标准方式:

using (var client = new SomeWCFServiceClient()) 
{
    //Do something with the client 
}

但是,正如这篇MSDN文章中所指出的那样,将WCF客户端包装在using块中可能会掩盖任何导致客户端处于故障状态的错误(例如超时或通信问题)。长话短说,当调用Dispose()时,客户端的Close()方法会触发,但由于它处于故障状态,因此会抛出一个错误。然后第二个异常掩盖了原始异常。不好。

MSDN文章中建议的解决方法是完全避免使用using块,并改为实例化您的客户端并像这样使用它们:

try
{
    ...
    client.Close();
}
catch (CommunicationException e)
{
    ...
    client.Abort();
}
catch (TimeoutException e)
{
    ...
    client.Abort();
}
catch (Exception e)
{
    ...
    client.Abort();
    throw;
}

相比于using块,我认为那很丑陋。每次需要客户端时都需要编写大量的代码。

幸运的是,我发现了一些其他的解决方法,比如在(现已停止维护的)IServiceOriented博客上发现的这个方法。你可以从以下开始:

public delegate void UseServiceDelegate<T>(T proxy); 

public static class Service<T> 
{ 
    public static ChannelFactory<T> _channelFactory = new ChannelFactory<T>(""); 
    
    public static void Use(UseServiceDelegate<T> codeBlock) 
    { 
        IClientChannel proxy = (IClientChannel)_channelFactory.CreateChannel(); 
        bool success = false; 
        try 
        { 
            codeBlock((T)proxy); 
            proxy.Close(); 
            success = true; 
        } 
        finally 
        { 
            if (!success) 
            { 
                proxy.Abort(); 
            } 
        } 
     } 
} 

这样就可以实现:

Service<IOrderService>.Use(orderService => 
{ 
    orderService.PlaceOrder(request); 
}); 

这并不错,但我认为它不如using块表达清晰易懂。

我目前尝试使用的解决方法是我在blog.davidbarret.net上读到的。基本上,您需要在使用客户端的Dispose()方法时覆盖它。例如:

public partial class SomeWCFServiceClient : IDisposable
{
    void IDisposable.Dispose() 
    {
        if (this.State == CommunicationState.Faulted) 
        {
            this.Abort();
        } 
        else 
        {
            this.Close();
        }
    }
}

看起来这可以允许再次使用 using 块,而不会掩盖故障状态异常。

所以,使用这些解决方法时还有其他需要注意的地方吗?有人想出了更好的解决办法吗?


44
最后一个(检查 this.State 的)是一个竞争条件;当您检查布尔值时它可能不会出现故障,但是在调用 Close() 时可能会出现故障。 - Brian
16
你读取了通道的状态,发现它并没有出错。在调用Close()方法之前,通道突然出错了,并抛出异常。游戏结束。 - Brian
4
时间流逝。这段时间可能很短,但从检查通道状态到请求关闭通道之间的时间段内,通道的状态可能会发生变化。为了保持原意,我尽力使翻译通俗易懂。 - Eric King
9
我建议使用 Action<T> 替代 UseServiceDelegate<T>,这是一个次要的更改。 - hIpPy
2
我真的不喜欢这个静态助手 Service<T>,因为它会使单元测试变得复杂(就像大多数静态东西一样)。我更希望它是非静态的,这样它就可以被注入到正在使用它的类中。 - Fabio Marreco
显示剩余6条评论
26个回答

4
如果您不需要IoC或者使用自动生成的客户端(服务引用),那么您可以简单地使用一个包装器来管理关闭,并让GC在安全状态下接管clientbase,这样就不会抛出任何异常。GC将调用serviceclient中的Dispose,这将调用Close。由于它已经关闭,所以不会造成任何损害。我在生产代码中使用这个没有问题。
public class AutoCloseWcf : IDisposable
{

    private ICommunicationObject CommunicationObject;

    public AutoDisconnect(ICommunicationObject CommunicationObject)
    {
        this.CommunicationObject = CommunicationObject;
    }

    public void Dispose()
    {
        if (CommunicationObject == null)
            return;
        try {
            if (CommunicationObject.State != CommunicationState.Faulted) {
                CommunicationObject.Close();
            } else {
                CommunicationObject.Abort();
            }
        } catch (CommunicationException ce) {
            CommunicationObject.Abort();
        } catch (TimeoutException toe) {
            CommunicationObject.Abort();
        } catch (Exception e) {
            CommunicationObject.Abort();
            //Perhaps log this

        } finally {
            CommunicationObject = null;
        }
    }
}

当您访问服务器时,创建客户端并使用 using 自动断开连接:

var Ws = new ServiceClient("netTcpEndPointName");
using (new AutoCloseWcf(Ws)) {

    Ws.Open();

    Ws.Test();
}

4

我使用了Castle动态代理来解决Dispose()问题,并且实现了当通道处于不可用状态时自动刷新。要使用此功能,您必须创建一个继承服务契约和IDisposable的新接口。动态代理实现了这个接口并包装了WCF通道:

Func<object> createChannel = () =>
    ChannelFactory<IHelloWorldService>
        .CreateChannel(new NetTcpBinding(), new EndpointAddress(uri));
var factory = new WcfProxyFactory();
var proxy = factory.Create<IDisposableHelloWorldService>(createChannel);
proxy.HelloWorld();

我喜欢这个东西,因为你可以注入WCF服务而无需消费者担心任何WCF的细节。而且与其他解决方案不同,它没有添加任何多余的东西。

看看代码,其实很简单: WCF 动态代理


4

使用扩展方法:

public static class CommunicationObjectExtensions
{
    public static TResult MakeSafeServiceCall<TResult, TService>(this TService client, Func<TService, TResult> method) where TService : ICommunicationObject
    {
        TResult result;

        try
        {
            result = method(client);
        }
        finally
        {
            try
            {
                client.Close();
            }
            catch (CommunicationException)
            {
                client.Abort(); // Don't care about these exceptions. The call has completed anyway.
            }
            catch (TimeoutException)
            {
                client.Abort(); // Don't care about these exceptions. The call has completed anyway.
            }
            catch (Exception)
            {
                client.Abort();
                throw;
            }
        }

        return result;
    }
}

3

摘要

使用本答案中描述的技术,可以使用以下语法在using块中消耗WCF服务:

var channelFactory = new ChannelFactory<IMyService>("");

var serviceHelper = new ServiceHelper<IMyService>(channelFactory);
var proxy = serviceHelper.CreateChannel();
using (proxy as IDisposable)
{
    proxy.DoWork();
}

当然,您可以进一步调整此内容,以实现更简洁的编程模型,以适应您的特定情况 - 但重点是我们可以创建一个实现可释放模式的通道,代表IMyService

详细信息

到目前为止,所有给出的答案都解决了绕过WCF Channel实现中IDisposable的“错误”的问题。看起来提供了最简洁的编程模型(允许您使用using块来处理未托管的资源)的答案是this one - 在这个答案中,代理被修改以实现具有无错误实现的IDisposable。这种方法的问题在于可维护性-我们必须为每个使用的代理重新实现此功能。在这个答案的变体中,我们将看到如何使用组合而不是继承来使这个技术通用。

第一次尝试

似乎有各种各样的IDisposable实现,但为了论证我们将使用当前接受的答案的改编。

[ServiceContract]
public interface IMyService
{
    [OperationContract]
    void DoWork();
}

public class ProxyDisposer : IDisposable
{
    private IClientChannel _clientChannel;


    public ProxyDisposer(IClientChannel clientChannel)
    {
        _clientChannel = clientChannel;
    }

    public void Dispose()
    {
        var success = false;
        try
        {
            _clientChannel.Close();
            success = true;
        }
        finally
        {
            if (!success)
                _clientChannel.Abort();
            _clientChannel = null;
        }
    }
}

public class ProxyWrapper : IMyService, IDisposable
{
    private IMyService _proxy;
    private IDisposable _proxyDisposer;

    public ProxyWrapper(IMyService proxy, IDisposable disposable)
    {
        _proxy = proxy;
        _proxyDisposer = disposable;
    }

    public void DoWork()
    {
        _proxy.DoWork();
    }

    public void Dispose()
    {
        _proxyDisposer.Dispose();
    }
}

有了上述类,我们现在可以编写

public class ServiceHelper
{
    private readonly ChannelFactory<IMyService> _channelFactory;

    public ServiceHelper(ChannelFactory<IMyService> channelFactory )
    {
        _channelFactory = channelFactory;
    }

    public IMyService CreateChannel()
    {
        var channel = _channelFactory.CreateChannel();
        var channelDisposer = new ProxyDisposer(channel as IClientChannel);
        return new ProxyWrapper(channel, channelDisposer);
    }
}

这使我们能够使用using块来消费我们的服务:
ServiceHelper serviceHelper = ...;
var proxy = serviceHelper.CreateChannel();
using (proxy as IDisposable)
{
    proxy.DoWork();
}

泛化这个过程

到目前为止,我们所做的就是重新制定Tomas' solution。导致此代码无法泛化的原因在于ProxyWrapper类必须针对我们想要的每个服务合同进行重新实现。现在,我们将看一个类,它允许我们使用IL动态创建此类型:

public class ServiceHelper<T>
{
    private readonly ChannelFactory<T> _channelFactory;

    private static readonly Func<T, IDisposable, T> _channelCreator;

    static ServiceHelper()
    {
        /** 
         * Create a method that can be used generate the channel. 
         * This is effectively a compiled verion of new ProxyWrappper(channel, channelDisposer) for our proxy type
         * */
        var assemblyName = Guid.NewGuid().ToString();
        var an = new AssemblyName(assemblyName);
        var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run);
        var moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName);

        var proxyType = CreateProxyType(moduleBuilder, typeof(T), typeof(IDisposable));

        var channelCreatorMethod = new DynamicMethod("ChannelFactory", typeof(T),
            new[] { typeof(T), typeof(IDisposable) });

        var ilGen = channelCreatorMethod.GetILGenerator();
        var proxyVariable = ilGen.DeclareLocal(typeof(T));
        var disposableVariable = ilGen.DeclareLocal(typeof(IDisposable));
        ilGen.Emit(OpCodes.Ldarg, proxyVariable);
        ilGen.Emit(OpCodes.Ldarg, disposableVariable);
        ilGen.Emit(OpCodes.Newobj, proxyType.GetConstructor(new[] { typeof(T), typeof(IDisposable) }));
        ilGen.Emit(OpCodes.Ret);

        _channelCreator =
            (Func<T, IDisposable, T>)channelCreatorMethod.CreateDelegate(typeof(Func<T, IDisposable, T>));

    }

    public ServiceHelper(ChannelFactory<T> channelFactory)
    {
        _channelFactory = channelFactory;
    }

    public T CreateChannel()
    {
        var channel = _channelFactory.CreateChannel();
        var channelDisposer = new ProxyDisposer(channel as IClientChannel);
        return _channelCreator(channel, channelDisposer);
    }

   /**
    * Creates a dynamic type analogous to ProxyWrapper, implementing T and IDisposable.
    * This method is actually more generic than this exact scenario.
    * */
    private static Type CreateProxyType(ModuleBuilder moduleBuilder, params Type[] interfacesToInjectAndImplement)
    {
        TypeBuilder tb = moduleBuilder.DefineType(Guid.NewGuid().ToString(),
            TypeAttributes.Public | TypeAttributes.Class);

        var typeFields = interfacesToInjectAndImplement.ToDictionary(tf => tf,
            tf => tb.DefineField("_" + tf.Name, tf, FieldAttributes.Private));

        #region Constructor

        var constructorBuilder = tb.DefineConstructor(
            MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName |
            MethodAttributes.RTSpecialName,
            CallingConventions.Standard,
            interfacesToInjectAndImplement);

        var il = constructorBuilder.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Call, typeof(object).GetConstructor(new Type[0]));

        for (var i = 1; i <= interfacesToInjectAndImplement.Length; i++)
        {
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldarg, i);
            il.Emit(OpCodes.Stfld, typeFields[interfacesToInjectAndImplement[i - 1]]);
        }
        il.Emit(OpCodes.Ret);

        #endregion

        #region Add Interface Implementations

        foreach (var type in interfacesToInjectAndImplement)
        {
            tb.AddInterfaceImplementation(type);
        }

        #endregion

        #region Implement Interfaces

        foreach (var type in interfacesToInjectAndImplement)
        {
            foreach (var method in type.GetMethods())
            {
                var methodBuilder = tb.DefineMethod(method.Name,
                    MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig |
                    MethodAttributes.Final | MethodAttributes.NewSlot,
                    method.ReturnType,
                    method.GetParameters().Select(p => p.ParameterType).ToArray());
                il = methodBuilder.GetILGenerator();

                if (method.ReturnType == typeof(void))
                {
                    il.Emit(OpCodes.Nop);
                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Ldfld, typeFields[type]);
                    il.Emit(OpCodes.Callvirt, method);
                    il.Emit(OpCodes.Ret);
                }
                else
                {
                    il.DeclareLocal(method.ReturnType);

                    il.Emit(OpCodes.Nop);
                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Ldfld, typeFields[type]);

                    var methodParameterInfos = method.GetParameters();
                    for (var i = 0; i < methodParameterInfos.Length; i++)
                        il.Emit(OpCodes.Ldarg, (i + 1));
                    il.Emit(OpCodes.Callvirt, method);

                    il.Emit(OpCodes.Stloc_0);
                    var defineLabel = il.DefineLabel();
                    il.Emit(OpCodes.Br_S, defineLabel);
                    il.MarkLabel(defineLabel);
                    il.Emit(OpCodes.Ldloc_0);
                    il.Emit(OpCodes.Ret);
                }

                tb.DefineMethodOverride(methodBuilder, method);
            }
        }

        #endregion

        return tb.CreateType();
    }
}

有了我们的新助手类,现在我们可以编写

var channelFactory = new ChannelFactory<IMyService>("");

var serviceHelper = new ServiceHelper<IMyService>(channelFactory);
var proxy = serviceHelper.CreateChannel();
using (proxy as IDisposable)
{
    proxy.DoWork();
}

请注意,您也可以使用相同的技术(稍加修改)来为继承自 ClientBase<> 的自动生成客户端(而不是使用 ChannelFactory<>),或者如果您想使用不同的 IDisposable 实现关闭您的通道。

2

我喜欢这种关闭连接的方式:

var client = new ProxyClient();
try
{
    ...
    client.Close();
}
finally
{
    if(client.State != CommunicationState.Closed)
        client.Abort();
}

1

我希望添加Marc Gravell的答案中的Service实现,以便在使用ServiceClient而不是ChannelFactory的情况下进行操作。

public interface IServiceConnector<out TServiceInterface>
{
    void Connect(Action<TServiceInterface> clientUsage);
    TResult Connect<TResult>(Func<TServiceInterface, TResult> channelUsage);
}

internal class ServiceConnector<TService, TServiceInterface> : IServiceConnector<TServiceInterface>
    where TServiceInterface : class where TService : ClientBase<TServiceInterface>, TServiceInterface, new()
{
    public TResult Connect<TResult>(Func<TServiceInterface, TResult> channelUsage)
    {
        var result = default(TResult);
        Connect(channel =>
        {
            result = channelUsage(channel);
        });
        return result;
    }

    public void Connect(Action<TServiceInterface> clientUsage)
    {
        if (clientUsage == null)
        {
            throw new ArgumentNullException("clientUsage");
        }
        var isChanneldClosed = false;
        var client = new TService();
        try
        {
            clientUsage(client);
            client.Close();
            isChanneldClosed = true;
        }
        finally
        {
            if (!isChanneldClosed)
            {
                client.Abort();
            }
        }
    }
}

1
我们的系统架构通常使用Unity IoC框架来创建ClientBase实例,因此没有确切的方法来强制其他开发人员甚至使用using{}块。为了尽可能地防止出错,我创建了这个自定义类来扩展ClientBase,并在dispose时或在finalize时处理关闭通道,以防某些人没有显式地处理Unity创建的实例。
构造函数中还需要完成一些设置通道的工作,以适应自定义凭据等内容,所以这也包含在其中...
public abstract class PFServer2ServerClientBase<TChannel> : ClientBase<TChannel>, IDisposable where TChannel : class
{
    private bool disposed = false;

    public PFServer2ServerClientBase()
    {
        // Copy information from custom identity into credentials, and other channel setup...
    }

    ~PFServer2ServerClientBase()
    {
        this.Dispose(false);
    }

    void IDisposable.Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    public void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            try
            {
                    if (this.State == CommunicationState.Opened)
                        this.Close();
            }
            finally
            {
                if (this.State == CommunicationState.Faulted)
                    this.Abort();
            }
            this.disposed = true;
        }
    }
}

然后客户端就可以:

internal class TestClient : PFServer2ServerClientBase<ITest>, ITest
{
    public string TestMethod(int value)
    {
        return base.Channel.TestMethod(value);
    }
}

而调用者可以执行以下任何操作:

public SomeClass
{
    [Dependency]
    public ITest test { get; set; }

    // Not the best, but should still work due to finalizer.
    public string Method1(int value)
    {
        return this.test.TestMethod(value);
    }

    // The good way to do it
    public string Method2(int value)
    {
        using(ITest t = unityContainer.Resolve<ITest>())
        {
            return t.TestMethod(value);
        }
    }
}

你在Dispose方法中从未使用参数disposing。 - CaffGeek
@Chad - 我一直在遵循微软的常见Finalize/Dispose设计模式:http://msdn.microsoft.com/en-us/library/b1yfkh5e%28VS.71%29.aspx。虽然我没有使用该变量,但这是真实的,因为我不需要在正常处理和终结之间进行任何不同的清理。可以重写代码,只需让Finalize调用Dispose()并将代码从Dispose(bool)移动到Dispose()中。 - CodingWithSpike
Finalizers增加开销,而且不确定性较高。我尽可能避免使用它们。你可以使用Unity的自动工厂来注入委托并将其放置在using块中,或者(更好地)将创建/调用/释放服务行为隐藏在注入接口的方法后面。每次调用依赖项都会创建代理,调用它并释放它。 - TrueWill

1

我已经编写了一个简单的基类来处理这个问题。它作为一个NuGet包可用,并且非常容易使用。

//MemberServiceClient is the class generated by SvcUtil
public class MemberServiceManager : ServiceClientBase<MemberServiceClient>
{
    public User GetUser(int userId)
    {
        return PerformServiceOperation(client => client.GetUser(userId));
    }

    //you can also check if any error occured if you can't throw exceptions       
    public bool TryGetUser(int userId, out User user)
    {
        return TryPerformServiceOperation(c => c.GetUser(userId), out user);
    }
}

VS2013-.net 4.5.1 有更新吗?是否有类似于https://dev59.com/6nRB5IYBdhLWcg3wn4QL#9370880的重试选项? - Kiquenet
@Kiquenet 我已经不再从事WCF的工作了。如果您向我发送一个pull request,我可以合并它并更新包。 - Ufuk Hacıoğulları

1
public static class Service<TChannel>
{
    public static ChannelFactory<TChannel> ChannelFactory = new ChannelFactory<TChannel>("*");

    public static TReturn Use<TReturn>(Func<TChannel,TReturn> codeBlock)
    {
        var proxy = (IClientChannel)ChannelFactory.CreateChannel();
        var success = false;
        try
        {
            var result = codeBlock((TChannel)proxy);
            proxy.Close();
            success = true;
            return result;
        }
        finally
        {
            if (!success)
            {
                proxy.Abort();
            }
        }
    }
}

所以它允许很好地编写返回语句:
return Service<IOrderService>.Use(orderService => 
{ 
    return orderService.PlaceOrder(request); 
}); 

1

对于那些感兴趣的人,这里提供了接受答案(下面)的VB.NET翻译。我稍微修改了一下以缩短长度,并结合了本主题中其他人的一些提示。

我承认这与原始标签(C#)不相关,但由于我无法找到此解决方案的VB.NET版本,我认为其他人也会寻找它。Lambda翻译可能有点棘手,因此我想帮助某些人避免麻烦。

请注意,此特定实现提供了在运行时配置ServiceEndpoint的能力。


代码:

Namespace Service
  Public NotInheritable Class Disposable(Of T)
    Public Shared ChannelFactory As New ChannelFactory(Of T)(Service)

    Public Shared Sub Use(Execute As Action(Of T))
      Dim oProxy As IClientChannel

      oProxy = ChannelFactory.CreateChannel

      Try
        Execute(oProxy)
        oProxy.Close()

      Catch
        oProxy.Abort()
        Throw

      End Try
    End Sub



    Public Shared Function Use(Of TResult)(Execute As Func(Of T, TResult)) As TResult
      Dim oProxy As IClientChannel

      oProxy = ChannelFactory.CreateChannel

      Try
        Use = Execute(oProxy)
        oProxy.Close()

      Catch
        oProxy.Abort()
        Throw

      End Try
    End Function



    Public Shared ReadOnly Property Service As ServiceEndpoint
      Get
        Return New ServiceEndpoint(
          ContractDescription.GetContract(
            GetType(T),
            GetType(Action(Of T))),
          New BasicHttpBinding,
          New EndpointAddress(Utils.WcfUri.ToString))
      End Get
    End Property
  End Class
End Namespace

用法:

Public ReadOnly Property Jobs As List(Of Service.Job)
  Get
    Disposable(Of IService).Use(Sub(Client) Jobs = Client.GetJobs(Me.Status))
  End Get
End Property

Public ReadOnly Property Jobs As List(Of Service.Job)
  Get
    Return Disposable(Of IService).Use(Function(Client) Client.GetJobs(Me.Status))
  End Get
End Property

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接