c# - explained - wcf async await
Patrón para llamar al servicio WCF usando async/await (6)
Creo que una solución factible podría ser usar un awaiter personalizado para hacer fluir el nuevo contexto de operación a través de OperationContext.Current
. La implementación de OperationContext
sí misma no parece requerir afinidad de subprocesos. Aquí está el patrón:
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);
}
}
Aquí está la implementación de FlowingOperationContextScope
y ContinueOnScope
(solo ligeramente probada):
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);
}
}
}
Genere un proxy con operaciones basadas en tareas .
¿Cómo debería invocarse este servicio de forma adecuada (eliminando ServiceClient
y OperationContext
después) utilizando async / await?
Mi primer intento fue:
public async Task<HomeInfo> GetHomeInfoAsync(DateTime timestamp)
{
using (var helper = new ServiceHelper<ServiceClient, ServiceContract>())
{
return await helper.Proxy.GetHomeInfoAsync(timestamp);
}
}
Siendo ServiceHelper
una clase que crea ServiceClient
y OperationContextScope
y los descarta después:
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;
}
Sin embargo, esto falló miserablemente al llamar a dos servicios al mismo tiempo con el siguiente error: "Este OperationContextScope se descarta en un hilo diferente de lo que se creó".
MSDN dice:
No utilice el patrón "en espera" asincrónico dentro de un bloque OperationContextScope. Cuando se produce la continuación, puede ejecutarse en un subproceso diferente y OperationContextScope es específico del subproceso. Si necesita llamar a "aguardar" para una llamada asíncrona, úselo fuera del bloque OperationContextScope.
¡Así que ese es el problema! Pero, ¿cómo lo arreglamos correctamente?
Este chico hizo exactamente lo que dice 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);
}
}
Mi problema con su código es que nunca llama a Close (o Abort) en el ServiceClient.
También encontré una forma de propagar OperationContextScope
utilizando un SynchronizationContext
personalizado. Pero, además del hecho de que es una gran cantidad de código "arriesgado", afirma que:
Vale la pena señalar que tiene algunos pequeños problemas con respecto a la eliminación de ámbitos de contexto de operación (ya que solo le permiten disponer de ellos en el hilo de llamada), pero esto no parece ser un problema ya que (al menos de acuerdo con el desmontaje), implementan Dispose () pero no Finalize ().
Entonces, ¿no tenemos suerte aquí? ¿Existe un patrón comprobado para llamar a los servicios de WCF utilizando async / await Y disponer tanto de ServiceClient
como de OperationContextScope
? Tal vez alguien de Microsoft (quizás el gurú Stephen Toub :) pueda ayudar.
¡Gracias!
[ACTUALIZAR]
Con mucha ayuda del usuario Noseratio, se me ocurrió algo que funciona: no use OperationContextScope
. Si lo está utilizando por alguna de these razones, intente encontrar una solución que se adapte a su situación. De lo contrario, si realmente necesitas OperationContextScope
, tendrás que crear una implementación de SynchronizationContext
que la capture, y eso parece muy difícil (si es posible, debe haber una razón por la cual este no sea el comportamiento por defecto).
Entonces, el código de trabajo completo es:
public async Task<HomeInfo> GetHomeInfoAsync(DateTime timestamp)
{
using (var helper = new ServiceHelper<ServiceClient, ServiceContract>())
{
return await helper.Proxy.GetHomeInfoAsync(timestamp);
}
}
Con ServiceHelper
siendo:
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;
}
}
}
}
Tenga en cuenta que la clase admite extensiones; quizás necesite heredar y proporcionar credenciales.
El único posible "error" es que en GetHomeInfoAsync
, no puedes simplemente devolver la Task
que obtienes del proxy (lo cual debería parecer natural, por qué crear una nueva Task
cuando ya la tienes). Bueno, en este caso debe await
la Task
proxy y luego cerrar (o abortar) el ServiceClient
, de lo contrario lo cerrará inmediatamente después de invocar el servicio (¡mientras que los bytes se envían por cable)!
De acuerdo, tenemos una forma de hacerlo funcionar, pero sería bueno obtener una respuesta de una fuente autorizada, como dice Noseratio.
Decido escribir mi propio código que ayuda con esto, publicar en caso de que esto ayude a alguien. Parece ser un poco menos equivocarse (carreras imprevistas, etc.) frente a la implementación de SimpleAwaiter anterior, pero usted es el juez:
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();
}
}
}
Uso (un pequeño manual y anidación no probados ...):
/// <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));
}
}
Estoy un poco confundido, encontré este Blog: Operación asincrónica basada en tareas en WCF
Aquí esta es una comunicación async 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);
}
}
Cliente:
var client = new Proxy("BasicHttpBinding_IMessage");
var task = Task.Factory.StartNew(() => client.GetMessages("Hello"));
var str = await task;
Entonces, ¿esta también es una buena manera?
Manera simple es mover la espera fuera del bloque de uso
public Task<Document> GetDocumentAsync(string docId)
{
var docClient = CreateDocumentServiceClient();
using (new OperationContextScope(docClient.InnerChannel))
{
var task = docClient.GetDocumentAsync(docId);
}
return await task;
}
Me encontré con el mismo problema, sin embargo, caí en la cuenta de que no necesitaba usar async / await en absoluto.
Como no está procesando el resultado, no hay necesidad de esperar la respuesta. Si necesita procesar el resultado, solo use la continuación de TPL de la vieja moda.
public Task<MyDomainModel> GetHomeInfoAsync(DateTime timestamp)
{
using (var helper = new ServiceHelper<ServiceClient, ServiceContract>())
{
return helper.Proxy.GetHomeInfoAsync(timestamp).ContinueWith(antecedent=>processReplay(antecedent.Result));
}
}
No sé si esto ayuda, pero después de ver esta pregunta en mi búsqueda para responder la misma pregunta, me encontré con this .
A partir de eso, creo que tu código debería verse más o menos así:
public async Task<HomeInfo> GetHomeInfoAsync(DateTime timestamp)
{
using (var client = CreateDocumentServiceClient())
{
await client.BeginGetHomeInfoAsync(timestamp);
}
}
Me doy cuenta de que mi respuesta llega un poco tarde: P, pero podría ayudar a alguien más.