Utilizing Async Await for Invoking WCF Services in a Structured Manner

It is possible for both the client and server to operate synchronously, use asynchronous code with APM, or use asynchronous code with TAP. By using this method, you can wait for the message to be sent (in case of delays) and identify any communication errors in the channel (if a reliable channel is being used). It should be noted that this does not involve waiting for a response.


Solution 1:

To transmit the new operation context through

OperationContext.Current

, using a custom awaiter seems like a practical resolution. It seems that the implementation of

OperationContext

doesn’t call for thread affinity. This is the suggested approach:

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

The following code snippets, namely

FlowingOperationContextScope

and

ContinueOnScope

, have been implemented and subjected to limited testing.

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


Solution 2:


To avoid redundancy, one can relocate the “await” statement outside the “using” block.

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


Solution 3:


To assist with this, I opt to create my personal code and share it in case it might benefit someone. It appears to have a lower probability of encountering unexpected races compared to the SimpleAwaiter implementation mentioned earlier, but it’s up to you to decide.

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

Instructions (with limited guidance and untested nesting) on how to use it:

    /// 
    /// 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];
            string[] keys = incomingMessageProperty.Headers.AllKeys;
            var headersOrig = keys.ToDictionary(t => t, t => incomingMessageProperty.Headers[t]);
            OperationContext.Current = oldContext;
            return new ResultCallWrapper(result, new ReadOnlyDictionary(headersOrig));
        }
    }

Frequently Asked Questions

Posted in Uncategorized