Подтвердить что ты не робот

Где поднимать события, зависящие от состояния, зависящие от состояния - сервис, репозиторий или пользовательский интерфейс?

В моем приложении ASP.NET MVC3/NHibernate есть требование отключить и обработать различные события, связанные с объектами моего домена. Например, объект Order может иметь такие события, как OrderStatusChanged или NoteCreatedForOrder. В большинстве случаев эти события приводят к отправке электронного письма, поэтому я не могу просто оставить их в приложении MVC.

Я прочитал Udi Dahan События домена и десятки других мыслей о том, как это сделать, и я решил используя узел NServiceBus, который обрабатывает сообщения о событиях. Я сделал несколько пробных доказательств, и это, похоже, хорошо работает.

Мой вопрос , какой уровень приложения должен действительно поднимать события. Я не хочу запускать события до тех пор, пока объект, о котором идет речь, не будет успешно сохранен (не может отправить электронное письмо, которое было создано записью, если сбой сохранения не удалось).

Еще одна проблема заключается в том, что в некоторых случаях событие привязывается к объекту, который находится под совокупным корнем. В приведенном выше примере a Note сохраняется путем добавления его в коллекцию Order.Notes и сохранения порядка. Это создает проблему в том, что сложно оценить, какие события следует запускать при сохранении Order. Я хотел бы избежать необходимости вытаскивать текущую копию объекта и искать различия до сохранения обновленной копии.

  • Соответствует ли пользовательскому интерфейсу повышение этих событий? Он знает, какие события произошли, и может их запустить только после успешного сохранения уровня обслуживания. Что-то просто кажется неправильным в том, что контроллер отключает события домена.

  • Должен ли репозиторий отключать события после успешного сохранения?

  • Следует ли вообще отделять события, хранить хранилище объекта Event, который затем подбирается службой poller, а затем превращается в событие для NServiceBus (или обрабатывается непосредственно из службы poller)?

  • Есть ли лучший способ сделать это? Может быть, мои объекты домена останавливают события, которые запускаются на уровне сервиса только после того, как объект сохраняется?

  • Обновление: У меня есть сервисный уровень, но для него сложнее и сложнее проследить процесс сопоставления, чтобы определить, какие события должны быть запущены при сохранении заданного корня. Поскольку некоторые из этих событий являются деталями (например, "статус заказа изменен" ), я думаю, что мне нужно будет восстановить копию объекта DB, сравнить свойства для создания событий, сохранить новый объект, а затем отправить события в NServiceBus, когда операция сохранения успешно завершена.

Update

То, что я закончил, после ответа, который я опубликовал ниже (ниже), должен был встроить в мои объекты домена свойство EventQueue, которое было List<IDomainEvent>. Затем я добавил события, поскольку изменения в домене заслужили его, что позволило мне сохранить логику внутри домена, что, по моему мнению, уместно, поскольку я запускаю события на основе того, что происходит внутри объекта.

Затем, когда я сохраняю объект на своем уровне обслуживания, я обрабатываю эту очередь и фактически отправляю события на служебную шину. Вначале я планировал использовать устаревшую базу данных, в которой использовались идентификационные ПК, поэтому мне пришлось после этого обработать эти события, чтобы заполнить идентификатор объекта, но в конечном итоге я решил переключиться на Guid.Comb PK, что позволяет мне пропустить этот шаг.

4b9b3361

Ответ 1

Решение оказалось основано на реализации этих методов расширения на объекте сеанса NHibernate.

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

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

Итак, что я сделал, добавили несколько методов в мою базу репозитория:

public bool IsDirtyEntity(T entity)
{
    // Use the extension method...
    return SessionFactory.GetCurrentSession().IsDirtyEntity(entity);
}

public bool IsDirtyEntityProperty(T entity, string propertyName)
{
    // Use the extension method...
    return SessionFactory.GetCurrentSession().IsDirtyProperty(entity, propertyName);
}

Затем в моем сервисе Save я могу сделать что-то вроде этого (помните, что я использую NServiceBus здесь, но если вы использовали статический класс домена домена Udi Dahan, он работал бы аналогичным образом):

var pendingEvents = new List<IMessage>();
if (_repository.IsDirtyEntityProperty(order, "Status"))
    pendingEvents.Add(new OrderStatusChanged()); // In reality I'd set the properties of this event message object

_repository.Save(order);
_unitOfWork.Commit();

// If we get here then the save operation succeeded
foreach (var message in pendingEvents)
    Bus.Send(message);

Поскольку в некоторых случаях объект Id объекта может не быть установлен до тех пор, пока он не будет сохранен (я использую столбцы с целым числом Identity), мне, возможно, придется пройти через транзакцию, чтобы получить идентификатор для заполнения свойств в объектах моего события. Поскольку это существующие данные, я не могу легко переключиться на идентификатор типа hilo, назначенный клиентом.

Ответ 2

Мое решение состоит в том, что вы поднимаете события как на уровне домена, так и на уровне обслуживания.

Ваш домен:

public class Order
{
    public void ChangeStatus(OrderStatus status)
    {
        // change status
        this.Status = status;
        DomainEvent.Raise(new OrderStatusChanged { OrderId = Id, Status = status });
    }

    public void AddNote(string note)
    {
        // add note
        this.Notes.Add(note)
        DomainEvent.Raise(new NoteCreatedForOrder { OrderId = Id, Note = note });
    }
}

Ваш сервис:

public class OrderService
{
    public void SubmitOrder(int orderId, OrderStatus status, string note)
    {
        OrderStatusChanged orderStatusChanged = null;
        NoteCreatedForOrder noteCreatedForOrder = null;

        DomainEvent.Register<OrderStatusChanged>(x => orderStatusChanged = x);
        DomainEvent.Register<NoteCreatedForOrder>(x => noteCreatedForOrder = x);

        using (var uow = UnitOfWork.Start())
        {
            var order = orderRepository.Load(orderId);
            order.ChangeStatus(status);
            order.AddNote(note);
            uow.Commit(); // commit to persist order
        }

        if (orderStatusChanged != null)
        {
            // something like this
            serviceBus.Publish(orderStatusChanged);
        }

        if (noteCreatedForOrder!= null)
        {
            // something like this
            serviceBus.Publish(noteCreatedForOrder);
        }
    }
}

Ответ 3

События домена должны быть подняты в... Домене. Вот почему они являются событиями домена.

public void ExecuteCommand(MakeCustomerGoldCommand command)
{
    if (ValidateCommand(command) == ValidationResult.OK)
    {
        Customer item = CustomerRepository.GetById(command.CustomerId);
        item.Status = CustomerStatus.Gold;
        CustomerRepository.Update(item);
    }
}

(а затем в классе Customer, следующее):

public CustomerStatus Status
{
    ....
    set
    {
        if (_status != value)
        {
            _status = value;
            switch(_status)
            {
                case CustomerStatus.Gold:
                    DomainEvents.Raise(CustomerIsMadeGold(this));
                    break;
                ...
            }
        }
    }

Метод Raise сохранит событие в Хранилище событий. Он также может выполнять локально зарегистрированные обработчики событий.

Ответ 4

Похоже, вам нужен уровень сервиса. Уровень сервиса - это еще одна абстракция, которая находится между вашим интерфейсом или контроллерами, а также с бизнес-уровнем или моделью домена. Это похоже на API в ваше приложение. Тогда ваши контроллеры будут иметь доступ только к вашему уровню обслуживания.

Затем он становится вашей ответственностью уровня обслуживания, чтобы взаимодействовать с вашей моделью домена

public Order GetOrderById(int id) {
  //...
  var order = orderRepository.get(id);
  //...
  return order;
}

public CreateOrder(Order order) {
  //...
  orderRepositroy.Add(order);
  if (orderRepository.Submitchanges()) {
    var email = emailManager.CreateNewOrderEmail(order);
    email.Send();
  }
  //...
}

Как правило, в конечном итоге с объектами "manager", такими как OrderManager, взаимодействуют с заказами и уровнем обслуживания для работы с POCOs.

Уместно ли для пользовательского интерфейса поднять эти события? Он знает, какие события произошли, и может их запустить только после успешного сохранения уровня обслуживания. Что-то просто кажется неправильным в том, что контроллер отключает события домена.

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

Должен ли репозиторий отключать события после успешного сохранения?

Нет. Ответственность репозитория заключается в предоставлении абстракции доступа к данным и ничего другого

Есть ли лучший способ сделать это? Может быть, мои объекты домена останавливают события, которые запускаются на уровне сервиса только после того, как объект сохраняется?

Да, похоже, что это должно обрабатываться вашим уровнем обслуживания.

Ответ 5

Наличие событий домена работает хорошо, если у вас есть Команды, отправленные на ваш уровень обслуживания. Затем он может обновлять объекты в соответствии с командой и поднять соответствующие события домена в одной транзакции.

Если вы сами перемещаете объекты между пользовательским интерфейсом и уровнем обслуживания, становится очень сложно (а иногда и невозможно) определить, какие события домена произошли, поскольку они не явны, а скрыты под состоянием сущностей.

Ответ 6

Думаю, у вас есть доменная служба, как сказал Дэвид Гленн.

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

Ваша доменная служба должна содержать методы, в которых четко указывается, что вы хотите делать с доменом Entity, например: RegisterNewOrder, CreateNoteForOrder, ChangeOrderStatus и т.д.

public class OrderDomainService()
{
    public void ChangeOrderStatus(Order order, OrderStatus status)
    {
        try
        {
            order.ChangeStatus(status);
            using(IUnitOfWork unitOfWork = unitOfWorkFactory.Get())
            {
                IOrderRepository repository = unitOfWork.GetRepository<IOrderRepository>();
                repository.Save(order);
                unitOfWork.Commit();
            }
            DomainEvents.Publish<OrderStatusChnaged>(new OrderStatusChangedEvent(order, status));
        }

    }
}