Шаблон для вызова службы WCF с помощью async / await



я сгенерировал прокси с операции на основе задач.



как эта служба должна быть вызвана правильно (утилизация ServiceClient и OperationContext потом) с помощью async / await?



моя первая попытка была:



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


будучи ServiceHelper класс, который создает ServiceClient и OperationContextScope и избавляется от них впоследствии:



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;
}


однако это с треском провалилось при вызове двух служб одновременно с помощью следующая ошибка: "этот OperationContextScope находится в другом потоке, чем он был создан."



MSDN говорит:




не используйте асинхронный шаблон "await" в блоке OperationContextScope. Когда продолжение происходит,оно может выполняться в другом потоке, а 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);
}
}


моя проблема с его кодом заключается в том, что он никогда не вызывает Close (или Abort) на ServiceClient.



я нашел путь пропаганды OperationContextScope С помощью настраиваемого SynchronizationContext. Но, помимо того, что это много "рискованного" кода, он утверждает, что:




это стоит отметить, что у него есть несколько небольших проблем, связанных с удалением областей контекста операции (поскольку они позволяют вам только удалять их в вызывающем потоке), но это, похоже, не проблема, поскольку (по крайней мере, в соответствии с разборкой) они реализуют Dispose (), но не Finalize().




Итак, нам здесь не повезло? Существует ли проверенный шаблон для вызова служб WCF с использованием async / await и удаления обоих ServiceClient и OperationContextScope? Может быть, кто-то форма Microsoft (возможно, гуру Стивен Тоуб :)) может помочь.



спасибо!



[обновление]



С большой помощью от пользователя Noseratio, я придумал что-то, что работает: не используйте OperationContextScope. Если вы используете его по любой из этих причин, попробуйте найти обходной путь, который соответствует вашему сценарию. В противном случае, если вам действительно, действительно нужно OperationContextScope, вам придется придумать реализацию SynchronizationContext что захватывает его, и это, кажется,очень тяжело (если это вообще возможно - должна быть причина, почему это не поведение по умолчанию).



Итак, полный рабочий код:



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


С 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;
}
}
}
}


обратите внимание, что класс поддерживает расширения; возможно, вам нужно наследовать и предоставить учетные данные.



единственный возможный "попался" это то, что в GetHomeInfoAsync, вы не можете просто вернуть Task вы получаете от прокси (что должно казаться естественным, зачем создавать новый Task когда у вас уже есть один). Ну, в этом случае вам нужно await прокси Task и затем закрыть (или abort) в ServiceClient, иначе вы будете закрывать его сразу после вызова службы (в то время как байты передаются по проводам)!



хорошо, у нас есть способ заставить его работать, но было бы неплохо получить ответ из авторитетного источника, как утверждает Noseratio.

835   6  

6 ответов:

Я думаю, что возможным решением может быть использование custom 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);
    }
}

вот реализация FlowingOperationContextScope и ContinueOnScope (только немного протестировал):

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);
        }
    }
}

простой способ-переместить ожидание за пределы блока использования

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

Я решил написать свой собственный код, который помогает с этим, проводки в случае, если это помогает любому. Кажется, немного меньше ошибаться (непредвиденные гонки и т. д.) против реализации SimpleAwaiter выше, но вы будете судьей:

public static class WithOperationContextTaskExtensions
{
    public static ContinueOnOperationContextAwaiter<TResult> WithOperationContext<TResult>(this Task<TResult> @this, bool configureAwait = true)
    {
        return new ContinueOnOperationContextAwaiter<TResult>(@this, configureAwait);
    }

    public static ContinueOnOperationContextAwaiter WithOperationContext(this Task @this, bool configureAwait = true)
    {
        return new ContinueOnOperationContextAwaiter(@this, configureAwait);
    }

    public class ContinueOnOperationContextAwaiter : INotifyCompletion
    {
        private readonly ConfiguredTaskAwaitable.ConfiguredTaskAwaiter _awaiter;
        private OperationContext _operationContext;

        public ContinueOnOperationContextAwaiter(Task task, bool continueOnCapturedContext = true)
        {
            if (task == null) throw new ArgumentNullException("task");

            _awaiter = task.ConfigureAwait(continueOnCapturedContext).GetAwaiter();
        }

        public ContinueOnOperationContextAwaiter GetAwaiter() { return this; }

        public bool IsCompleted { get { return _awaiter.IsCompleted; } }

        public void OnCompleted(Action continuation)
        {
            _operationContext = OperationContext.Current;
            _awaiter.OnCompleted(continuation);
        }

        public void GetResult()
        {
            OperationContext.Current = _operationContext;
            _awaiter.GetResult();
        }
    }

    public class ContinueOnOperationContextAwaiter<TResult> : INotifyCompletion
    {
        private readonly ConfiguredTaskAwaitable<TResult>.ConfiguredTaskAwaiter _awaiter;
        private OperationContext _operationContext;

        public ContinueOnOperationContextAwaiter(Task<TResult> task, bool continueOnCapturedContext = true)
        {
            if (task == null) throw new ArgumentNullException("task");

            _awaiter = task.ConfigureAwait(continueOnCapturedContext).GetAwaiter();
        }

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

        public bool IsCompleted { get { return _awaiter.IsCompleted; } }

        public void OnCompleted(Action continuation)
        {
            _operationContext = OperationContext.Current;
            _awaiter.OnCompleted(continuation);
        }

        public TResult GetResult()
        {
            OperationContext.Current = _operationContext;
            return _awaiter.GetResult();
        }
    }
}

использование (немного руководства и вложенности не проверено...):

    /// <summary>
    /// Make a call to the service
    /// </summary>
    /// <param name="action"></param>
    /// <param name="endpoint"> </param>
    public async Task<ResultCallWrapper<TResult>> CallAsync<TResult>(Func<T, Task<TResult>> action, EndpointAddress endpoint)
    {
        using (ChannelLifetime<T> channelLifetime = new ChannelLifetime<T>(ConstructChannel(endpoint)))
        {
            // OperationContextScope doesn't work with async/await
            var oldContext = OperationContext.Current;
            OperationContext.Current = new OperationContext((IContextChannel)channelLifetime.Channel);

            var result = await action(channelLifetime.Channel)
                .WithOperationContext(configureAwait: false);

            HttpResponseMessageProperty incomingMessageProperty = (HttpResponseMessageProperty)OperationContext.Current.IncomingMessageProperties[HttpResponseMessageProperty.Name];

            string[] keys = incomingMessageProperty.Headers.AllKeys;
            var headersOrig = keys.ToDictionary(t => t, t => incomingMessageProperty.Headers[t]);

            OperationContext.Current = oldContext;

            return new ResultCallWrapper<TResult>(result, new ReadOnlyDictionary<string, string>(headersOrig));
        }
    }

Я немного запутался, я нашел этот блог:асинхронная операция на основе задач в WCF

там это асинхронная связь wcf:

[ServiceContract]
public interface IMessage
{
    [OperationContract]
    Task<string> GetMessages(string msg);
}

public class MessageService : IMessage
{
   async Task<string> IMessage.GetMessages(string msg)
   {
      var task = Task.Factory.StartNew(() =>
                                     {
                                         Thread.Sleep(10000);
                                         return "Return from Server : " + msg;
                                     });
     return await task.ConfigureAwait(false);
   }
}

клиент:

var client = new Proxy("BasicHttpBinding_IMessage");
       var task = Task.Factory.StartNew(() => client.GetMessages("Hello"));
       var str = await task;

Так это тоже хороший способ??

я столкнулся с той же проблемой, однако до меня дошло, что мне вообще не нужно использовать async/await.

поскольку вы не обрабатываете Результат после обработки, нет необходимости ждать ответа. Если вам нужно обработать результат, просто используйте старомодное продолжение TPL.

public Task<MyDomainModel> GetHomeInfoAsync(DateTime timestamp)
{
    using (var helper = new ServiceHelper<ServiceClient, ServiceContract>())
    {
        return helper.Proxy.GetHomeInfoAsync(timestamp).ContinueWith(antecedent=>processReplay(antecedent.Result));
    }
}

Я не знаю, помогает ли это, но, увидев этот вопрос в моем поиске, чтобы ответить на тот же вопрос, я наткнулся на этой.

из этого, я думаю, что ваш код должен выглядеть примерно так:

public async Task<HomeInfo> GetHomeInfoAsync(DateTime timestamp)
{
    using (var client = CreateDocumentServiceClient())
    {
        await client.BeginGetHomeInfoAsync(timestamp);
    }
}

Я понимаю, что мой ответ приходит довольно поздно :P, но это может помочь кому-то еще.

Comments

    Ничего не найдено.