Sourcing di eventi: ricostituzione dell'aggregato dal flusso di eventi errati

1

Sto lavorando a un progetto per aiutare a imparare DDD e sto cercando di fare CQRS e Event Sourcing. Il codice è in C #.

Per questo esempio, diciamo che ho 2 aggregati, Customer e Product . Il mio repository aggregato ha un metodo get Get<TAggregate>(Guid id) che carica tutti gli eventi per quell'ID, crea un'istanza TAggregate vuota, quindi riproduce tutti gli eventi su quell'istanza. L'aggregato ignora gli eventi che non sa cosa fare con.

Il sotto funzionerà come previsto, ogni aggregato è ricostituito dai suoi eventi nell'archivio degli eventi

Customer customer = repo.Get<Customer>(customerId);
Product product = repo.Get<Product>(productId);

Tuttavia, se provo a ricostituire un aggregato da una raccolta di eventi da un aggregato diverso, al momento non viene generato alcun errore, ma l'istanza aggregata ignora qualsiasi evento che non sa cosa fare in modo da è lasciato in questo stato "pulito" come se gli fossero passati zero eventi.

Customer customer = repo.Get<Customer>(productId);
Product product = repo.Get<Product>(customerId);

Vedo due modi per risolvere questo problema: - L'aggregato stesso controlla per assicurarsi che sia in uno stato valido prima di consentire qualsiasi opperation del dominio. - I tipi di eventi sono esplicitamente associati a tipi di aggregati specifici, gli eventi passati al risultato di tipo aggregato sbagliato in un'eccezione.

L'aggregato garantisce che sia in uno stato valido

Esempio:

public class Product : AggregateRoot
{
    private Guid _id;
    private bool _isConstructed;

    public Product(Guid id, ...)
    {
        // enforce domain rules here

        ApplyChange(new ProductAddedEvent(id, ...));
    }

    public void UpdatePrice(decimal newPrice)
    {
        if(!_isConstructed)
            throw new Exception(...);

        // enforce domain rules here

        ApplyChange(new ProductPriceUpdatedEvent(_id, newPrice));
    }

    private void Apply(ProductAddedEvent e)
    {
        _id = e.Id;
        _isConstructed = true;
    }

    private void Apply(ProductPriceUpdatedEvent e)
    {
        ...
    }
}

Potrebbe funzionare, ma mi sembra che possa sfuggire di mano molto rapidamente, con conseguente ingombrante e scomodo codice.

I tipi di eventi sono esplicitamente associati a tipi di aggregati specifici

Impostazione simile all'esempio precedente.

public abstract class Event<TAggregate> where TAggregate : AggregateRoot
{
    public bool IsValidFor(AggregateRoot aggregate)
    {
        return aggregate is TAggregate;
    }
}

public class ProductAddedEvent : Event<Product>
{
    ...
}

public abstract class AggregateRoot
{
    public void Reconstitute(Event[] events)
    {
        foreach(Event event in events)
        {
            if(!event.IsValidFor(this)
                throw new Exception(...);

            ApplyEvent(event);
        }
    }
}

Questo approccio ha più senso per me. C'è qualche potenziale odore qui che non vedo? C'è qualcos'altro che non sto considerando?

Modifica:

Un'altra idea che ho avuto è forse che il gestore di comandi ha bisogno di convalidare il comando, interrogando il modello letto per assicurarsi che esista un aggregato di tipo previsto con l'ID specificato. Ma anche se questo risulta essere l'approccio corretto, c'è qualcosa di sbagliato nell'associare eventi a tipi di aggregati specifici?

    
posta Entith 23.08.2018 - 20:43
fonte

3 risposte

1

Is there something else I'm not considering?

Una cosa che non stai considerando è un'interfaccia tipicamente idonea per i tuoi identificatori.

Customer customer = repo.Get<Customer>(productId);
Product product = repo.Get<Product>(customerId);

Se questo tipo di errore è ciò di cui sei preoccupato, perché stai utilizzando lo stesso tipo per il tuo ID? Avvolgi l'implementazione attorno a due tipi diversi, in modo che il compilatore possa distinguerli.

L'implementazione del repository può far scattare il guid fuori dal tipo quando sono necessari i dati grezzi.

CustomerId customerId = CustomerId.from(...)
ProductId productId = ProductId.from(...)

Customer customer = repo.Get<Customer>(productId); // Type Checker can catch this
    
risposta data 23.08.2018 - 21:18
fonte
1

Quello che stai cercando di impedire è strano perché non c'è differenza tra gli eventi che vengono ignorati intenzionalmente e gli eventi sbagliati.

Una soluzione consiste nel caricare gli eventi in base non solo all'ID di aggregazione, ma anche al tipo (o classe) di Aggregate. Per questo è necessario anche archiviare il tipo di Aggregate nel flusso di eventi e utilizzarlo quando si eseguono query sugli eventi di Aggregate.

L'aggiunta del tipo Aggregate al flusso di eventi apre la porta a più Aggregati che condividono lo stesso ID, che è una buona cosa, dato che puoi usare la stessa istanza dell'archivio eventi per più contesti limitati.

    
risposta data 24.08.2018 - 07:38
fonte
0

Come ha detto VoiceOfUnreason, un aspetto di questa è l'ossessione primitiva degli ID. Non c'è praticamente posto nel codice in cui productGUID e customerGUID dovrebbero essere nello scope contemporaneamente.

In particolare, ProductId.From(customerGuid) non si verifica perché non chiameresti alcun ProductId.From() in un contesto in cui hai un customerGuid non elaborato.

Inoltre, se non stai eseguendo il binding pigro del dominio, potresti aggiungere un tipo agli eventi, come suggerito da Constantin Galbenu, ma forse in un modo leggermente diverso:

È probabile che tu abbia un determinato tipo di evento o un determinato enum di tipi di eventi, in ogni caso inferiore al numero totale di tipi di eventi per l'aggregato, che può essere il primo evento mai visto per questo aggregato, a partire dal banale ProductRegisteredEvent . In questo modo, quando esegui query e ottieni >0 di eventi, ma il primo evento non è un ProductRegisteredEvent , puoi lanciare immediatamente senza aggiungere altri eventi.

Questo non ti consentirà di avere più aggregati separati con lo stesso ID, ma in questo modo ti permetterà di non avere alcun riferimento al tipo negli ulteriori eventi.

Infine, voglio essere certo di capire perché hai in atto la politica "Ignora gli eventi restituiti ma sconosciuti". Stai facendo questo nelle proiezioni al contrario del business logic aggregate? Gli eventi sconosciuti sono insignificanti per la convalida della business logic?

O disponi già di aggregati diversi che coesistono sullo stesso ID disponendo di insiemi di tipi di eventi che si escludono a vicenda? (Ciò significa che hai già cotto la proprietà 'aggregateType' nel tipo di evento, in un certo senso.

    
risposta data 30.08.2018 - 01:51
fonte

Leggi altre domande sui tag