Я создал прокси с операциями на основе задач.
Как правильно вызвать этот сервис (избавиться от ServiceClient
и OperationContext
впоследствии), используя async/await?
Моя первая попытка была:
public async Task<HomeInfo> GetHomeInfoAsync(DateTime timestamp)
{
using (var helper = new ServiceHelper<ServiceClient, ServiceContract>())
{
return await helper.Proxy.GetHomeInfoAsync(timestamp);
}
}
Будучи ServiceHelper
классом, который создает ServiceClient
и OperationContextScope
и избавляется от них впоследствии:
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 говорит:
Не используйте асинхронный шаблон 'await' в блоке 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);
}
}
Моя проблема с его кодом заключается в том, что он никогда не вызывает Close (или Abort) на ServiceClient.
Я также нашел способ распространения OperationContextScope
с помощью пользовательского SynchronizationContext
. Но, помимо того, что в нем много "рискованного" кода, он утверждает, что:
Стоит отметить, что у него есть несколько небольших проблем, связанных с удалением областей контекста операции (так как они позволяют вам размещать их только в вызывающем потоке), но это, похоже, не проблема, поскольку (по крайней мере, в соответствии с разборкой)), они реализуют Dispose(), но не Finalize().
Итак, нам здесь не повезло? Существует ли проверенный шаблон для вызова служб WCF с использованием async/await И утилизации ОБА ServiceClient
и OperationContextScope
? Может быть, кто-то из Microsoft (возможно, гуру Стивен Тауб :)) может помочь.
Спасибо!
[ОБНОВЛЕНИЕ]
С большой помощью от пользователя 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 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;
}
}
}
}
Обратите внимание, что класс поддерживает расширение; возможно, вам нужно унаследовать и предоставить учетные данные.
Единственная возможная "ошибка" заключается в том, что в GetHomeInfoAsync
вы не можете просто вернуть Task
, который вы получаете от прокси-сервера (что должно показаться естественным, зачем создавать новый Task
, когда он у вас уже есть). Что ж, в этом случае вам нужно await
прокси Task
, а затем закрыть (или прервать) ServiceClient
, в противном случае вы сразу же закроете его после вызова службы (пока байты отправляются по проводам). )!
Хорошо, у нас есть способ заставить это работать, но было бы неплохо получить ответ из авторитетного источника, как утверждает Носерацио.