A RetroSearch Logo

Home - News ( United States | United Kingdom | Italy | Germany ) - Football scores

Search Query:

Showing content from https://github.com/GreanTech/AtomEventStore/wiki/Reading-events below:

Reading events · GreanTech/AtomEventStore Wiki · GitHub

You can read events either forwards or backwards. In both cases, reading events is based on the standard IEnumerable<T> interface.

Events can be written using the AtomEventObserver<T> class.

Reading in original order

If you want to read the events in the order they were written, with the oldest event first, you should use the First-In, First-Out reader:

IEnumerable<object> events = new FifoEvents<object>(
    eventStreamId, // a Guid
    storage,       // an IAtomEventStorage object
    serializer);   // an IContentSerializer object
var firstEvent = events.First();

It's not necessary to explicitly declare events as IEnumerable<object>: you can use the var keyword as well; this example just uses explicit variable declaration in order to make it clearer what's going on.

The storage variable can be any IAtomEventStorage implementation.

The serializer variable can be any IContentSerializer implementation.

If you want to read the most recent events first, you can use the LifoEvents<T> class instead of FifoEvents<T>: it provides a Last-In, First-Out Iterator over the event stream.

Filtering, aggregation, and projections

Since both FifoEvents<T> and LifoEvents<T> implement IEnumerable<T>, you can perform any filtering, aggregation, and projection operation you're used to be able to do with LINQ. However, be aware that there's no protocol translation going on (IQueryable<T> is not in use). All operations on the event stream happen on the Iterator in memory, so if you're not careful, you may inadvertently read the entire event stream into memory from storage.

However, with a bit of care, and judicious selection of FifoEvents<T> or LifoEvents<T> you can still make your system efficient.

Interpreting events using an Acyclic Visitor

In object-oriented code with e.g. C#, the simplest (although not the most type-safe) way to interpret event streams is to use the Acyclic Visitor pattern. This enables you to aggregate events into a domain object, using run-time downcasts.

The advantage of this approach is that it's relatively easy to understand and implement. The disadvantage is that it's not type-safe, and, due to downcasting, prone to violate the Liskov Substitution Principle.

Consider a simple user sign-up process. A new user wishes to sign up for a service, so first provides a user name, password, and email address. Upon receiving the data, the system sends an email to the user, asking her or him to validate the address.

The events in the system may include:

(All the code in this example is available in the unit test library in the AtomEventStore code base.)

Such events can be written to the underlying storage, and FifoEvents<T> can be used to play back the events in the same order they happened. A User domain object can interpret such an event stream, aggregating the data into a snapshot of the current state of the user:

public class User
{
    private readonly Guid id;
    private readonly string name;
    private readonly string password;
    private readonly string email;
    private readonly bool emailVerified;

    public User(
        Guid id,
        string name,
        string password,
        string email,
        bool emailVerified)
    {
        this.id = id;
        this.name = name;
        this.password = password;
        this.email = email;
        this.emailVerified = emailVerified;
    }

    public static User Create(UserCreated @event)
    {
        return new User(
            @event.UserId,
            @event.UserName,
            @event.Password,
            @event.Email,
            false);
    }

    public User Accept(EmailVerified @event)
    {
        return new User(
            this.id,
            this.name,
            this.password,
            this.email,
            true);
    }

    public User Accept(EmailChanged @event)
    {
        return new User(
            this.id,
            this.name,
            this.password,
            @event.NewEmail,
            false);
    }

    private User Accept(object @event)
    {
        var emailChanged = @event as EmailChanged;
        if (emailChanged != null)
            return this.Accept(emailChanged);

        var emailVerified = @event as EmailVerified;
        if (emailVerified != null)
            return this.Accept(emailVerified);

        throw new ArgumentException("Unexpected event type.", "@event");
    }

    public static User Fold(IEnumerable<object> events)
    {
        var first = events.First();
        var rest = events.Skip(1);

        var created = first as UserCreated;
        if (created == null)
            throw new ArgumentException("The first event must be a UserCreated instance.", "events");

        var user = User.Create(created);
        return rest.Aggregate(user, (u, e) => u.Accept(e));
    }

    public static User Fold(params object[] events)
    {
        return User.Fold(events.AsEnumerable());
    }

    public Guid Id
    {
        get { return this.id; }
    }

    public string Name
    {
        get { return this.name; }
    }

    public string Password
    {
        get { return this.password; }
    }

    public string Email
    {
        get { return this.email; }
    }

    public bool EmailVerified
    {
        get { return this.emailVerified; }
    }
}

The User class understands how to aggregate an event stream into a User object, but alternatively, a separate Service could have that responsibility. It would also be possible to create a variation on this implementation that uses mutable state in the User class, instead of relying on copying of instances, but conceptually, it would still be the same solution.

The relevant entry points here are the two Fold overloads that take a stream of objects as input. Notice that Fold(IEnumerable<object>) attempts to downcast the first event to a UserCreated instance. In this implementation, this is the only legal way to initiate a new user, but other implementations may have less strict requirements.

The Fold method aggregates the rest of the event stream by repeatedly delegating to the Accept(object) method, which attempts more downcasts. If none of the downcasts succeed, an exception is thrown.

With this implementation, an event stream can be read and interpreted with AtomEventStore like this:

var events = new FifoEvents<object>(
    eventStreamId, // a Guid
    storage,       // an IAtomEventStorage object
    serializer);   // an IContentSerializer object
var user = User.Fold(events);

Notice that events is an instance of FifoEvents<object>; although FifoEvents<T> is a generic class, the events in this particular even stream share no common type, so the lowest common denominator is object. This underlines the lack of type-safety in this particular implementation.

Some people like to let all event classes implement a common IEvent interface, but the most common interface definitions are either Marker Interfaces, or interfaces that look like this:

public interface IEvent
{
    Guid Id { get; }
}

While such a hypothetical IEvent interface force all events to have an ID, it doesn't prevent downcasting when interpreting an event stream, so it adds limited value.

While the Acyclic Visitor pattern is easy to get started with, it tends to be brittle because it essentially bypasses the type checking built into languages such as C#.

Interpreting events using a Visitor

A more type-safe alternative to using an Acyclic Visitor is to use the original Visitor pattern. The Visitor pattern can be a good choice when you have a well-known set of types that are somehow related, but are structurally different, and share little behaviour.

Compared to using an Acyclic Visitor, using a Visitor has the following advantages:

However, the disadvantage compared to Acyclic Visitor is that the Visitor solution tends to be more complex.

Consider a simple user sign-up process. A new user wishes to sign up for a service, so first provides a user name, password, and email address. Upon receiving the data, the system sends an email to the user, asking her or him to validate the address.

The events in the system may include:

(All the code in this example is available in the unit test library in the AtomEventStore code base.)

Each event class implements the IUserEvent interface:

public interface IUserEvent
{
    IUserVisitor Accept(IUserVisitor visitor);
}

They all implement the Accept method in the same way:

public IUserVisitor Accept(IUserVisitor visitor)
{
    return visitor.Visit(this);
}

Such events can be written to the underlying storage, and FifoEvents<T> can be used to play back the events in the same order they happened.

In order to interpret user events, you can implement the IUserVisitor, which is defined like this:

public interface IUserVisitor
{
    IUserVisitor Visit(UserCreated @event);

    IUserVisitor Visit(EmailVerified @event);

    IUserVisitor Visit(EmailChanged @event);
}

Notice how the IUserVisitor interface provides a finite list of all concrete event types that can be handled by this system (UserCreated, EmailVerified, EmailChanged). This helps to remind you to handle all possible event types when implementing the interface.

You can implement IUserVisitor to collect all events into one or more User objects:

public class UserVisitor : IUserVisitor
{
    private readonly IEnumerable<User> users;

    public UserVisitor(IEnumerable<User> users)
    {
        this.users = users;
    }

    public UserVisitor(params User[] users)
        : this(users.AsEnumerable())
    {
    }

    public IEnumerable<User> Users
    {
        get { return this.users; }
    }

    public IUserVisitor Visit(UserCreated @event)
    {
        var user = new User(
            @event.UserId,
            @event.UserName,
            @event.Password,
            @event.Email,
            false);
        return new UserVisitor(this.users.Concat(new[] { user }));
    }

    public IUserVisitor Visit(EmailVerified @event)
    {
        var origUser = this.users.Single(u => u.Id == @event.UserId);
        var newUser = new User(
            origUser.Id,
            origUser.Name,
            origUser.Password,
            origUser.Email,
            true);
        var newUsers = this.users
            .Where(u => u.Id != newUser.Id)
            .Concat(new[] { newUser });
        return new UserVisitor(newUsers);
    }

    public IUserVisitor Visit(EmailChanged @event)
    {
        var origUser = this.users.Single(u => u.Id == @event.UserId);
        var newUser = new User(
            origUser.Id,
            origUser.Name,
            origUser.Password,
            @event.NewEmail,
            false);
        var newUsers = this.users
            .Where(u => u.Id != newUser.Id)
            .Concat(new[] { newUser });
        return new UserVisitor(newUsers);
    }
}

Notice how UserVisitor doesn't need to attempt to downcast incoming events, because each event has a well-known Visit overload.

There are various ways to implement such a Visitor; this particular example collects a sequence of User objects, enabling it to (potentially) handle events for more than a single user at a time.

In order to keep the example as simple as possible, the Visit(EmailVerified) and Visit(EmailChanged) methods implicitly assume that a corresponding user already exists. It would be possible to create a more robust implementation at the cost of more code.

The User class can use UserVisitor to aggregate a sequence of events into a User instance:

public class User
{
    private readonly Guid id;
    private readonly string name;
    private readonly string password;
    private readonly string email;
    private readonly bool emailVerified;

    public User(
        Guid id,
        string name,
        string password,
        string email,
        bool emailVerified)
    {
        this.id = id;
        this.name = name;
        this.password = password;
        this.email = email;
        this.emailVerified = emailVerified;
    }

    public static User Fold(IEnumerable<IUserEvent> events)
    {
        var result = events.Aggregate(
            new UserVisitor(),
            (v, e) => (UserVisitor)e.Accept(v));
        return result.Users.Single();
    }

    public Guid Id
    {
        get { return this.id; }
    }

    public string Name
    {
        get { return this.name; }
    }

    public string Password
    {
        get { return this.password; }
    }

    public string Email
    {
        get { return this.email; }
    }

    public bool EmailVerified
    {
        get { return this.emailVerified; }
    }
}

Notice how the Fold method uses UserVisitor to aggregate the events and return a correctly populated User instance.

A client can use FifoEvents<IUserEvent> to create a User object from an event stream:

var events = new FifoEvents<IUserEvent>(
    eventStreamId, // a Guid
    storage,       // an IAtomEventStorage object
    serializer);   // an IContentSerializer object
var user = User.Fold(events);

Using the Visitor pattern enables you to use a proper interface (like IUserEvent) as the generic type argument for both FifoEvents<T>, LifoEvents<T>, and AtomEventObserver<T>. Compared to the Acyclic Visitor implementation, it offfers a more type-safe solution, but at the expense of additional complexity.

Interpreting events with F# using a Discriminated Union

While interpretation and aggregation is either weakly typed or quite complicated using Object-Oriented C#, it can be simple and strongly typed in F#, using Discriminated Unions. Like the Visitor pattern example above, using a Discriminated Union in F# has the following advantages:

Contrary to the Visitor example, there are few disadvantages, as Discriminated Unions is a core feature of F#, so the use of it is very idiomatic.

Consider a simple user sign-up process. A new user wishes to sign up for a service, so first provides a user name, password, and email address. Upon receiving the data, the system sends an email to the user, asking her or him to validate the address.

The events in the system may include:

(All the code in this example is available in the F# unit test library in the AtomEventStore code base.)

The events are defined as record types:

[<CLIMutable>]
type UserCreated = {
    UserId : Guid
    UserName : string
    Password : string
    Email : string }

[<CLIMutable>]
type EmailVerified = {
    UserId : Guid
    Email : string }

[<CLIMutable>]
type EmailChanged = {
    UserId : Guid
    NewEmail : string }

Such events can be written to the underlying storage, because they are adorned with the [<CLIMutable>] attribute.

A Discriminated Union defines all the events that can occur in the system:

type UserEvent =
    | UserCreated of UserCreated
    | EmailVerified of EmailVerified
    | EmailChanged of EmailChanged

You can use FifoEvents<UserEvent> to play back the events in the same order they happened.

If, for example, you want to interpret the event stream as one or more users, the first thing you'll need to do is to define a User record type:

type User = {
    Id : Guid
    Name : string
    Password : string
    Email : string
    EmailVerified : bool }

In order to interpret a sequence of UserEvent records into one or more User values, you can define a module with the appropriate functions:

module UserEvents =
    let createUser (uc : UserCreated) =
        {
            Id = uc.UserId
            Name = uc.UserName
            Password = uc.Password
            Email = uc.Email
            EmailVerified = false
        }

    let verifyEmail u = { u with EmailVerified = true }

    let changeEmail ec u = { u with Email = ec.NewEmail; EmailVerified = false }

    let applyEvent users e =
        let findUser id = users |> Seq.find (fun x -> x.Id = id)
        let updateUser u =
            users
            |> Seq.filter (fun x -> x.Id <> u.Id)
            |> Seq.append [u]
        match e with
        | UserCreated uc -> users |> Seq.append [createUser uc]
        | EmailVerified ev -> findUser ev.UserId |> verifyEmail |> updateUser
        | EmailChanged ec -> findUser ec.UserId |> changeEmail ec |> updateUser

    let foldEvents events =
        events |> Seq.fold applyEvent Seq.empty

Notice that createUser, verifyEmail, and changeEmail are helper functions created with the sole purpose of making the applyEvent function easier to read; each of these three functions are only used once inside of applyEvent, so they could have been inlined.

All three helper functions take the appropriate event as input, and returns a User value.

The applyEvent function has the signature seq<User> -> UserEvent -> seq<User>; in other words, it takes an already (partially) populated sequence of users, as well as a single UserEvent, and returns a new sequence of users, with the event applied.

It matches the incoming event with all the cases in the UserEvent Discriminated Union, and applies the result to the users. If the event is a UserCreated event, it creates a new User value and appends it to users; otherwise, it finds the affected user by its Id, applies the event to that value, and updates the users.

Finally, the foldEvents function has the signature seq<UserEvent> -> seq<User>; in other words, it takes an event stream of UserEvent values, and interprets it as a sequence of User values. Since the event stream may contain more than one UserCreated event, the result may be more than a single User value.

A client can use FifoEvents<UserEvent> to create a User value from an event stream:

let events =
    FifoEvents<UserEvent>(
        eventStreamId, // an UuidIri (basically, a Guid)
        storage,       // an IAtomEventStorage object
        serializer)    // an IContentSerializer object
let users = events |> UserEvents.foldEvents

Using a Discriminated Union to interpret the event stream lets you use pattern matching and F#'s built-in aggregation functions to a arrive at a concice and type-safe solution.


RetroSearch is an open source project built by @garambo | Open a GitHub Issue

Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo

HTML: 3.2 | Encoding: UTF-8 | Version: 0.7.4