Gdzie zgłaszać zdarzenia domeny zależne od trwałości-usługi, repozytorium lub interfejsu użytkownika?

Mój ASP.NET aplikacja MVC3 / NHibernate ma wymóg odpalania i obsługi różnych zdarzeń związanych z obiektami mojej domeny. Na przykład obiekt Order może mieć zdarzenia takie jak OrderStatusChanged lub NoteCreatedForOrder. W większości przypadków zdarzenia te powodują wysłanie wiadomości e-mail, więc nie mogę po prostu zostawić ich w aplikacji MVC.

[[10]} przeczytałem wydarzenia domeny Udi Dahana i dziesiątki innych myśli na temat tego, jak zrobić tego typu rzeczy, i zdecydowałem się użyć Host oparty na NServiceBus, który obsługuje komunikaty o zdarzeniach. Zrobiłem kilka testów proof-of-concept I to wydaje się działać dobrze.

Moje pytanie brzmi jaka warstwa aplikacji powinna rzeczywiście podnosić zdarzenia. Nie chcę uruchamiać zdarzeń, dopóki dany obiekt nie zostanie pomyślnie utrzymany (nie mogę wysłać e-maila, że notatka została utworzona, jeśli trwałość nie powiodła się).

Innym problemem jest to, że w niektórych przypadkach zdarzenie jest powiązane z obiektem, który znajduje się pod zbiorczym korzeniem. W powyższym przykładzie, Note jest zapisywany przez dodanie go do kolekcji Order.Notes i zapisanie kolejności. Problem polega na tym, że trudno jest ocenić, jakie zdarzenia powinny zostać wywołane Po zapisaniu Order. Chciałbym uniknąć konieczności wyciągania bieżącej kopii obiektu i szukania różnic przed zapisaniem zaktualizowanej kopii.

  • Czy odpowiednie jest, aby UI informowaÅ‚o o tych wydarzeniach? Wie, jakie zdarzenia miaÅ‚y miejsce i może je odpalić dopiero po pomyÅ›lnym warstwa usÅ‚ug zapisuje obiekt. CoÅ› po prostu wydaje siÄ™ nie tak z kontrolerem odpalajÄ…cym zdarzenia domeny.

  • Czy repozytorium powinno odpalić zdarzenia po pomyÅ›lnym utrzymywaniu siÄ™?

  • Czy powinienem oddzielić wszystkie zdarzenia, aby repozytorium przechowywaÅ‚o obiekt Event, który jest nastÄ™pnie przechwytywany przez usÅ‚ugÄ™ Pollera i , a nastÄ™pnie zamienione w Zdarzenie dla NServiceBus (lub obsÅ‚ugiwane bezpoÅ›rednio z usÅ‚ugi Pollera)?

  • Jest lepszy sposób na to? Może aby obiekty mojej domeny ustawiaÅ‚y w kolejce zdarzenia, które sÄ… wywoÅ‚ywane przez warstwÄ™ usÅ‚ug dopiero po utrzymaniu obiektu?

  • Update: mam warstwÄ™ usÅ‚ug, ale wydaje siÄ™ kÅ‚opotliwe i nadmierne, aby przejść przez proces porównania, aby okreÅ›lić, jakie zdarzenia powinny być wywoÅ‚ane, gdy dany zagregowany root jest zapisywany. Ponieważ niektóre z tych zdarzeÅ„ sÄ… ziarniste( np. "status zamówienia zmieniony"), myÅ›lÄ™, że bÄ™dÄ™ musiaÅ‚ odzyskać kopiÄ™ DB obiekt, porównaj wÅ‚aÅ›ciwoÅ›ci, aby utworzyć zdarzenia, zapisz nowy obiekt, a nastÄ™pnie wyÅ›lij zdarzenia do NServiceBus, gdy operacja zapisania zakoÅ„czyÅ‚a siÄ™ pomyÅ›lnie.

Update

To, co zrobiłem, po odpowiedzi, którą opublikowałem poniżej (znacznie poniżej ), to wbudowanie w moje podmioty domenowe właściwości EventQueue, która była List<IDomainEvent>. Następnie dodałem zdarzenia jako zmiany w domenie, które pozwoliły mi zachować logikę w domenie, co moim zdaniem jest odpowiednie, ponieważ odpalam zdarzenia w oparciu o to, co dzieje się wewnątrz jednostki.

Następnie, gdy utrzymuję obiekt w warstwie usług, przetwarzam tę kolejkę i faktycznie wysyłam zdarzenia do magistrali usług. Początkowo planowałem użyć starszej bazy danych, która używała identyfikatora PKs, więc musiałem przetworzyć te zdarzenia, aby wypełnić identyfikator podmiotu, ale ostatecznie zdecydowałem się przełączyć na Guid.Comb PK, który pozwala mi pominąć ten krok.

Author: Community, 2011-05-04

6 answers

Moim rozwiązaniem jest to, że podnosisz zdarzenia zarówno w warstwie domeny, jak i w warstwie usług.

Twoja domena:

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 });
    }
}

Twój serwis:

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);
        }
    }
}
 10
Author: Yanfeng Tian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-12-12 04:50:53

Zdarzenia domeny powinny być podnoszone w... domena. Dlatego są to zdarzenia domenowe.

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

(a następnie w klasie Klienta, następujące):

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

Metoda Raise zapisze Zdarzenie w magazynie zdarzeń. Może również wykonywać lokalnie zarejestrowane procedury obsługi zdarzeń.

 7
Author: Roy Dictus,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-05-05 12:44:37

Wygląda na to, że potrzebujesz warstwy serwisowej . Warstwa usług to kolejna abstrakcja, która znajduje się między interfejsem lub kontrolerami a warstwą biznesową lub modelem domeny. To coś w rodzaju API do Twojej aplikacji. Kontrolery będą wtedy miały dostęp tylko do warstwy usług.

Następnie warstwa usług staje się odpowiedzialna za interakcję z modelem domeny.]}
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();
  }
  //...
}

Często kończy się na obiektach 'manager', takich jak OrderManager, aby współdziałać z zamówienia i warstwy usługowej do obsługi POCOs.

Czy odpowiednie jest, aby UI informowało o tych wydarzeniach? Wie, jakie zdarzenia miały miejsce i może je uruchomić dopiero po pomyślnym zapisaniu obiektu przez warstwę usług. Coś po prostu wydaje się nie tak z kontrolerem odpalającym zdarzenia domeny.

Nie. Pojawią się problemy, jeśli zostaną dodane nowe akcje, a deweloper nie będzie świadomy lub zapomni, że wiadomość e-mail powinna zostać wyłączona.

Powinien repozytorium odpala zdarzenia po pomyślnym utrzymaniu?

Nie. repozytorium jest odpowiedzialne za zapewnienie abstrakcji nad dostępem do danych i nic więcej
Czy jest na to lepszy sposób? Może aby obiekty mojej domeny ustawiały w kolejce zdarzenia, które są wywoływane przez warstwę usług dopiero po utrzymaniu obiektu?

Tak, wygląda na to, że powinno to być obsługiwane przez warstwę usług.

 3
Author: David Glenn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-05-04 16:35:54

Rozwiązanie okazało się oparte na implementacji tych metod rozszerzenia na obiekcie sesji NHibernate.

Byłem prawdopodobnie trochę niejasny z brzmieniem pytania. Powodem problemu architektonicznego był fakt, że obiekty NHibernate są zawsze w tym samym stanie, chyba że ręcznie je odłączysz i przejdziesz przez różnego rodzaju machinacje. To było coś, czego nie chciałem robić, aby określić, jakie właściwości zostały zmienione, a zatem jakie wydarzenia odpalić.

Odpalenie tych zdarzeń w Ustawieniach właściwości nie zadziała, ponieważ zdarzenia powinny wystrzelić tylko po utrzymaniu zmian, aby uniknąć odpalenia zdarzeń w operacji, która może ostatecznie zawieść.

Więc dodałem kilka metod do bazy repozytorium:

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);
}

Następnie, w metodzie mojego serwisu Save, mogę zrobić coś takiego (pamiętaj, że używam tutaj NServiceBus, ale jeśli używasz statycznej klasy Udi Dahan ' s domain events to zadziała podobnie):

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);

Ponieważ w niektórych przypadkach Id encji może nie być ustawiona, dopóki nie zostanie zapisana (używam kolumn całkowitych Identity), może być konieczne uruchomienie po zatwierdzeniu transakcji, aby pobrać identyfikator, aby wypełnić właściwości w moich obiektach event. Ponieważ są to istniejące dane, nie mogę łatwo przełączyć się na identyfikator przypisany klientowi typu hilo.

 1
Author: Josh Anderson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-05-06 13:31:49

Posiadanie zdarzeń domeny działa dobrze, jeśli masz polecenia , wysłane do twojej warstwy usług . Następnie może aktualizować encje zgodnie z poleceniem i wywoływać odpowiednie zdarzenia domeny w pojedynczej transakcji.

Jeśli przenosisz same byty pomiędzy UI i service layer , bardzo trudno (a czasami nawet niemożliwe) jest określić, jakie zdarzenia w domenie miały miejsce, ponieważ nie są one jawne, ale ukryte pod state of the domain. / align = "left" /

 0
Author: driushkin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-05-04 17:09:05

Myślę, że powinieneś mieć usługę domenową, jak powiedział David Glenn.

Problem, na który wpadłem, polegał na tym, że warstwa usług skończy się na aby pobrać kopię niektórych obiektów przed zapisaniem zmodyfikowanej wersji do porównaj nowy z starym, a następnie zdecyduj, jakie wydarzenia powinien zostać zwolniony.

Usługa domeny powinna zawierać metody, które jasno określają, co chcesz zrobić z podmiotem domeny, takie jak: RegisterNewOrder, Createnoteforoorder, ChangeOrderStatus itp.

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));
        }

    }
}
 0
Author: Alex Burtsev,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-12-11 10:18:49