Неправильный поток.CurrentPrincipal в асинхронном конечном методе WCF
-
21-12-2019 - |
Вопрос
У меня есть служба WCF, которая имеет свой Thread.CurrentPrincipal
установленный в ServiceConfiguration.ClaimsAuthorizationManager
.
Когда я реализую службу асинхронно, как это:
public IAsyncResult BeginMethod1(AsyncCallback callback, object state)
{
// Audit log call (uses Thread.CurrentPrincipal)
var task = Task<int>.Factory.StartNew(this.WorkerFunction, state);
return task.ContinueWith(res => callback(task));
}
public string EndMethod1(IAsyncResult ar)
{
// Audit log result (uses Thread.CurrentPrincipal)
return ar.AsyncState as string;
}
private int WorkerFunction(object state)
{
// perform work
}
Я нахожу, что поток.CurrentPrincipal установлен на правильный ClaimsPrincipal в Begin-методе, а также в WorkerFunction, но в End-методе он установлен на GenericPrincipal.
Я знаю, что могу включить ASP.NET совместимость для сервиса и использования HttpContext.Current.User
который имеет правильный принцип действия во всех методах, но я бы предпочел этого не делать.
Есть ли способ принудительно изменить поток.CurrentPrincipal на правильный ClaimsPrincipal без включения ASP.NET совместимости?
Решение
Начиная с краткое описание точек расширения WCF, вы увидите тот, который специально разработан для решения вашей проблемы.Это называется Инициализатор CallContextInitializer.Взгляните на это статья, в которой приводится пример кода CallContextInitializer.
Если вы создадите расширение ICallContextInitializer, вам будет предоставлен контроль над обоими контекстами потока BeginXXX и контекст потока EndXXX.Вы говорите, что ClaimsAuthorizationManager правильно установил пользователя-участника в вашем методе BeginXXX(...).В этом случае вы затем создаете для себя пользовательский ICallContextInitializer, который либо назначает, либо записывает CurrentPrincipal, в зависимости от того, обрабатывает ли он ваш BeginXXX() или ваш EndXXX().Что-то вроде:
public object BeforeInvoke(System.ServiceModel.InstanceContext instanceContext, System.ServiceModel.IClientChannel channel, System.ServiceModel.Channels.Message request){
object principal = null;
if (request.Properties.TryGetValue("userPrincipal", out principal))
{
//If we got here, it means we're about to call the EndXXX(...) method.
Thread.CurrentPrincipal = (IPrincipal)principal;
}
else
{
//If we got here, it means we're about to call the BeginXXX(...) method.
request.Properties["userPrincipal"] = Thread.CurrentPrincipal;
}
...
}
Чтобы внести дальнейшую ясность, рассмотрим два случая.Предположим, вы реализовали как ICallContextInitializer, так и IParameterInspector.Предположим, что ожидается, что эти перехватчики будут выполняться с синхронной службой WCF и с асинхронной службой WCF (что является вашим особым случаем).
Ниже приведена последовательность событий и объяснение того, что происходит:
Синхронный случай
ICallContextInitializer.BeforeInvoke();
IParemeterInspector.BeforeCall();
//...service executes...
IParameterInspector.AfterCall();
ICallContextInitializer.AfterInvoke();
Ничего удивительного в приведенном выше коде нет.Но теперь посмотрите ниже на то, что происходит с асинхронными сервисными операциями...
Асинхронный случай
ICallContextInitializer.BeforeInvoke(); //TryGetValue() fails, so this records the UserPrincipal.
IParameterInspector.BeforeCall();
//...Your BeginXXX() routine now executes...
ICallContextInitializer.AfterInvoke();
//...Now your Task async code executes (or finishes executing)...
ICallContextInitializercut.BeforeInvoke(); //TryGetValue succeeds, so this assigns the UserPrincipal.
//...Your EndXXX() routine now executes...
IParameterInspector.AfterCall();
ICallContextInitializer.AfterInvoke();
Как вы можете видеть, CallContextInitializer гарантирует, что у вас есть возможность инициализировать значения, такие как ваш CurrentPrincipal, непосредственно перед запуском процедуры EndXXX().Поэтому не имеет значения, что процедура EndXXX(), несомненно, выполняется в другом потоке, чем процедура BeginXXX().И да, тот System.ServiceModel.Channels.Message
объект, который хранит ваш пользовательский принципал между методами Begin/End, сохраняется и должным образом передается WCF, даже если поток изменился.
В целом, этот подход позволяет вашему EndXXX(IAsyncResult) выполняться с правильным IPrincipal без необходимости явно переустанавливать CurrentPrincipal в подпрограмме EndXXX().И, как и в случае с любым поведением WCF, вы можете решить, применяется ли это к отдельным операциям, ко всем операциям в контракте или ко всем операциям в конечной точке.
Другие советы
На самом деле это не ответ на мой вопрос, а альтернативный подход к реализации службы WCF (в .NET 4.5), который не проявляет тех же проблем с Thread.CurrentPrincipal
.
public async Task<string> Method1()
{
// Audit log call (uses Thread.CurrentPrincipal)
try
{
return await Task.Factory.StartNew(() => this.WorkerFunction());
}
finally
{
// Audit log result (uses Thread.CurrentPrincipal)
}
}
private string WorkerFunction()
{
// perform work
return string.Empty;
}
Правильным подходом к этому является создание расширения:
public class SLOperationContext : IExtension<OperationContext>
{
private readonly IDictionary<string, object> items;
private static ReaderWriterLockSlim _instanceLock = new ReaderWriterLockSlim();
private SLOperationContext()
{
items = new Dictionary<string, object>();
}
public IDictionary<string, object> Items
{
get { return items; }
}
public static SLOperationContext Current
{
get
{
SLOperationContext context = OperationContext.Current.Extensions.Find<SLOperationContext>();
if (context == null)
{
_instanceLock.EnterWriteLock();
context = new SLOperationContext();
OperationContext.Current.Extensions.Add(context);
_instanceLock.ExitWriteLock();
}
return context;
}
}
public void Attach(OperationContext owner) { }
public void Detach(OperationContext owner) { }
}
Теперь это расширение используется в качестве контейнера для объектов, которые вы хотите сохранить между переключениями потоков, поскольку OperationContext.Current останется прежним.
Теперь вы можете использовать это в BeginMethod1 для сохранения текущего пользователя:
SLOperationContext.Current.Items["Principal"] = OperationContext.Current.ClaimsPrincipal;
И затем в EndMethod1 вы можете получить пользователя, набрав:
ClaimsPrincipal principal = SLOperationContext.Current.Items["Principal"];
РЕДАКТИРОВАТЬ (другой подход):
public IAsyncResult BeginMethod1(AsyncCallback callback, object state)
{
var task = Task.Factory.StartNew(this.WorkerFunction, state);
var ec = ExecutionContext.Capture();
return task.ContinueWith(res =>
ExecutionContext.Run(ec, (_) => callback(task), null));
}