Skip to content
This repository has been archived by the owner on Jan 1, 2024. It is now read-only.

Quick Start

David Rettenbacher edited this page Aug 6, 2017 · 8 revisions

SimpleEventSourcing enables you to create event sourced applications with ddd, event-streams and read-models.

Events

Events describe what happened in your domain. I.e. CustomerRegistered, CustomerAddressTypoFixed, CustomerMoved,...

Your events must be marked with the IEvent interface.

[Versioned("ArticleCreated", 0)]
public class ArticleCreated : IEvent
{
    public string Articlenumber { get; private set; }
    public string Description { get; private set; }
    public DateTime DateTime { get; private set; }
    
    public ArticleCreated(
        string id,
        string articlenumber,
        string description,
        DateTime dateTime)
    {
        Id = id;
        Articlenumber = articlenumber;
        Description = description;
        DateTime = dateTime;
    }
}
  • Versioned["<identifier>", <Version>]
    Attribute used to make events refactorable and evolveable. With this in place you can move to different namespaces/assemblies and/or rename the event class without having to worry about old events not getting deserialized anymore.
  • Event Name
    Past tense because it already happened and can't be changed ("it's part of the immutable past")
  • Properties
    Contain the data neccessary to describe the change expressed by the event at hand.
    Public are only getters, not setters, because of the immutable nature of events.
    Tip: always save the date and time an event happend.

Aggregate-Roots

Aggregate-roots' responsibility is to keep its state consistent - at all times! It guards its state by just letting actions (aka "commands") happen, if and only if they are allowed in the current state. I.e. an deactivated article cannot be put into a shopping cart.

In SimpleEventSourcing the aggregate-roots really only contain the logic to

  • check if action is possible
  • raise an event if action is possible and action was carried out

Aggregates must implement IAggregateRoot interface. For ease of use you can just inherit from AggregateRoot<TState, TKey>

  • TState
    The current state of your aggregate
  • TKey
    The type of your aggregate-root key, i.e. Guid, string, int or a custom type like ArticleId

Here's an example of a simple aggregate-root:

public class Article : AggregateRoot<ArticleState, ArticleId>
{
    public Article() : base(Enumerable.Empty<IEvent>()) { }

    public Article(ArticleId id, Articlenumber articlenumber, string description)
        : base(new ArticleCreated(id, articlenumber, description, DateTime.UtcNow))
    {

    }

    public void ChangeArticlenumber(Articlenumber articlenumber)
    {
        RaiseEvent(new ArticleArticlenumberChanged(Id, articlenumber, DateTime.UtcNow));
    }
}

Constructor

The constructor of AggregateRoot<,> accepts either

  • one event or
  • a list of events
    Moreover it supports an optional initial state.

You can use the overload with one event to pass the creation-event (as seen in the example above).
Providing an initial state could come handy later by the time snapshots get supported.

RaiseEvent

RaiseEvent is used to add a new event to the events to be saved later on. It adds the event to a list of uncommitted events. By saving the aggregate-root these events get added permanently in the write-model.

Child-Entities

Child-entities are entities that require an aggregate-root to exist, i.e. a shopping-cart-item needs a shopping-cart - without it it cannot exist.

Events of child-entities are saved in the stream of the aggregate-root. The state of the child-entity is part of the aggregate-root-state and cannot exist without it.

Child-entities contain logic to check, whether an action is valid for this child-entity.

I.e. removing a shopping-cart-item from a shopping-cart. If the item would be already marked deleted or the containing shopping-cart is already ordered, the item cannot be removed.

If the action is valid it raises a new child-event via the RaiseEvent method.

Here's an example of a child-entity:

public class ShoppingCartArticle : ChildEntity<ShoppingCartArticleState, ShoppingCartId, ShoppingCartArticleId>
    {
        public ShoppingCartArticle() { }
        public ShoppingCartArticle(ShoppingCart aggregateRoot, IEnumerable<IChildEntityEvent> events) : base(aggregateRoot, events) { }
        public ShoppingCartArticle(ShoppingCart aggregateRoot, ShoppingCartArticleId shoppingCartArticleId, ArticleId articleId, Articlenumber articlenumber, string description, int quantity, DateTime? dateTime)
            : base(aggregateRoot, new[] { new ShoppingCartArticlePlaced(aggregateRoot.Id, shoppingCartArticleId, articleId, articlenumber, description, quantity, dateTime ?? DateTime.UtcNow) })
        {

        }

        public void RemoveFromShoppingCart()
        {
            RaiseEvent(new ShoppingCartArticleRemoved(aggregateRootId, Id, StateModel.ArticleId, StateModel.Articlenumber, StateModel.Description, StateModel.Quantity, DateTime.UtcNow));
        }
    }
  • Constructor
    The constructor needs a reference to the containing aggregate-root, its own id and the other required data to build up a valid new child-entity.
  • ChildEntity<<type of child-entity-state>, <type of aggregate-root-id>, <type of child-entity-id>>
    Child-entities must implement IChildEntity<,,>, but for convenience just inherit from ChildEntity<,,>.
    For informations on the type-parameters, see "Aggregates".

States

States hold the current calculated state of an entity or projection. A state gets populated by passing a series of relevant events into it one after another in chronological order.

Projections

The term "projection" is used to describe the process of converting a stream of events into a more queryable representation. Projections use states for this. You can think of projections as the package of reading a event-stream and states that consume them.

But there are 2 types of projections:

  • transient and
  • persistent.

Transient vs. Persistent Projections

Transient projections are created and started just-in-time, to create transient data - it is lost as soon as the application or code-block completes.

Persistent projections are usually created and started at application-start, and create persistent data. This data could be files, database-tables, or whatever is required.
Persistent projections also remember what event was last processed, so it can resume from there on. SimpleEventSourcing uses an incrementing checkpoint-number.

States of Entities (Transient)

Here an example of a state for an aggregate-root:

public class ArticleState : AggregateRootState<ArticleState, ArticleId>
{
    public string Articlenumber { get; private set; }
    public bool Active { get; private set; }
    public string Description { get; private set; }

    public ArticleState() { }
    public ArticleState(ArticleState state)
        : base(state)
    {
        Articlenumber = state.Articlenumber;
        Active = state.Active;
        Description = state.Description;
    }

    public ArticleState Apply(ArticleCreated @event)
    {
        var s = new ArticleState(this);
        s.StreamName = @event.Id;
        s.Articlenumber = @event.Articlenumber;
        s.Description = @event.Description;
        s.Active = true;

        return s;
    }

    public override object ConvertFromStreamName(Type tkey, string streamName)
    {
        return new ArticleId(streamName);
    }

    public override string ConvertToStreamName(Type tkey, object id)
    {
        return ((ArticleId)id).Value;
    }
}
  • AggregateRootState<<type of current state>, <type of aggregate-root key>>
    A state for an aggregate-root inherits from AggregateRootState<,>.
  • Apply( @event)
    A state can implement any number of handlers for events. Such event handlers are resolved at runtime, no need to register them explicitly.
    The return-value can be either void or the type of the current state. Returning a new state of the same type but with the new changes underlines the immutability of a state. If you choose to return void it works, but the immutable nature of such a state is lost.

Projecting

To project persistent data SimpleEventSourcing offers the CatchUpProjector<TState> class.

CatchUpProjector is for persistent states as it saves the last processed checkpoint-number.

TState should be a state inherited from ReadRepositoryState.

[ControlsReadModels(new[] { typeof(ArticleViewModel) })]
public class ArticleReadModelState : ReadRepositoryState<ArticleReadModelState>
{
    public ArticleReadModelState() { throw new NotSupportedException(); }

    public ArticleReadModelState(IReadRepository readRepository)
        : base(readRepository)
    {

    }

    public Task Apply(ArticleCreated @event)
    {
        var article = new ArticleViewModel();
        article.ArticleId = @event.Id;
        article.Articlenumber = @event.Articlenumber;
        article.Description = @event.Description;
        article.Active = true;

        return InsertAsync(article);
    }

    public Task Apply(ArticleArticlenumberChanged @event)
    {
        return UpdateByStreamnameAsync<ArticleViewModel>(@event.Id,
            article =>
            {
                article.Articlenumber = @event.Articlenumber;
            });
    }
}
  • ControlsReadModels
    Specifies the read-model entity-types used to store the projected data. As the name implies, the tables represented by this read-model types are under full control of TState; no other state is going to alter this data. This is required so multiple projectors can be running in parallel and restarted independently. This is an advantage, if a faulty or changed projection needs to be rebuilt without rebuilding all others.
    This attribute's information is used by CatchUpProjector to (re-)generate the tables with the right schema if no previous handled checkpoint is found (checkpoint-number == -1).
  • Constructor
    Currently it is required to provide a default constructor on states. In this case this is not supported and should throw a NotSupportedException. A persistent read-repository-state needs a read-repository passed in to execute the CRUD operations on the data-target.
  • CRUD Operations
    ReadRepositoryState offers the following methods to simplify the update operations:
    • InsertAsync
    • GetByStreamName
    • UpdateAsync
    • UpdateByIdAsync
    • UpdateByStreamnameAsync and
    • QueryAndUpdateAsync.