Шаблон для вызова службы 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.
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