Как настроить Simple Injector для запуска фоновых потоков в ASP.NET MVC

Я использую Simple Injector для управления жизненным циклом моих внедренных зависимостей (в данном случае UnitOfWork), и я очень доволен тем, что у меня есть отдельный декоратор, а не моя служба или обработчик команд, который занимается сохранением и удалением, что значительно упрощает код при написании бизнеса. логические уровни (я следую архитектуре, описанной в эта запись в блоге).

Вышеупомянутое работает отлично (и очень легко) с использованием пакета Simple Injector MVC NuGet и следующего кода во время создания корневого контейнера композиции, если в графе существует более одной зависимости, один и тот же экземпляр вводится во все — идеально подходит для Контекст модели Entity Framework.

private static void InitializeContainer(Container container)
{
    container.RegisterPerWebRequest<IUnitOfWork, UnitOfWork>();
    // register all other interfaces with:
    // container.Register<Interface, Implementation>();
}

Теперь мне нужно запустить несколько фоновых потоков и понять из Simple Injector документация по потокам, в которой команды могут быть проксированы следующим образом:

public sealed class TransactionCommandHandlerDecorator<TCommand>
    : ICommandHandler<TCommand>
{
    private readonly ICommandHandler<TCommand> handlerToCall;
    private readonly IUnitOfWork unitOfWork;

    public TransactionCommandHandlerDecorator(
        IUnitOfWork unitOfWork, 
        ICommandHandler<TCommand> decorated)
    {
        this.handlerToCall = decorated;
        this.unitOfWork = unitOfWork;
    }

    public void Handle(TCommand command)
    {
         this.handlerToCall.Handle(command);
         unitOfWork.Save();
    }
}

ThreadedCommandHandlerProxy:

public class ThreadedCommandHandlerProxy<TCommand>
    : ICommandHandler<TCommand>
{
    Func<ICommandHandler<TCommand>> instanceCreator;

    public ThreadedCommandHandlerProxy(
        Func<ICommandHandler<TCommand>> creator)
    {
        this.instanceCreator = creator;
    }

    public void Handle(TCommand command)
    {
        Task.Factory.StartNew(() =>
        {
            var handler = this.instanceCreator();
            handler.Handle(command);
        });
    }
} 

Однако из этого примера документации по потокам я вижу, что фабрики используются, если я добавлю фабрики к своим командам и сервисному уровню, все станет запутанным и несогласованным, поскольку у меня будут разные методологии сохранения для разных служб (один контейнер обрабатывает сохранение, другие экземпляры фабрик внутри сервисы обрабатывают сохранения и удаление) — вы видите, насколько понятен и прост скелет кода сервиса без каких-либо фабрик:

public class BusinessUnitCommandHandlers :
    ICommandHandler<AddBusinessUnitCommand>,
    ICommandHandler<DeleteBusinessUnitCommand>
{
    private IBusinessUnitService businessUnitService;
    private IInvoiceService invoiceService;

    public BusinessUnitCommandHandlers(
        IBusinessUnitService businessUnitService, 
        IInvoiceService invoiceService)
    {
        this.businessUnitService = businessUnitService;
        this.invoiceService = invoiceService;
    }

    public void Handle(AddBusinessUnitCommand command)
    {
        businessUnitService.AddCompany(command.name);
    }

    public void Handle(DeleteBusinessUnitCommand command)
    {
        invoiceService.DeleteAllInvoicesForCompany(command.ID);
        businessUnitService.DeleteCompany(command.ID);
    }
}

public class BusinessUnitService : IBusinessUnitService
{
    private readonly IUnitOfWork unitOfWork;
    private readonly ILogger logger;

    public BusinessUnitService(IUnitOfWork unitOfWork, 
        ILogger logger)
    {
        this.unitOfWork = unitOfWork;
        this.logger = logger;
    }

    void IBusinessUnitService.AddCompany(string name)
    {
        // snip... let container call IUnitOfWork.Save()
    }

    void IBusinessUnitService.DeleteCompany(int ID)
    {
        // snip... let container call IUnitOfWork.Save()
    }
}

public class InvoiceService : IInvoiceService
{
    private readonly IUnitOfWork unitOfWork;
    private readonly ILogger logger;

    public BusinessUnitService(IUnitOfWork unitOfWork, 
        ILogger logger)
    {
        this.unitOfWork = unitOfWork;
        this.logger = logger;
    }

    void IInvoiceService.DeleteAllInvoicesForCompany(int ID)
    {
        // snip... let container call IUnitOfWork.Save()
    }
}

С вышеизложенным моя проблема начинает формироваться, как я понимаю из документации по времени жизни ASP .NET PerWebRequest используется следующий код:

public T GetInstance()
{
    var context = HttpContext.Current;

    if (context == null)
    {
        // No HttpContext: Let's create a transient object.
        return this.instanceCreator();
    }

    object key = this.GetType();
    T instance = (T)context.Items[key];

    if (instance == null)
    {
        context.Items[key] = instance = this.instanceCreator();
    }
    return instance;
}

Вышеприведенное отлично работает для каждого HTTP-запроса, будет действительный HttpContext.Current, однако, если я запущу новый поток с ThreadedCommandHandlerProxy, он создаст новый поток, а HttpContext больше не будет существовать в этом потоке.

Поскольку HttpContext будет нулевым при каждом последующем вызове, все экземпляры объектов, внедренные в конструкторы служб, будут новыми и уникальными, в отличие от обычного HTTP для каждого веб-запроса, где объекты правильно совместно используются как один и тот же экземпляр во всех службах.

Итак, резюмируя вышесказанное в вопросы:

Как мне получить созданные объекты и внедрить общие элементы независимо от того, созданы ли они из HTTP-запроса или через новый поток?

Существуют ли какие-либо особые соображения для управления UnitOfWork потоком в прокси-сервере обработчика команд? Как можно обеспечить его сохранение и удаление после выполнения обработчика?

Если бы у нас возникла проблема на уровне обработчика команд/уровня службы, и мы не хотели бы сохранять UnitOfWork, мы бы просто выдали исключение? Если да, то можно ли поймать это на глобальном уровне или нам нужно перехватить исключение для каждого запроса из try-catch в декораторе обработчика или прокси?

Спасибо,

Крис


person morleyc    schedule 14.06.2012    source источник
comment
@Steven, спасибо, я рассмотрел этот вопрос, однако я не уверен на 100%, что мне делать с RegisterPerWebRequest‹DbContext, MyDbContext› и RegisterLifetimeScope‹IObjectContextAdapter,MyDbContext› для моего случая IUnitOfWork и UnitOfWork, в моем случае это было бы DbContext => UnitOfWorkBase, MyDbContext => UnitOfWork и IObjectContextAdapter => IUnitOfWork? Если бы вы могли уточнить, был бы признателен. Также упоминалось, что есть несколько способов сделать это, является ли один из способов сделать для этого явное продление срока службы? Было бы неплохо, если бы не нужно было наследовать от базовых классов.   -  person morleyc    schedule 15.06.2012
comment
С новым async ActionResults для MVC5 эта проблема станет распространенной.   -  person Mrchief    schedule 14.04.2014
comment
Если единицей работы является длительная и важная задача, подумайте об очереди работы шины сообщений, которую вы можете забрать, когда ваше приложение вернется из рециркуляции. К вашему сведению, читая о QueueBackgroundWorkItem, я увидел, что задача будет уничтожена, если она займет больше 90 секунд, поэтому вы подвергаетесь риску, если вам требуется больше минуты.   -  person KCD    schedule 21.04.2016


Ответы (2)


Позвольте мне начать с предупреждения: если вы хотите выполнять команды асинхронно в веб-приложении, вы можете сделать шаг назад и посмотреть, чего вы пытаетесь достичь. Всегда существует риск перезапуска веб-приложения сразу после запуска обработчика в фоновом потоке. Когда приложение ASP.NET перезапускается, все фоновые потоки прерываются. Возможно, было бы лучше публиковать команды в (транзакционной) очереди и позволять фоновой службе забирать их. Это гарантирует, что команды не могут «потеряться». А также позволяет повторно выполнять команды, когда обработчик не удается успешно. Это также может избавить вас от необходимости делать некоторые неприятные регистрации (которые у вас, вероятно, будут, независимо от того, какую среду DI вы выберете), но это, вероятно, просто побочная проблема. И если вам нужно запускать обработчики асинхронно, по крайней мере постарайтесь свести к минимуму количество обработчиков, которые вы запускаете асинхронно.

С учетом этого вам нужно следующее.

Как вы заметили, поскольку вы запускаете (некоторые) обработчики команд асинхронно, вы не можете использовать для них образ жизни для каждого веб-запроса. Вам понадобится гибридное решение, которое сочетает в себе веб-запрос и «что-то еще». Это что-то еще, скорее всего, будет для каждой области действия. Для этих гибридных решений нет встроенных расширений по нескольким причинам. Во-первых, это достаточно экзотическая функция, которая не многим нужна. Во-вторых, вы можете смешивать любые два или три образа жизни вместе, так что это будет почти бесконечная комбинация гибридов. И, наконец, (довольно) легко зарегистрировать это самостоятельно.

В Simple Injector 2 был добавлен класс Lifestyle, который содержит метод CreateHybrid, позволяющий комбинировать любые два образа жизни для создания нового образа жизни. Вот пример:

var hybridLifestyle = Lifestyle.CreateHybrid(
    () => HttpContext.Current != null,
    new WebRequestLifestyle(),
    new LifetimeScopeLifestyle());

Вы можете использовать этот гибридный образ жизни для регистрации единицы работы:

container.Register<IUnitOfWork, DiUnitOfWork>(hybridLifestyle);

Поскольку вы регистрируете единицу работы как «Пожизненная область», вы должны явно создать и удалить «Пожизненную область» для определенного потока. Самое простое, что можно сделать, это добавить это в свой ThreadedCommandHandlerProxy. Это не самый НАДЕЖНЫЙ способ что-то делать, но для меня это самый простой способ показать вам, как это сделать.

Если бы у нас возникла проблема на уровне обработчика команд/уровня обслуживания и мы не хотели бы сохранять UnitOfWork, мы бы просто выдали исключение?

Типичная вещь, которую нужно сделать, это создать исключение. Это на самом деле общее правило исключений:

Если ваш метод не может делать то, что обещает его имя, выбросьте его. ->

Обработчик команды не должен знать, в каком контексте он выполняется, и последнее, что вам нужно, — это различать, должен ли он генерировать исключение. Так что метание - лучший вариант. Однако при работе в фоновом потоке вам лучше поймать это исключение, поскольку .NET убьет весь AppDomain, если вы его не поймаете. В веб-приложении это означает перезапуск AppDomain, что означает, что ваше веб-приложение (или, по крайней мере, этот сервер) будет отключено в течение короткого периода времени.

С другой стороны, вы также не хотите терять какую-либо информацию об исключении, поэтому вам следует зарегистрировать это исключение и, вероятно, захотеть зарегистрировать сериализованное представление этой команды с этим исключением, чтобы вы могли видеть, какие данные были переданы. добавленный в метод ThreadedCommandHandlerProxy.Handle, он будет выглядеть так:

public void Handle(TCommand command)
{
    string xml = this.commandSerializer.ToXml(command);    

    Task.Factory.StartNew(() =>
    {
        var logger = 
            this.container.GetInstance<ILogger>();

        try
        {
            using (container.BeginTransactionScope())
            {
                // must be created INSIDE the scope.
                var handler = this.instanceCreator();
                handler.Handle(command);
            }
        }
        catch (Exception ex)
        {
            // Don't let the exception bubble up, 
            // because we run in a background thread.
            this.logger.Log(ex, xml);
        }
    });
}

Я предупредил, что асинхронный запуск обработчиков может быть не лучшей идеей. Однако, поскольку вы применяете этот шаблон команды/обработчика, вы сможете позже переключиться на использование очередей, не изменяя ни одной строки кода в своем приложении. Это просто вопрос написания какого-то QueueCommandHandlerDecorator<T> (который сериализует команду и отправляет ее в очередь) и изменить способ подключения в корне вашей композиции, и все готово (и, конечно, не забудьте реализовать сервис, выполняющий команды из очереди). Другими словами, что хорошо в этом дизайне SOLID, так это то, что реализация этих функций не зависит от размера приложения.

person Steven    schedule 15.06.2012
comment
Спасибо, Стивен, ваш комментарий о том, что фоновая служба запускает все, может быть именно тем, что я ищу, без каких-либо других проблем с потоками или регистрацией - с помощью «службы» просто для уточнения, можем ли мы заставить QueueCommandHandlerDecorator записать задание в базу данных ( или веб-служба WCF, или какой-либо другой метод межпроцессного взаимодействия) и иметь полностью отдельный процесс (написать вторую программу в качестве службы Windows или службы WCF), который собирает эти запросы на работу и обрабатывает их? Спасибо за отличное понимание дизайна и избегание подводных камней. - person morleyc; 16.06.2012
comment
@ g18c: вы можете использовать любую очередь, которая вам больше нравится (база данных, MSMQ), и вы можете записывать в свою очередь любым способом (непосредственно из MVC или позволить службе WCF записывать в очередь), но для выбора команды из очереди и их выполнение, лучше всего использовать службу Windows. Или, по крайней мере, не используйте веб-сервис, так как в этом случае вы столкнетесь с теми же проблемами с перезапуском домена приложения, что и с вашим веб-приложением. Веб-службы не предназначены для фоновой обработки. Они предназначены для обработки запросов. Удачи. - person Steven; 16.06.2012
comment
Отличные вещи, в дополнение к моему существующему приложению MVC я буду использовать свою общую библиотеку классов службы/контрактов с новой службой Windows, которая регистрирует необходимые команды в контейнере Simple Injector (с областью действия для каждого потока или времени жизни) - поскольку доступ к базе данных является транзакционным и обрабатывается с помощью контейнера я могу легко ставить задачи в таблицу заданий для запуска, не беспокоясь о состоянии гонки, поскольку служба может опрашивать эту таблицу на наличие новых записей. Эта методология проектирования очень эффективна, поскольку сервисы можно использовать мгновенно, независимо от приложения MVC или службы Windows — большое спасибо за это. - person morleyc; 16.06.2012
comment
Одно предупреждение. Когда вы используете таблицу базы данных в качестве очереди команд, очень сложно реализовать это корректно с точки зрения транзакций при наличии нескольких потоков или нескольких служб Windows, обрабатывающих одну очередь. Самый простой (самый безопасный) способ — иметь одну многопоточную службу Windows, обрабатывающую эту очередь. Или иметь один поток, читающий/записывающий в очередь и распределяющий команды другим потокам, которые выполняют команды. Или пусть каждый поток/служба использует свой собственный набор команд. Это предотвращает получение одной и той же команды несколькими потоками/службами. - person Steven; 16.06.2012
comment
Если вы не выберете эту стратегию, вам придется использовать UPDLOCK, READPAST (что мы и сделали), но я обнаружил, что эта модель легко ломается (молча) при добавлении индексов в таблицу, что дает вам ложное чувство безопасности. - person Steven; 16.06.2012

Часть проблемы с запуском фоновых потоков в ASP.NET можно решить с помощью удобного декоратора обработчика команд:

public class AspNetSafeBackgroundCommandHandlerDecorator<T>
    : ICommandHandler<T>, IRegisteredObject
{
    private readonly ICommandHandler<T> decorated;

    private readonly object locker = new object();

    public AspNetSafeBackgroundCommandHandlerDecorator(
        ICommandHandler<T> decorated)
    {
        this.decorated = decorated;
    }

    public void Handle(T command)
    {
        HostingEnvironment.RegisterObject(this);

        try
        {
            lock (this.locker)
            {
                this.decorated.Handle(command);
            }
        }
        finally
        {
            HostingEnvironment.UnregisterObject(this);
        }            
    }

    void IRegisteredObject.Stop(bool immediate)
    {
        // Ensure waiting till Handler finished.
        lock (this.locker) { }
    }
}

Когда вы помещаете этот декоратор между обработчиком команды и ThreadedCommandHandlerProxy, вы гарантируете, что (при нормальных условиях) AppDomain не будет выгружен во время выполнения такой команды.

person Steven    schedule 10.08.2012