Motif pour appeler le service WCF en utilisant async / waiting

J’ai généré un proxy avec des opérations basées sur des tâches .

Comment ce service devrait-il être appelé correctement (en supprimant ServiceClient et OperationContext après) en utilisant async / wait?

Ma première tentative était:

 public async Task GetHomeInfoAsync(DateTime timestamp) { using (var helper = new ServiceHelper()) { return await helper.Proxy.GetHomeInfoAsync(timestamp); } } 

Étant ServiceHelper une classe qui crée le ServiceClient et le OperationContextScope et les dispose ensuite:

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

Toutefois, cela a échoué lamentablement lors de l’appel de deux services en même temps avec l’erreur suivante: “Cette OperationContextScope est en cours d’élimination sur un thread différent de celui créé.”

MSDN dit:

N’utilisez pas le modèle asynchrone «wait» dans un bloc OperationContextScope. Lorsque la continuation se produit, elle peut s’exécuter sur un thread différent et OperationContextScope est spécifique au thread. Si vous devez appeler «wait» pour un appel asynchrone, utilisez-le en dehors du bloc OperationContextScope.

Donc c’est le problème! Mais comment pouvons-nous le réparer correctement?

Ce gars a fait exactement ce que MSDN dit :

 private async void DoStuffWithDoc(ssortingng docId) { var doc = await GetDocumentAsync(docId); if (doc.YadaYada) { // more code here } } public Task GetDocumentAsync(ssortingng docId) { var docClient = CreateDocumentServiceClient(); using (new OperationContextScope(docClient.InnerChannel)) { return docClient.GetDocumentAsync(docId); } } 

Mon problème avec son code, c’est qu’il n’appelle jamais Close (ou Abort) sur le ServiceClient.

J’ai également trouvé un moyen de propager OperationContextScope utilisant un SynchronizationContext personnalisé. Mais, outre le fait qu’il s’agit d’un code “risqué”, il déclare que:

Il est intéressant de noter qu’il ya quelques petits problèmes concernant la disposition des scopes de contexte d’exploitation (car elles ne vous permettent de les disposer que sur le thread appelant), mais cela ne semble pas être un problème car (au moins selon le désassemblage), ils implémentent Dispose () mais pas Finalize ().

Alors, on a pas de chance ici? Existe-t-il un modèle éprouvé pour appeler les services WCF en utilisant async / wait AND en disposant les deux servicesClient et OperationContextScope ? Peut-être que quelqu’un de Microsoft (peut-être le gourou Stephen Toub :)) peut aider.

Merci!

[METTRE À JOUR]

Avec l’aide de l’utilisateur Noseratio, j’ai trouvé quelque chose qui fonctionne: n’utilisez pas OperationContextScope . Si vous l’utilisez pour l’une de ces raisons, essayez de trouver une solution adaptée à votre scénario. Sinon, si vous avez vraiment besoin de OperationContextScope , vous devrez mettre en place une implémentation d’un SynchronizationContext qui le capture, et cela semble très difficile (si possible – il doit y avoir une raison pour laquelle ce n’est pas le cas). comportement par défaut).

Le code de travail complet est donc:

 public async Task GetHomeInfoAsync(DateTime timestamp) { using (var helper = new ServiceHelper()) { return await helper.Proxy.GetHomeInfoAsync(timestamp); } } 

Avec ServiceHelper étant:

 public class ServiceHelper : IDisposable where TServiceClient : ClientBase, 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; } } } } 

Notez que la classe prend en charge l’extension; peut-être avez-vous besoin d’hériter et de fournir des informations d’identification.

Le seul “possible” est que dans GetHomeInfoAsync , vous ne pouvez pas simplement renvoyer la Task vous obtenez depuis le proxy (ce qui devrait sembler naturel, pourquoi créer une nouvelle Task quand vous en avez déjà une). Eh bien, dans ce cas, vous devez await la Task proxy, puis fermer (ou annuler) le ServiceClient , sinon vous allez le fermer immédiatement après avoir appelé le service (pendant que les octets sont envoyés via le réseau)!

OK, nous avons un moyen de le faire fonctionner, mais ce serait bien d’obtenir une réponse d’une source faisant autorité, comme l’indique Noseratio.

Je pense qu’une solution réalisable pourrait consister à utiliser un serveur personnalisé pour diffuser le nouveau contexte d’opération via OperationContext.Current . L’ implémentation d’ OperationContext lui-même ne semble pas nécessiter d’affinité de thread. Voici le motif:

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

Voici l’implémentation de FlowingOperationContextScope et ContinueOnScope (seulement légèrement testé):

 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 ContinueOnScope(this Task @this, FlowingOperationContextScope scope) { return new SimpleAwaiter(@this, scope.BeforeAwait, scope.AfterAwait); } // awaiter public class SimpleAwaiter : System.Runtime.ComstackrServices.INotifyCompletion { readonly Task _task; readonly Action _beforeAwait; readonly Action _afterAwait; public SimpleAwaiter(Task task, Action beforeAwait, Action afterAwait) { _task = task; _beforeAwait = beforeAwait; _afterAwait = afterAwait; } public SimpleAwaiter 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); } } } 

Le moyen le plus simple est de déplacer l’attente hors du bloc

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

Je décide d’écrire mon propre code qui aide à cela, en postant au cas où cela aiderait quelqu’un. Semble un peu moins se tromper (courses imprévues, etc.) que l’implémentation de SimpleAwaiter ci-dessus, mais vous êtes le juge:

 public static class WithOperationContextTaskExtensions { public static ContinueOnOperationContextAwaiter WithOperationContext(this Task @this, bool configureAwait = true) { return new ContinueOnOperationContextAwaiter(@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 : 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 TResult GetResult() { OperationContext.Current = _operationContext; return _awaiter.GetResult(); } } } 

Utilisation (un peu de manuel et l’imbrication n’est pas testée …):

  ///  /// Make a call to the service ///  ///  ///   public async Task> CallAsync(Func> action, EndpointAddress endpoint) { using (ChannelLifetime channelLifetime = new ChannelLifetime(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]; ssortingng[] keys = incomingMessageProperty.Headers.AllKeys; var headersOrig = keys.ToDictionary(t => t, t => incomingMessageProperty.Headers[t]); OperationContext.Current = oldContext; return new ResultCallWrapper(result, new ReadOnlyDictionary(headersOrig)); } } 

Je suis un peu confus, j’ai trouvé ce blog: Opération asynchrone basée sur des tâches dans WCF

Voici une communication asynchrone:

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

Client:

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

Est-ce que c’est aussi un bon moyen?

J’ai rencontré le même problème, mais je me suis rendu compte que je n’avais pas besoin d’utiliser async / waiting.

Puisque vous ne postez pas le résultat, vous n’avez pas besoin d’attendre la réponse. Si vous avez besoin de traiter le résultat, utilisez simplement l’ancienne continuation TPL.

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

Je ne sais pas si cela aide, mais après avoir vu cette question dans ma recherche pour répondre à la même question, je suis tombé sur cela .

À partir de là, je pense que votre code devrait ressembler à ceci:

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

Je réalise que ma réponse est plutôt tardive: P mais cela pourrait aider quelqu’un d’autre.