使用async/await调用WCF服务的模式

gab*_*ldi 59 .net c# asp.net wcf async-await

我使用基于任务的操作生成了代理.

如何使用async/await 正确调用此服务(处理ServiceClientOperationContext之后)?

我的第一次尝试是:

public async Task<HomeInfo> GetHomeInfoAsync(DateTime timestamp)
{
    using (var helper = new ServiceHelper<ServiceClient, ServiceContract>())
    {
        return await helper.Proxy.GetHomeInfoAsync(timestamp);
    }
}
Run Code Online (Sandbox Code Playgroud)

作为ServiceHelper一个创造ServiceClientOperationContextScope后来处理它们的类:

try
{
    if (_operationContextScope != null)
    {
        _operationContextScope.Dispose();
    }

    if (_serviceClient != null)
    {
        if (_serviceClient.State != CommunicationState.Faulted)
        {
            _serviceClient.Close();
        }
        else
        {
            _serviceClient.Abort();
        }
    }
}
catch (CommunicationException)
{
    _serviceClient.Abort();
}
catch (TimeoutException)
{
    _serviceClient.Abort();
}
catch (Exception)
{
    _serviceClient.Abort();
    throw;
}
finally
{
    _operationContextScope = null;
    _serviceClient = null;
}
Run Code Online (Sandbox Code Playgroud)

但是,当同时调用两个服务时出现以下错误,这种情况很糟糕:"此OperationContextScope正在处理与创建时不同的线程."

MSDN说:

不要在OperationContextScope块中使用异步"await"模式.当继续发生时,它可以在不同的线程上运行,而OperationContextScope是特定于线程的.如果需要为异步调用调用"await",请在OperationContextScope块之外使用它.

这就是问题所在!但是,我们如何正确地修复它?

这家伙做了MSDN说的话:

private async void DoStuffWithDoc(string docId)
{
   var doc = await GetDocumentAsync(docId);
   if (doc.YadaYada)
   {
        // more code here
   }
}

public Task<Document> GetDocumentAsync(string docId)
{
  var docClient = CreateDocumentServiceClient();
  using (new OperationContextScope(docClient.InnerChannel))
  {
    return docClient.GetDocumentAsync(docId);
  }
}
Run Code Online (Sandbox Code Playgroud)

我的代码问题是他从不在ServiceClient上调用Close(或Abort).

我还找到了一种OperationContextScope使用自定义传播的方法SynchronizationContext.但是,除了它是很多"有风险"的代码之外,他还指出:

值得注意的是,它确实有一些关于操作上下文范围处理的小问题(因为它们只允许你将它们放在调用线程上),但这似乎不是一个问题,因为(至少根据反汇编),它们实现Dispose()但不实现Finalize().

那么,我们这里运气不好吗?是否存在使用async/await调用WCF服务的已证明模式ServiceClient并且将两者一起处理OperationContextScope?也许某人形成微软(也许是大师Stephen Toub :))可以提供帮助.

谢谢!

[UPDATE]

在Noseratio用户的帮助下,我想出了一些有用的东西:不要使用OperationContextScope.如果你正在使用它任何的这些理由,试图找到适合您的方案一种解决方法.否则,如果你真的,真的,需要OperationContextScope,你将不得不想出一个SynchronizationContext捕获它的实现,这似乎非常困难(如果可能的话 - 必须有一个原因,为什么这不是默认行为).

所以,完整的工作代码是:

public async Task<HomeInfo> GetHomeInfoAsync(DateTime timestamp)
{
    using (var helper = new ServiceHelper<ServiceClient, ServiceContract>())
    {
        return await helper.Proxy.GetHomeInfoAsync(timestamp);
    }
}
Run Code Online (Sandbox Code Playgroud)

随着ServiceHelper:

public class ServiceHelper<TServiceClient, TService> : IDisposable
    where TServiceClient : ClientBase<TService>, new()
    where TService : class
{
protected bool _isInitialized;
    protected TServiceClient _serviceClient;

    public TServiceClient Proxy
    {
        get
        {
            if (!_isInitialized)
            {
                Initialize();
                _isInitialized = true;
            }
            else if (_serviceClient == null)
            {
                throw new ObjectDisposedException("ServiceHelper");
            }

            return _serviceClient;
        }
    }

    protected virtual void Initialize()
    {
        _serviceClient = new TServiceClient();
    }

    // Implement IDisposable.
    // Do not make this method virtual.
    // A derived class should not be able to override this method.
    public void Dispose()
    {
        Dispose(true);

        // Take yourself off the Finalization queue 
        // to prevent finalization code for this object
        // from executing a second time.
        GC.SuppressFinalize(this);
    }

    // Dispose(bool disposing) executes in two distinct scenarios.
    // If disposing equals true, the method has been called directly
    // or indirectly by a user's code. Managed and unmanaged resources
    // can be disposed.
    // If disposing equals false, the method has been called by the 
    // runtime from inside the finalizer and you should not reference 
    // other objects. Only unmanaged resources can be disposed.
    protected virtual void Dispose(bool disposing)
    {
        // If disposing equals true, dispose all managed 
        // and unmanaged resources.
        if (disposing)
        {
            try
            {
                if (_serviceClient != null)
                {
                    if (_serviceClient.State != CommunicationState.Faulted)
                    {
                        _serviceClient.Close();
                    }
                    else
                    {
                        _serviceClient.Abort();
                    }
                }
            }
            catch (CommunicationException)
            {
                _serviceClient.Abort();
            }
            catch (TimeoutException)
            {
                _serviceClient.Abort();
            }
            catch (Exception)
            {
                _serviceClient.Abort();
                throw;
            }
            finally
            {
                _serviceClient = null;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,该类支持扩展; 也许你需要继承并提供证书.

唯一可能的"问题"在于GetHomeInfoAsync,你不能只从Task你的代理中返回你得到的东西(这看起来应该是自然的,为什么Task你已经拥有它时创建一个新东西).那么,在这种情况下,你需要await代理Task,并随后关闭(或终止)的ServiceClient,否则你会被关闭它调用服务(而字节正在通过网络发送)后,马上!

好吧,我们有办法让它发挥作用,但是从权威来源得到答案会很好,正如Noseratio所说.

nos*_*tio 31

我认为一个可行的解决方案可能是使用自定义awaiter来通过新的操作上下文OperationContext.Current.它本身的实现OperationContext似乎不需要线程亲和力.这是模式:

async Task TestAsync()
{
    using(var client = new WcfAPM.ServiceClient())
    using (var scope = new FlowingOperationContextScope(client.InnerChannel))
    {
        await client.SomeMethodAsync(1).ContinueOnScope(scope);
        await client.AnotherMethodAsync(2).ContinueOnScope(scope);
    }
}
Run Code Online (Sandbox Code Playgroud)

这是FlowingOperationContextScopeContinueOnScope(仅经过轻微测试)的实现:

public sealed class FlowingOperationContextScope : IDisposable
{
    bool _inflight = false;
    bool _disposed;
    OperationContext _thisContext = null;
    OperationContext _originalContext = null;

    public FlowingOperationContextScope(IContextChannel channel):
        this(new OperationContext(channel))
    {
    }

    public FlowingOperationContextScope(OperationContext context)
    {
        _originalContext = OperationContext.Current;
        OperationContext.Current = _thisContext = context;
    }

    public void Dispose()
    {
        if (!_disposed)
        {
            if (_inflight || OperationContext.Current != _thisContext)
                throw new InvalidOperationException();
            _disposed = true;
            OperationContext.Current = _originalContext;
            _thisContext = null;
            _originalContext = null;
        }
    }

    internal void BeforeAwait()
    {
        if (_inflight)
            return;
        _inflight = true;
        // leave _thisContext as the current context
   }

    internal void AfterAwait()
    {
        if (!_inflight)
            throw new InvalidOperationException();
        _inflight = false;
        // ignore the current context, restore _thisContext
        OperationContext.Current = _thisContext;
    }
}

// ContinueOnScope extension
public static class TaskExt
{
    public static SimpleAwaiter<TResult> ContinueOnScope<TResult>(this Task<TResult> @this, FlowingOperationContextScope scope)
    {
        return new SimpleAwaiter<TResult>(@this, scope.BeforeAwait, scope.AfterAwait);
    }

    // awaiter
    public class SimpleAwaiter<TResult> :
        System.Runtime.CompilerServices.INotifyCompletion
    {
        readonly Task<TResult> _task;

        readonly Action _beforeAwait;
        readonly Action _afterAwait;

        public SimpleAwaiter(Task<TResult> task, Action beforeAwait, Action afterAwait)
        {
            _task = task;
            _beforeAwait = beforeAwait;
            _afterAwait = afterAwait;
        }

        public SimpleAwaiter<TResult> GetAwaiter()
        {
            return this;
        }

        public bool IsCompleted
        {
            get 
            {
                // don't do anything if the task completed synchronously
                // (we're on the same thread)
                if (_task.IsCompleted)
                    return true;
                _beforeAwait();
                return false;
            }

        }

        public TResult GetResult()
        {
            return _task.Result;
        }

        // INotifyCompletion
        public void OnCompleted(Action continuation)
        {
            _task.ContinueWith(task =>
            {
                _afterAwait();
                continuation();
            },
            CancellationToken.None,
            TaskContinuationOptions.ExecuteSynchronously,
            SynchronizationContext.Current != null ?
                TaskScheduler.FromCurrentSynchronizationContext() :
                TaskScheduler.Current);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 好吧,我印象深刻!我刚刚测试了一下它似乎工作正常:我没有得到任何`OperationContextScope被放置在一个不同的线程而不是它创建的例外,我能够传递上下文信息并通过服务器端检索它`OperationContext.Current.IncomingMessageHeaders`(`ClientCredentials`工作正常,FWIW).我要花一些时间来解决你的解决方案,@ noseratio :)在生产代码中使用它之前,你能想出我们应该考虑的边缘情况或事情吗?谢谢! (4认同)
  • 这是一个非常有用的答案!然而,我找到了一个可能的改进.我在嵌套上下文中使用它,发现_inflight的布尔值导致`InvalidOperationException`.相反,您可以使用`int`类型的_inflight计数器,`BeforeAwait`上的递增,以及`AfterAwait`上的递减. (3认同)
  • 我接受了这个答案.@Noseratio,如果您考虑可以进行的任何改进,只需编辑它,我就会更新我的代码.从现在开始的一段时间(遗憾的是,不少于一个月)我会回来看看有关负载测试的结果,我必须先将其运行才能将其运送到生产中.让我们希望一切都按照应有的方式运行:)感谢您的帮助并与之合作! (2认同)

小智 5

简单的方法是将等待移动到using块之外

public Task<Document> GetDocumentAsync(string docId)
{
    var docClient = CreateDocumentServiceClient();
    using (new OperationContextScope(docClient.InnerChannel))
    {
        var task = docClient.GetDocumentAsync(docId);
    }
    return await task;
}
Run Code Online (Sandbox Code Playgroud)

  • 这是解决问题的真正简单的方法 (2认同)