Внедрение зависимостей на основе контекста в многопоточное приложение

У меня есть служба, работающая на сервере, который прослушивает очередь сообщений. Когда сообщение получено, запускается новый поток, и сообщение передается этому потоку для обработки.

Я определил интерфейс, который предоставляет доступ текущему пользователю для использования в различных классах, используемых для обработки сообщений:

public interface IUserContext {
    User CurrentUser { get; }
}

Скорее всего, этот пользователь будет меняться от сообщения к сообщению.

Мой вопрос заключается в том, как зарегистрировать реализацию IUserContext в SimpleInjector, чтобы правильный пользователь, содержащийся во входящем сообщении, правильно возвращался свойством CurrentUser?

В моем приложении Asp.Net это было выполнено следующим образом:

container.Register<IUserContext>(() => {
    User user = null;
    try {
         user = HttpContext.Current?.Session[USER_CONTEXT] as IUser;
    }
    catch { }

    return new UserContext(user);
});

Я бы предположил, что это будет достигнуто с помощью Lifetime Scoping, но я не могу определить это в статическом классе и установить пользователя в каждом потоке, потому что это может повредить другой процесс. Это мое лучшее предположение о реализации?

 public static Func<User> UserContext { get; set; }

Затем в моем коде в новом потоке:

using (container.BeginLifetimeScope()) {
    .....
    var user = GetUserContext(message);
    UserContextInitializer.UserContext = () => new UserContext(user);
    .....
}

Тогда регистрация будет выглядеть примерно так:

container.Register<IUserContext>(() => UserContextInitializer.UserContext);

Помимо безопасности потоков, это правильный подход к реализации этого в SimpleInjector? Есть ли другой шаблон, который был бы более правильным?


person Stuart    schedule 22.04.2019    source источник
comment
Я предполагаю, что под правильным вы подразумеваете потокобезопасный. Можно ли не писать UserContext?   -  person Robert Harvey    schedule 22.04.2019
comment
@Renat: Хорошая идея, но предположительно UserContext содержит полезное состояние. Возможно, неизменяемая реализация?   -  person Robert Harvey    schedule 22.04.2019
comment
@RobertHarvey Спасибо, что нашли время прочитать мой вопрос. На самом деле я ищу правильный способ внедрить состояние пользователя, которое может меняться от сообщения к сообщению, используя создание экземпляра объекта через SimpleInjector. Я отредактировал свой вопрос, чтобы быть более ясным.   -  person Stuart    schedule 22.04.2019
comment
@Renat: UserContext будет меняться от сообщения к сообщению.   -  person Stuart    schedule 22.04.2019
comment
Что ж, если вы имеете в виду потокобезопасность, я считаю, что у @Renat есть правильная идея. Настройте его так, чтобы SimpleInjector предоставил вам неизменяемый экземпляр.   -  person Robert Harvey    schedule 22.04.2019
comment
@Renat Да, прослушиватель сообщений передает сообщение процессору, работающему в новом потоке. Процессор извлечет пользователя из сериализованного сообщения. Реализация IUserContext должна будет возвращать этого пользователя на время выполнения этого потока, но не для других запущенных потоков, которые могут иметь разных пользователей.   -  person Stuart    schedule 22.04.2019
comment
Вы правы - статический класс, используемый таким образом, не будет работать, если у него есть какое-то свойство, которое устанавливается каждым запросом. Они бы переписали друг друга.   -  person Scott Hannen    schedule 22.04.2019


Ответы (1)


Давайте начнем с вашей регистрации IUserContext для ASP.NET:

container.Register<IUserContext>(() => {
    User user = null;
    try {
         user = HttpContext.Current?.Session[USER_CONTEXT] as IUser;
    }
    catch { }

    return new UserContext(user);
});

Эта регистрация проблематична, поскольку компонент UserContext зависит от доступности данных времени выполнения, в то время как, как описано здесь создание графов объектов должно быть отделено от данных среды выполнения, а данные среды выполнения должны проходить через систему.

Другими словами, вы должны переписать свой класс UserContext следующим образом:

public class AspNetUserContext : IUserContext
{
    User CurrentUser => (User)HttpContext.Current.Session[USER_CONTEXT];
}

Это позволяет зарегистрировать эту специфичную для ASP.NET реализацию IUserContext следующим образом:

container.RegisterInstance<IUserContext>(new AspNetUserContext());

Конечно, предыдущее не решает проблему в вашей службе Windows, но предыдущее закладывает основу для решения.

Для службы Windows вам также нужна пользовательская реализация (адаптер):

public class ServiceUserContext : IUserContext
{
    User CurrentUser { get; set; }
}

Эта реализация намного проще, и здесь свойство CurrentUser ServiceUserContext является доступным для записи свойством. Это элегантно решает вашу проблему, потому что теперь вы можете сделать следующее:

// Windows Service Registration:
container.Register<IUserContext, ServiceUserContext>(Lifestyle.Scoped);
container.Register<ServiceUserContext>(Lifestyle.Scoped);

// Code in the new Thread:
using (container.BeginLifetimeScope())
{
    .....
    var userContext = container.GetInstance<ServiceUserContext>();

    // Set the user of the scoped ServiceUserContext
    userContext.CurrentUser = GetUserContext(message);

    var handler = container.GetInstance<IHandleMessages<SomeMessage>>();

    handler.Handle(message);

    .....
}

Здесь решение также заключается в разделении создания графов объектов и использования данных времени выполнения. В этом случае данные времени выполнения предоставляются графу объектов после построения (т. е. с использованием userContext.CurrentUser = GetUserContext(message)).

person Steven    schedule 22.04.2019
comment
Спасибо, что нашли время ответить на мой вопрос. Таким образом, ServiceUserContext предоставляет установщик, и правильный UserContext может быть установлен там и в новом потоке с помощью BeginLifetimeScope, который ограничит область действия IUserContext этим потоком. Гениально, я должен был подумать об этом. - person Stuart; 22.04.2019