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个回答

142

实际上,尽管我在博客中(请参见Luke的答案),但我认为这个方法比我的IDisposable包装器更好。典型代码:

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

(根据评论修改)

由于Use返回void,处理返回值最简单的方法是通过捕获变量:

int newOrderId = 0; // need a value for definite assignment
Service<IOrderService>.Use(orderService=>
  {
    newOrderId = orderService.PlaceOrder(request);
  });
Console.WriteLine(newOrderId); // should be updated

2
@MarcGravell 我应该在哪里注入客户端?我假设ChannelFactory创建客户端,而工厂对象是在Service类内部new的,这意味着代码需要进行一些重构才能允许自定义工厂。这正确吗,还是我漏掉了什么明显的东西? - Anttu
17
你可以轻松修改包装器,这样就不需要为结果使用捕获变量。类似这样:public static TResult Use<TResult>(Func<T, TResult> codeBlock) { ... } - chris
3
也许有用:https://devzone.channeladam.com/articles/2014/07/how-to-call-wcf-service-properly/https://devzone.channeladam.com/articles/2014/09/how-to-easily-call-wcf-service-properly/以及http://dzimchuk.net/post/wcf-error-helpers - PreguntonCojoneroCabrón
我该如何使用这种方式添加凭据? - Hippasus
4
在我看来,最正确的解决方案应该是: 1)在没有竞争条件的情况下执行Close/Abort模式 2)处理服务操作抛出异常的情况 3)处理Close和Abort方法同时抛出异常的情况 4)处理异步异常,如ThreadAbortException。来源链接:https://devzone.channeladam.com/articles/2014/07/how-to-call-wcf-service-properly/ - Kiquenet
显示剩余7条评论

92
给出IServiceOriented.com提倡的解决方案和David Barret's blog提倡的解决方案之间的选择,我更喜欢通过覆盖客户端的Dispose()方法提供的简单性。这使我可以像使用可处理对象一样继续使用using()语句。但是,正如@Brian指出的那样,该解决方案包含竞争条件,因为在检查状态时可能不会发生错误,但在调用Close()时可能会发生,此时仍会发生CommunicationException。
因此,为了解决这个问题,我采用了一个将两种方法结合起来的解决方案。
void IDisposable.Dispose()
{
    bool success = false;
    try 
    {
        if (State != CommunicationState.Faulted) 
        {
            Close();
            success = true;
        }
    } 
    finally 
    {
        if (!success) 
            Abort();
    }
}

2
使用“Try-Finally”(或语法糖“using(){}”)语句处理非托管资源是否存在风险?例如,如果“Close”选项失败,则异常不会被捕获,finally可能无法运行。此外,如果finally语句中出现异常,它可能掩盖其他异常。我认为这就是为什么首选Try-Catch的原因。 - Zack Jannsen
1
@jmoreno,我撤销了你的编辑。如果你注意到,这个方法中根本没有catch块。这个想法是,任何发生的异常(即使在finally中)都应该被抛出,而不是被静默地捕获。 - Matt Davis
5
你为什么需要success标志呢?为什么不使用try { Close(); } catch { Abort(); throw; }?注意:已经翻译成中文。 - Konstantin Spirin
Close(); success = true; 放在 try/catch 语句块中怎么样?如果我可以在 finally 块中成功中止它,我就不想抛出异常。只有在中止失败的情况下才希望抛出异常。这样,try/catch 就可以隐藏潜在的竞争条件异常,仍然允许你在 finally 块中中止连接。 - goku_da_master
仅想澄清,我是要说使用 try/catch - 不需要重新抛出异常。(我已经超过了5分钟的编辑评论窗口。) - goku_da_master
显示剩余2条评论

34

我编写了一个高阶函数来使其正常工作。我们在多个项目中使用它,似乎效果很好。这才是一开始就应该做到的,而不是使用“using”模式等等。

TReturn UseService<TChannel, TReturn>(Func<TChannel, TReturn> code)
{
    var chanFactory = GetCachedFactory<TChannel>();
    TChannel channel = chanFactory.CreateChannel();
    bool error = true;
    try {
        TReturn result = code(channel);
        ((IClientChannel)channel).Close();
        error = false;
        return result;
    }
    finally {
        if (error) {
            ((IClientChannel)channel).Abort();
        }
    }
}
你可以这样调用:
int a = 1;
int b = 2;
int sum = UseService((ICalculator calc) => calc.Add(a, b));
Console.WriteLine(sum);

这基本上就像你在示例中所写的一样。在某些项目中,我们编写强类型的帮助方法,所以我们最终会编写类似于"Wcf.UseFooService(f=>f...)"的代码。

考虑到所有事情,我认为这相当优雅。您遇到了特定的问题吗?

这使得其他方便的功能可以被插入。例如,在一个站点上,该站点代表已登录用户对服务进行身份验证。(该站点本身没有凭据。)通过编写自己的"UseService"方法帮助程序,我们可以按照我们想要的方式配置通道工厂等。我们也不必使用生成的代理 - 任何接口都可以使用。


我遇到了异常:ChannelFactory.Endpoint上的Address属性为空。必须在ChannelFactory的Endpoint中指定有效的Address。 GetCachedFactory方法是什么? - Marshall
将通道工厂缓存起来听起来有些不对,因为当通道出现故障时,工厂也会(尝试处理它也会抛出CommunicationObjectFaultedException)! - Medinoc

30
这是微软处理 WCF 客户端调用的推荐方法:
更多详细信息请参见:预期的异常
try
{
    ...
    double result = client.Add(value1, value2);
    ...
    client.Close();
}
catch (TimeoutException exception)
{
    Console.WriteLine("Got {0}", exception.GetType());
    client.Abort();
}
catch (CommunicationException exception)
{
    Console.WriteLine("Got {0}", exception.GetType());
    client.Abort();
}

附加信息 许多人似乎在WCF上问这个问题,微软甚至创建了一个专门的示例来演示如何处理异常:

c:\WF_WCF_Samples\WCF\Basic\Client\ExpectedExceptions\CS\client

下载示例: C#VB

考虑到有这么多关于此问题的问题涉及使用语句, (激烈的?)内部讨论线程, 我不会浪费时间尝试成为代码牛仔并找到更清晰的方法。我只会像这样冗长(但可信赖)地实现WCF客户端,以供我的服务器应用程序使用。

可选的其他故障捕获

许多异常都源自CommunicationException,但我认为大多数这些异常不应该重试。我在MSDN上耐心查阅了每个异常,并列出了几个可重试的异常(除了上面提到的TimeOutException)。如果我错过了应该重试的异常,请告诉我。
  // The following is typically thrown on the client when a channel is terminated due to the server closing the connection.
catch (ChannelTerminatedException cte)
{
secureSecretService.Abort();
// todo: Implement delay (backoff) and retry
}

// The following is thrown when a remote endpoint could not be found or reached.  The endpoint may not be found or 
// reachable because the remote endpoint is down, the remote endpoint is unreachable, or because the remote network is unreachable.
catch (EndpointNotFoundException enfe)
{
secureSecretService.Abort();
// todo: Implement delay (backoff) and retry
}

// The following exception that is thrown when a server is too busy to accept a message.
catch (ServerTooBusyException stbe)
{
secureSecretService.Abort();
// todo: Implement delay (backoff) and retry
}

可以承认,写这段代码有点枯燥。我目前更喜欢这个答案,并且没有看到任何可能在未来引起问题的“黑客”代码。


1
示例代码是否仍然存在问题?我尝试运行UsingUsing项目(VS2013),但带有“希望这段代码不重要,因为它可能不会发生。”的行仍然被执行... - janv8000

14

我终于找到了一些解决这个问题的可靠步骤。

这个自定义工具扩展了WCFProxyGenerator,提供了一个异常处理代理。它生成了一个名为ExceptionHandlingProxy<T>的额外代理,该代理继承ExceptionHandlingProxyBase<T>,后者实现了代理功能的核心部分。结果是你可以选择使用继承ClientBase<T>的默认代理或ExceptionHandlingProxy<T>来封装通道工厂和通道的生命周期管理。ExceptionHandlingProxy会尊重在添加服务引用对话框中选择的异步方法和集合类型。

Codeplex有一个名为Exception Handling WCF Proxy Generator的项目。它基本上安装了一个新的自定义工具到Visual Studio 2008,然后使用这个工具生成新的服务代理(添加服务引用)。它具有一些不错的功能来处理故障通道、超时和安全处置。这里有一个非常好的视频ExceptionHandlingProxyWrapper,详细解释了它的工作原理。

你可以放心地再次使用Using语句,如果通道在任何请求上出现故障(TimeoutException或CommunicationException),Wrapper将重新初始化故障通道并重试查询。如果失败,则调用Abort()命令并处理代理并重新抛出异常。如果服务抛出FaultException代码,则停止执行,并安全地中止代理并抛出正确的异常。


@Shimmy 状态 Beta。日期:2009 年 7 月 11 日,由 Michele Bustamante。项目已死? - Kiquenet

11

根据Marc Gravell、MichaelGG和Matt Davis的答案,我们的开发人员得出了以下结论:

public static class UsingServiceClient
{
    public static void Do<TClient>(TClient client, Action<TClient> execute)
        where TClient : class, ICommunicationObject
    {
        try
        {
            execute(client);
        }
        finally
        {
            client.DisposeSafely();
        }
    }

    public static void DisposeSafely(this ICommunicationObject client)
    {
        if (client == null)
        {
            return;
        }

        bool success = false;

        try
        {
            if (client.State != CommunicationState.Faulted)
            {
                client.Close();
                success = true;
            }
        }
        finally
        {
            if (!success)
            {
                client.Abort();
            }
        }
    }
}

使用示例:

string result = string.Empty;

UsingServiceClient.Do(
    new MyServiceClient(),
    client =>
    result = client.GetServiceResult(parameters));

它尽可能接近于“using”语法,当调用无返回值方法时,您不必返回虚拟值,并且您可以多次调用服务(并返回多个值)而无需使用元组。

此外,如果需要,您可以将其与ClientBase<T>后代一起使用,而不是ChannelFactory。

如果开发人员想要手动处理代理/通道,则公开了扩展方法。


如果我正在使用PoolingDuplex且在调用后不关闭连接,以便我的客户端服务可以存活数天并处理服务器回调,那么使用这个东西是否有意义?就我所了解的,这里讨论的解决方案是否对每个会话只进行一次调用有意义? - sll
@sll - 这是为了在调用返回后立即关闭连接(每个会话一次调用)。 - TrueWill
@cacho 将DisposeSafely设为私有确实是一个选择,这样可以避免混淆。可能存在一些使用情况,某人可能希望直接调用它,但我暂时想不到这样的情况。 - TrueWill
@truewill 仅作为文档,还重要提到这个方法是线程安全的,对吗? - Cacho Santa
@cacho 这是静态的,不访问任何全局状态,因此它应该是线程安全的 - 但这实际上取决于代理。根据这个描述,似乎通常情况下都是这样的:https://dev59.com/Hm445IYBdhLWcg3w7uk0 如果您依赖它来做生意,我建议您确保一下。 - TrueWill
1
在我看来,最正确的解决方案应该是:1)执行Close/Abort模式而不会出现竞争条件 2)处理服务操作抛出异常的情况 3)处理Close和Abort方法都抛出异常的情况 4)处理异步异常,如ThreadAbortExceptionhttps://devzone.channeladam.com/articles/2014/07/how-to-call-wcf-service-properly/ - Kiquenet

8

@Marc Gravell

使用这个不可以吗:

public static TResult Using<T, TResult>(this T client, Func<T, TResult> work)
        where T : ICommunicationObject
{
    try
    {
        var result = work(client);

        client.Close();

        return result;
    }
    catch (Exception e)
    {
        client.Abort();

        throw;
    }
}

或者,针对 Service<IOrderService>.Use,可以使用相同的东西 (Func<T, TResult>)

这将使返回变量更加容易。


2
+1 @MarcGravell,我认为你的回答“还有提升的空间”太客气了:P(而且行动方案可以采用一个空返回的Func实现)。整个页面都很混乱 - 如果我在这十年里想使用WCF,我会去制定一个统一的页面并评论重复内容... - Ruben Bartelink

7
以下是这个问题的源代码的增强版本,扩展了缓存多个通道工厂并尝试通过合同名称在配置文件中查找端点。
它使用.NET 4(具体来说:逆变性,LINQ,var):
/// <summary>
/// Delegate type of the service method to perform.
/// </summary>
/// <param name="proxy">The service proxy.</param>
/// <typeparam name="T">The type of service to use.</typeparam>
internal delegate void UseServiceDelegate<in T>(T proxy);

/// <summary>
/// Wraps using a WCF service.
/// </summary>
/// <typeparam name="T">The type of service to use.</typeparam>
internal static class Service<T>
{
    /// <summary>
    /// A dictionary to hold looked-up endpoint names.
    /// </summary>
    private static readonly IDictionary<Type, string> cachedEndpointNames = new Dictionary<Type, string>();

    /// <summary>
    /// A dictionary to hold created channel factories.
    /// </summary>
    private static readonly IDictionary<string, ChannelFactory<T>> cachedFactories =
        new Dictionary<string, ChannelFactory<T>>();

    /// <summary>
    /// Uses the specified code block.
    /// </summary>
    /// <param name="codeBlock">The code block.</param>
    internal static void Use(UseServiceDelegate<T> codeBlock)
    {
        var factory = GetChannelFactory();
        var proxy = (IClientChannel)factory.CreateChannel();
        var success = false;

        try
        {
            using (proxy)
            {
                codeBlock((T)proxy);
            }

            success = true;
        }
        finally
        {
            if (!success)
            {
                proxy.Abort();
            }
        }
    }

    /// <summary>
    /// Gets the channel factory.
    /// </summary>
    /// <returns>The channel factory.</returns>
    private static ChannelFactory<T> GetChannelFactory()
    {
        lock (cachedFactories)
        {
            var endpointName = GetEndpointName();

            if (cachedFactories.ContainsKey(endpointName))
            {
                return cachedFactories[endpointName];
            }

            var factory = new ChannelFactory<T>(endpointName);

            cachedFactories.Add(endpointName, factory);
            return factory;
        }
    }

    /// <summary>
    /// Gets the name of the endpoint.
    /// </summary>
    /// <returns>The name of the endpoint.</returns>
    private static string GetEndpointName()
    {
        var type = typeof(T);
        var fullName = type.FullName;

        lock (cachedFactories)
        {
            if (cachedEndpointNames.ContainsKey(type))
            {
                return cachedEndpointNames[type];
            }

            var serviceModel = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None).SectionGroups["system.serviceModel"] as ServiceModelSectionGroup;

            if ((serviceModel != null) && !string.IsNullOrEmpty(fullName))
            {
                foreach (var endpointName in serviceModel.Client.Endpoints.Cast<ChannelEndpointElement>().Where(endpoint => fullName.EndsWith(endpoint.Contract)).Select(endpoint => endpoint.Name))
                {
                    cachedEndpointNames.Add(type, endpointName);
                    return endpointName;
                }
            }
        }

        throw new InvalidOperationException("Could not find endpoint element for type '" + fullName + "' in the ServiceModel client configuration section. This might be because no configuration file was found for your application, or because no endpoint element matching this name could be found in the client element.");
    }
}

1
为什么要使用 UseServiceDelegate<T> 而不是 Action<T> - Mike Mayer
1
我能想到原作者这样做的唯一原因是为了让开发人员知道属于调用服务的强类型委托。但就我所见,Action<T>同样有效。 - Jesse C. Slicer

7

这是什么?

这是CW版本的被接受答案,但包含(我认为完整的)异常处理。

被接受的答案引用了一个已经不存在的网站。为了节省您的麻烦,我在此处包含了最相关的部分。此外,我对其进行了轻微的修改,以包括异常重试处理来处理那些讨厌的网络超时。

简单的WCF客户端用法

一旦您生成了客户端代理,这就是您需要实现它的全部内容。

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

ServiceDelegate.cs

将此文件添加到您的解决方案中。无需更改此文件,除非您想更改重试次数或要处理的异常。

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;


       Exception mostRecentEx = null;
       int millsecondsToSleep = 1000;

       for(int i=0; i<5; i++)  // Attempt a maximum of 5 times 
       {
           try
           {
               codeBlock((T)proxy);
               proxy.Close();
               success = true; 
               break;
           }

           // The following is typically thrown on the client when a channel is terminated due to the server closing the connection.
           catch (ChannelTerminatedException cte)
           {
              mostRecentEx = cte;
               proxy.Abort();
               //  delay (backoff) and retry 
               Thread.Sleep(millsecondsToSleep  * (i + 1)); 
           }

           // The following is thrown when a remote endpoint could not be found or reached.  The endpoint may not be found or 
           // reachable because the remote endpoint is down, the remote endpoint is unreachable, or because the remote network is unreachable.
           catch (EndpointNotFoundException enfe)
           {
              mostRecentEx = enfe;
               proxy.Abort();
               //  delay (backoff) and retry 
               Thread.Sleep(millsecondsToSleep * (i + 1)); 
           }

           // The following exception that is thrown when a server is too busy to accept a message.
           catch (ServerTooBusyException stbe)
           {
              mostRecentEx = stbe;
               proxy.Abort();

               //  delay (backoff) and retry 
               Thread.Sleep(millsecondsToSleep * (i + 1)); 
           }
           catch (TimeoutException timeoutEx)
           {
               mostRecentEx = timeoutEx;
               proxy.Abort();

               //  delay (backoff) and retry 
               Thread.Sleep(millsecondsToSleep * (i + 1)); 
           } 
           catch (CommunicationException comException)
           {
               mostRecentEx = comException;
               proxy.Abort();

               //  delay (backoff) and retry 
               Thread.Sleep(millsecondsToSleep * (i + 1)); 
           }
           catch(Exception )
           {
                // rethrow any other exception not defined here
                // You may want to define a custom Exception class to pass information such as failure count, and failure type
                proxy.Abort();
                throw ;  
           }
       }
       if (success == false && mostRecentEx != null) 
       { 
           proxy.Abort();
           throw new Exception("WCF call failed after 5 retries.", mostRecentEx );
       }

    }
}

提示:我已将此帖子设为社区wiki页面。我不会从此答案中收集“点数”,但如果您同意此实现方式,请为其投票,或编辑它以使其更好。


我不确定我同意你对这个答案的描述。它是 CW 版本,加上了 对异常处理的想法 - John Saunders
成功变量怎么样了?需要将它添加到源代码中:if (success) return; ?? - Kiquenet
如果第一个调用抛出异常而第二个成功,则 mostRecentEx 将不为 null,所以您将抛出一个无论如何都失败了 5 次重试的异常。或者我漏掉了什么?我没有看到您清除 mostRecentEx 的地方,如果第二、三、四或五次尝试成功,也不见得有返回o succeed。我应该在这里漏掉了什么,但是如果没有抛出异常,这段代码并不总是运行 5 次吗? - Bart Calixto
@Bart - 我在最后的if语句中添加了success == false - makerofthings7
在这个参考链接 https://devzone.channeladam.com/articles/2014/07/how-to-call-wcf-service-properly/ 中,最正确的解决方案应该是:1) 执行Close/Abort模式而不会出现竞争条件 2) 处理服务操作抛出异常的情况 3) 处理Close和Abort方法都抛出异常的情况 4) 处理异步异常,如ThreadAbortException
  • 这个怎么样?
- Kiquenet
显示剩余2条评论

5
这样的包装器可以起到作用:
public class ServiceClientWrapper<ServiceType> : IDisposable
{
    private ServiceType _channel;
    public ServiceType Channel
    {
        get { return _channel; }
    }

    private static ChannelFactory<ServiceType> _channelFactory;

    public ServiceClientWrapper()
    {
        if(_channelFactory == null)
             // Given that the endpoint name is the same as FullName of contract.
            _channelFactory = new ChannelFactory<ServiceType>(typeof(T).FullName);
        _channel = _channelFactory.CreateChannel();
        ((IChannel)_channel).Open();
    }

    public void Dispose()
    {
        try
        {
            ((IChannel)_channel).Close();
        }
        catch (Exception e)
        {
            ((IChannel)_channel).Abort();
            // TODO: Insert logging
        }
    }
}

这将使您能够编写如下代码:

ResponseType response = null;
using(var clientWrapper = new ServiceClientWrapper<IService>())
{
    var request = ...
    response = clientWrapper.Channel.MyServiceCall(request);
}
// Use your response object.

包装器当然可以捕获更多的异常,如果需要的话,但原则仍然是相同的。

我记得有关Dispose在某些情况下未被调用的讨论...导致了WCF中的内存泄漏。 - makerofthings7
我不确定这是否导致了内存泄漏,但问题在于:当您在IChannel上调用Dispose时,如果通道处于故障状态,则可能会引发异常。这是一个问题,因为Microsoft指定Dispose不应该抛出异常。所以上面的代码处理了Close引发异常的情况。如果Abort引发异常,那么可能有严重的问题。我去年十二月写了一篇关于它的博客文章:http://blog.tomasjansson.com/2010/12/disposible-wcf-client-wrapper/ - Tomas Jansson

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