code

async / await를 사용하여 WCF 서비스를 호출하기위한 패턴

codestyles 2020. 12. 13. 09:34
반응형

async / await를 사용하여 WCF 서비스를 호출하기위한 패턴


작업 기반 작업으로 프록시를 생성했습니다 .

async / await를 사용 하여이 서비스를 어떻게 적절하게 호출 ( ServiceClientOperationContext이후 처리)해야합니까?

나의 첫 번째 시도는 :

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

ServiceHelper생성하는 클래스 ServiceClientOperationContextScope그들과 처분하는 이후 :

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

그러나 "이 OperationContextScope는 생성 된 스레드와 다른 스레드에서 삭제되고 있습니다."라는 오류와 함께 두 서비스를 동시에 호출 할 때 비참하게 실패했습니다.

MSDN 말한다 :

OperationContextScope 블록 내에서 비동기 "대기"패턴을 사용하지 마십시오. 연속이 발생하면 다른 스레드에서 실행될 수 있으며 OperationContextScope는 스레드에 따라 다릅니다. 비동기 호출을 위해 "await"를 호출해야하는 경우 OperationContextScope 블록 외부에서 사용하십시오.

그게 문제입니다! 그러나 우리는 그것을 어떻게 올바르게 고칠까요?

이 사람은 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);
  }
}

그의 코드에 대한 내 문제는 그가 ServiceClient에서 Close (또는 Abort)를 호출하지 않는다는 것입니다.

나는 또한 사용자 정의를 사용하여 전파 하는 방법찾았 습니다 . 그러나 "위험한"코드가 많다는 사실 외에도 그는 다음과 같이 말합니다.OperationContextScopeSynchronizationContext

작업 컨텍스트 범위의 처리와 관련하여 몇 가지 작은 문제가 있다는 점은 주목할 가치가 있지만 (호출 스레드에서만 처리 할 수 ​​있기 때문에) 이것은 문제가되지 않는 것 같습니다 (적어도 디스 어셈블리), 그들은 Dispose ()를 구현하지만 Finalize ()는 구현하지 않습니다.

그래서 우리는 여기서 운이 좋지 않습니까? async / await를 사용하여 WCF 서비스를 호출하고 ServiceClient둘 다 폐기하는 입증 된 패턴 OperationContextScope있습니까? 누군가 Microsoft (아마도 전문가 Stephen Toub :))를 형성하면 도움이 될 수 있습니다.

감사!

[최신 정보]

사용자 Noseratio의 많은 도움을 받아 작동하는 것을 생각해 냈습니다 OperationContextScope. 이러한 이유로 사용하는 경우 시나리오에 맞는 해결 방법을 찾으십시오. 그렇지 않으면 정말로 필요하다면 그것을 포착 OperationContextScope하는의 구현을 생각해 내야 SynchronizationContext할 것입니다. 그리고 그것은 매우 어려워 보입니다 (가능하다면-이것이 기본 동작이 아닌 이유가 있어야합니다) ).

따라서 전체 작업 코드는 다음과 같습니다.

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

ServiceHelper되는 :

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

클래스는 확장을 지원합니다. 자격 증명을 상속하고 제공해야 할 수도 있습니다.

유일한 가능한 "gotcha"는 에서 프록시에서 얻은 GetHomeInfoAsync값을 반환 할 수 없다는 Task것입니다 (자연스러워 보일 것 Task입니다. 이미 하나가 있는데 새 파일을 만드는 이유 ). 글쎄,이 경우 await프록시 Task에 접근 한 다음을 닫아야합니다 (또는 중단). ServiceClient그렇지 않으면 서비스를 호출 한 후 즉시 닫을 것입니다 (바이트가 유선을 통해 전송되는 동안)!

좋아, 우리는 그것을 작동시킬 방법이 있지만 Noseratio가 말했듯이 권위있는 출처로부터 답변을 얻는 것이 좋을 것입니다.


실행 가능한 솔루션은 사용자 지정 awaiter사용하여 OperationContext.Current. 의 구현OperationContext 자체는 스레드 선호도를 필요로 표시되지 않습니다. 패턴은 다음과 같습니다.

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

다음은 FlowingOperationContextScopeContinueOnScope(약간 테스트) 의 구현입니다 .

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

간단한 방법은 사용 블록 외부로 대기를 이동하는 것입니다.

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

나는 이것에 도움이되는 내 자신의 코드를 작성하기로 결정했다. 위의 SimpleAwaiter 구현에 비해 잘못되는 것 (예상치 못한 경주 등)이 조금 덜한 것처럼 보이지만 당신이 판단합니다.

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

사용법 (약간의 수동 및 중첩은 테스트되지 않음 ...) :

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

비동기 흐름은 .Net 4.6.2에서 지원됩니다.

우리는 받아 들여진 대답을 사용한 .Net 4.6에서 실행되는 ASP.Net WebApi 애플리케이션이 있습니다. TaskScheduler.FromCurrentSynchronizationContext()현재 동기화 컨텍스트가 인 경우 교착 상태 문제가 발생했습니다 AspNetSynchronizationContext.

연속 작업은 실제 작업 후에 대기열에 추가되어 실제 작업이 연속을 기다리고있는 반면 연속 작업은 실제 작업을 완료하기 위해 실행되어야한다고 생각합니다. 즉, 작업이 모두 서로를 기다리고 있습니다.

그래서 계속 작업을 사용하여 TaskAwaiter를 사용하도록 변경하여 문제를 해결했습니다. 참조 : https://blogs.msdn.microsoft.com/lucian/2012/12/11/how-to-write-a-custom-awaiter/


이것에 대해서는 한참이 지났지 만 집에서 만든 솔루션으로 차임 할 것입니다.

없이도 괜찮다면 OperationContextScope다음과 같은 사항을 고려할 수 있습니다.

확장 방법

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

클라이언트 클래스

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

사용법 (코드가 변환되지 않으므로 VB에서)

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

I've had this running reliably in production under frequency loads of around 15 calls/sec on the client side (that's as fast as serial processing would allow). That was on a single thread, though—this hasn't been rigorously tested for thread safety. YMMV.

In my case, I decided to roll the extension methods into their own private NuGet package. The whole construct has turned out to be pretty handy.

This will have to be reevaluated, of course, if OperationContextScope ever ends up being needed.

The bit with the Tuple in the Client class is for Service Discovery support. If anyone would like to see that code as well, give a shout and I'll update my answer.


I am a little bit confused, I found this Blog : Task-based asynchronous operation in WCF

There this is a async wcf communication:

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

So is this also a good way??


I ran into the same issue, however it dawned on me that I didn't need to use async/await at all.

Since you are not post processing the result, there is no need to wait for the reply. If you do need to process the result, just use the old fashion TPL continuation.

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

I don't know if this helps, but after seeing this question on my search to answer the same question, I came upon this.

Leading from that, I should think your code should look something like this:

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

I realise my answer comes rather late :P but it might help someone else.

참고URL : https://stackoverflow.com/questions/18284998/pattern-for-calling-wcf-service-using-async-await

반응형