web-dev-qa-db-fra.com

Modèle d'appel du service WCF à l'aide de async / wait

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

Comment ce service devrait-il être appelé correctement (en éliminant ensuite ServiceClient et le OperationContext) en utilisant async/wait?

Ma première tentative a été:

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

Etre ServiceHelper une classe qui crée le ServiceClient et le OperationContextScope et en dispose par la suite:

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 simultané de deux services avec l'erreur suivante: "Ce OperationContextScope est en cours de traitement dans un autre thread que celui où il a été créé."

[~ # ~] msdn [~ # ~] dit:

N'utilisez pas le modèle "en attente" asynchrone dans un bloc OperationContextScope. Lorsque la continuation se produit, il peut s'exécuter sur un thread différent et OperationContextScope est spécifique à un thread. Si vous devez appeler "attendre" pour un appel asynchrone, utilisez-le en dehors du bloc OperationContextScope.

C'est donc le problème! Mais, comment pouvons-nous résoudre ce problème correctement?

Ce gars a fait exactement ce que MSDN dit :

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

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

J'ai aussi trouvé un moyen de propager le OperationContextScope à l'aide d'un SynchronizationContext personnalisé. Mais, outre le fait qu'il y ait beaucoup de code "risqué", il déclare que:

Il convient de noter qu'il présente quelques problèmes mineurs concernant la disposition des portées de contexte d'opération (puisqu'elles vous permettent uniquement de les disposer sur le fil d'appel), mais cela ne semble pas être un problème depuis (du moins selon désassemblage), ils implémentent Dispose () mais pas Finalize ().

Alors, avons-nous de la chance ici? Existe-t-il une méthode éprouvée pour appeler des services WCF en utilisant asynchrone/wait ET en éliminant À LA FOIS le ServiceClient et le OperationContextScope? Peut-être que quelqu'un de Microsoft (peut-être le gourou Stephen Toub :)) peut-il aider.

Merci!

[UPDATE]

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 des raisons this , essayez de trouver une solution de contournement adaptée à votre scénario. Sinon, si vous avez vraiment, vraiment, besoin de OperationContextScope, vous devrez trouver une implémentation de SynchronizationContext qui le capture et qui semble very hard (si possible - il doit y avoir une raison pour laquelle ce n'est pas le comportement par défaut).

Donc, le code de travail complet est:

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

Avec ServiceHelper étant:

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

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

Le seul "gotcha" possible est que dans GetHomeInfoAsync, vous ne pouvez pas simplement renvoyer le Task que vous obtenez du proxy (ce qui devrait sembler naturel, pourquoi créer un nouveau Task quand vous en avez déjà un). Eh bien, dans ce cas, vous devez await le proxy Task et alors fermer (ou abandonner) le ServiceClient, sinon vous aurez fermez-le immédiatement après avoir appelé le service (pendant que des octets sont envoyés sur 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 le dit Noseratio.

57
gabrielmaldi

Je pense qu'une solution réalisable pourrait être d'utiliser un attente personnalisé pour transmettre le nouveau contexte d'opération via OperationContext.Current. Le implémentation de 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<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);
        }
    }
}
30
noseratio

Un moyen simple est de déplacer l'attente en dehors du bloc using

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

Je décide d'écrire mon propre code qui aide à cela, en écrivant au cas où cela aiderait quelqu'un. Cela semble un peu moins difficile de se tromper (courses imprévues, etc.) par rapport à la mise en œuvre de SimpleAwaiter ci-dessus, mais à vous de juger:

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

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

    /// <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));
        }
    }
2
jamespconnor

Le flux async est pris en charge à partir de .Net 4.6.2.

Nous avons une application ASP.Net WebApi sous .Net 4.6 où nous avons utilisé la réponse acceptée. TaskScheduler.FromCurrentSynchronizationContext() a généré des problèmes de blocage lorsque le contexte de synchronisation actuel est AspNetSynchronizationContext.

Je crois que la tâche de continuation a été mise en file d'attente après la tâche proprement dite, ce qui a pour conséquence que la tâche en attente attend la continuation, alors que la tâche de continuation doit être exécutée pour terminer la tâche. c'est-à-dire que les tâches sont en attente l'une de l'autre.

J'ai donc résolu le problème en modifiant l'utilisation de la tâche de continuation pour utiliser TaskAwaiter. Voir: https://blogs.msdn.Microsoft.com/lucian/2012/12/11/how-to-write-a-custom-awaiter/

2
weichch

Cela fait longtemps que je ne parle pas de ça, mais je vais vous parler de ma propre solution maison.

Si cela ne vous dérange pas de vous passer de OperationContextScope, vous pourriez envisager quelque chose dans ce sens:

méthodes d'extension

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Text;
using System.Threading.Tasks;

namespace Intexx.ServiceModel
{
    public static class WcfExtensions
    {
        [DebuggerStepThrough]
        public static void Call<TChannel>(this TChannel Client, Action<TChannel> Method) where TChannel : ICommunicationObject
        {
            try
            {
                Method.Invoke(Client);
            }
            finally
            {
                Cleanup(Client);
            }
        }

        [DebuggerStepThrough]
        public static TResult Call<TChannel, TResult>(this TChannel Client, Func<TChannel, TResult> Method) where TChannel : ICommunicationObject
        {
            try
            {
                return Method.Invoke(Client);
            }
            finally
            {
                Cleanup(Client);
            }
        }

        [DebuggerStepThrough]
        public async static Task CallAsync<TChannel>(this TChannel Client, Func<TChannel, Task> Method) where TChannel : ICommunicationObject
        {
            try
            {
                await Method.Invoke(Client);
            }
            finally
            {
                Cleanup(Client);
            }
        }

        [DebuggerStepThrough]
        public async static Task<TResult> CallAsync<TChannel, TResult>(this TChannel Client, Func<TChannel, Task<TResult>> Method) where TChannel : ICommunicationObject
        {
            try
            {
                return await Method.Invoke(Client);
            }
            finally
            {
                Cleanup(Client);
            }
        }

        private static void Cleanup<TChannel>(TChannel Client) where TChannel : ICommunicationObject
        {
            try
            {
                if (Client.IsNotNull)
                {
                    if (Client.State == CommunicationState.Faulted)
                        Client.Abort();
                    else
                        Client.Close();
                }
            }
            catch (Exception ex)
            {
                Client.Abort();

                if (!ex is CommunicationException && !ex is TimeoutException)
                    throw new Exception(ex.Message, ex);
            }

            finally
            {
                Client = null;
            }
        }
    }
}

Classe client

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Text;
using System.Threading.Tasks;

namespace Reader
{
    public class Client
    {
        public static CemReaderClient Create()
        {
            Tuple<Channels.Binding, EndpointAddress, double> oService;

            try
            {
                oService = Main.Services(typeof(ICemReader));
                return new CemReaderClient(oService.Item1, oService.Item2);
            }
            catch (KeyNotFoundException ex)
            {
                return null;
            }
        }
    }
}

sage (en VB, le code ne convertissant pas)

Using oReader As Reader.CemReaderClient = Reader.Client.Create
  If oReader.IsNotNothing Then
    Dim lIsReading = Await oReader.CallAsync(Function(Reader As Reader.CemReaderClient)
                                               Me.ConfigFilePath = If(Me.ConfigFilePath, Reader.GetConfigFilePath)
                                               Me.BackupDrive = If(Me.BackupDrive, Reader.GetBackupDrive)
                                               Me.SerialPort = If(Me.SerialPort, Reader.GetSerialPort)
                                               Me.LogFolder = If(Me.LogFolder, Reader.GetLogFolder)

                                               Return Reader.GetIsReadingAsync
                                             End Function)
  End If
End Using

Cette production a fonctionné de manière fiable en production avec des charges de fréquence d’environ 15 appels/s côté client (c’est aussi rapide que le traitement en série le permet). C'était sur un seul fil, cependant - cela n'a pas été rigoureusement testé pour la sécurité du fil. YMMV.

Dans mon cas, j'ai décidé d'incorporer les méthodes d'extension dans leur propre paquet privé NuGet. L'ensemble de la construction s'est avéré très pratique.

Ceci devra évidemment être réévalué si OperationContextScope finit par être nécessaire.

Le bit avec le Tuple dans la classe Client concerne la prise en charge de Service Discovery. Si quelqu'un souhaite également voir ce code, criez-le et je mettrai à jour ma réponse.

1
InteXX

Je suis un peu confus, j'ai trouvé ce blog: Opération asynchrone basée sur une tâche dans WCF

Il s’agit d’une communication 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);
   }
}

Client:

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

Alors est-ce aussi un bon moyen ??

0
Franki1986