62 votes

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

J'ai généré un proxy avec de la tâche en fonction des opérations.

Comment ce service devrait-il être appelé correctement (élimination de l' ServiceClient et de la OperationContext par la suite) à l'aide de async/await?

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

Cours d' ServiceHelper d'une classe qui crée l' ServiceClient et de la OperationContextScope , et dispose d'entre eux 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;
}

Cependant, cela a échoué lamentablement lors de l'appel de deux services en même temps avec l'erreur suivante: "Ce OperationContextScope est disposé sur un autre fil qu'il a été créé."

MSDN dit:

Ne pas utiliser l'asynchrone "en attente de" motif à l'intérieur d'un OperationContextScope bloc. Lorsque la poursuite a lieu, il peut s'exécuter sur un thread différent et OperationContextScope est thread spécifique. Si vous devez appeler le "en attente" pour un appel asynchrone, utilisation à l'extérieur de la OperationContextScope bloc.

Donc, c'est ça le problème! Mais, comment pouvons-nous résoudre correctement?

Ce mec n'a juste ce 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 Fermer (ou Abandonner) sur le ServiceClient.

J'ai aussi trouvé un moyen de propagation de l' OperationContextScope aide personnalisée SynchronizationContext. Mais, outre le fait que c'est beaucoup de "risqué" de code, il affirme que:

Il est intéressant de noter que il possède quelques petites questions au sujet de l'élimination de l'opération-cadre étendues (puisqu'ils ne vous permettent de les disposer sur le thread appelant), mais cela ne semble pas être un problème depuis (au moins selon le démontage), ils mettent en œuvre Dispose (), mais pas Finalize().

Alors, sommes-nous hors de la chance ici? Est-il un schéma éprouvé pour appeler des services WCF avec async/await ET de disposer à la FOIS de l' ServiceClient et de la OperationContextScope? Peut-être quelqu'un formulaire Microsoft (peut-être le gourou de Stephen Toub :)) peut vous aider.

Merci!

[Mise à JOUR]

Avec beaucoup d'aide de la part de l'utilisateur Noseratio, je suis venu avec quelque chose qui fonctionne: ne pas utiliser OperationContextScope. Si vous l'utilisez pour une de ces raisons, essayez de trouver une solution qui convient à votre scénario. Sinon, si vous avez vraiment, vraiment, besoin OperationContextScope, vous aurez à venir avec une mise en œuvre d'un SynchronizationContext qui le capture, et qui semble très dur (si possible, il doit y avoir une raison pour laquelle ce n'est pas le comportement par défaut).

Donc, le travail plein de code est:

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

Avec ServiceHelper être:

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 vous avez besoin d'hériter et de fournir des informations d'identification.

Le seul "piège" est que, en GetHomeInfoAsync, vous ne pouvez pas il suffit de retourner l' Task vous obtenez à partir du proxy (ce qui devrait sembler naturel, pourquoi créer un nouveau Task lorsque vous en avez déjà un). Eh bien, dans ce cas, vous avez besoin d' await le proxy Task et ensuite fermer (ou abandon) le ServiceClient, sinon, vous serez le fermer tout de suite après l'invocation du service (alors que les octets sont envoyés sur le fil)!

OK, nous avons un moyen de le faire fonctionner, mais il serait bien d'obtenir une réponse à partir d'une source faisant autorité, comme Noseratio unis.

37voto

Noseratio Points 23840

Je pense qu'une solution pourrait être d'utiliser un custom awaiter à couler le nouveau contexte d'opération via OperationContext.Current. La mise en œuvre de l' OperationContext lui-même ne semble pas exiger que l'affinité de thread. Voici le modèle:

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

Ici est la mise en œuvre de l' FlowingOperationContextScope et ContinueOnScope (seulement un peu 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);
        }
    }
}

7voto

Noseratio Points 23840

A mon avis, cela dépend de votre côté client thread de l'environnement pour faire async WCF appel. Il ne devrait pas être un problème si vous les a appelés à partir d'un thread d'INTERFACE utilisateur, parce que dans ce cas, vous auriez du être garanti qu' await de continuation de ne pas passer les fils.

Dans le cas ci-dessus, je suppose que vous appelez votre WCF proxy à partir d'un service Windows (peut être une application de console, un enfant ou aléatoire de pool de threads). Il ce cas, la valeur par Défaut (pool de threads) SynchronizationContext contexte est utilisé, où un fil commutateur en effet peut se produire après l' await, rompant ainsi avec l' OperationContextScope objet qui n'est pas conçu pour le multi-thread accès ([ÉDITÉ]: nous avons clarifié, il est appelé à partir de ASP.NET l'API Web code, AspNetSynchronizationContext est utilisé ici).

[ÉDITÉ]

Pour coller avec la convinient using modèle pour l' OperationContextScope, il semble que vous avez réellement besoin pour configurer votre propre coutume SynchronizationContext qui constituerait un comportement semblable à l' DispatcherSynchronizationContext pour await poursuite, avant de faire un appel asynchrone. Je ne suis pas sûr si c'est une solution possible. J'ai besoin de creuser un peu plus, j'espère que quelqu'un peut donner une meilleure réponse.

[Mise à JOUR]

Le ce-mec-a-tout-ce-MSDN-dit code que vous l'avez mentionné semble être à peu de mal, parce que très probablement, la nouvelle - OperationContext seraient créés et éliminés avant le WCF appel asynchrone tâche pourraient même avoir une chance de commencer:

  var docClient = CreateDocumentServiceClient();
  using (new OperationContextScope(docClient.InnerChannel))
  {
    return docClient.GetDocumentAsync(docId);
  }

J'ai brièvement regardé OperationContext mise en œuvre à l'aide de Réflecteur. Il stocke son état sur une base par thread de par leur conception. Ce qui à mon avis signifie que, indépendamment de l' OperationContextScope wrapper, si vous avez vraiment besoin d'utiliser OperationContext, vous avez à venir avec une coutume SynchronizationContext mise en œuvre pour éviter de fil à passer.

[Mise à JOUR] Nous nous approchons, maintenant que nous savons que l'appel de l'environnement est ASP.NET. En effet, AspNetSynchronizationContext ne fournissent pas nécessairement le même thread pour async l'achèvement de la tâche rappels (et je viens d'apprendre que lors de la lecture de ce, de ce et la re-lecture de Stephen article). Qui font sens, sinon il serait éventuellement bloquer le thread appelant (quand il n'y a plus async travail), au lieu de retourner à la piscine.

Je pense que votre décision de se débarrasser de l' OperationContextScope (et de ne pas dépendre des en-têtes HTTP) est probablement la meilleure façon de gérer la situation. Je crois que vous pouvez rebord d'utiliser using ou try/catch/finally d'appel Close ou Abort sur votre proxy WCF, parce que son code est thread-safe et thread indépendant (contrairement à OperationContext).

Toujours en attente d'une réponse ou des commentaires à partir de sources fiables.

-1voto

Khoi Pham Points 39

J'ai rencontré le même problème, mais il m'est apparu que je n'avais pas du tout besoin d'utiliser async / wait.

Puisque vous ne post-traitez pas le résultat, il n’est pas nécessaire d’attendre la réponse. Si vous avez besoin de traiter le résultat, utilisez simplement la continuation de l'ancienne méthode TPL.

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

-1voto

TMVector Points 19

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

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

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

Je me rends compte que ma réponse arrive assez tard: P mais cela pourrait aider quelqu'un d'autre.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X