Skip to content

CanerPatir/Algorist.EventSourcing

Repository files navigation

An extensible lightweight library for .NET that manages the Aggregate lifecycle in an Event Sourced system.

Supports Event and Snapshot storage providers like EventStore/Redis or SQL Server. Built with dependency injection in mind and seamlessly integrates with AspNetCore.

What is Event Sourcing?

Use an append-only store to record the full series of events that describe actions taken on data in a domain, rather than storing just the current state.

Start here https://dasith.me/2016/12/02/event-sourcing-examined-part-1-of-3/

What does it solve?

It makes it easier to implement the event sourcing pattern in your .NET project. The library is opinionated and introduces some patterns to manage the life cycle of your Aggregates in an event sourced system. It will manage Creating, Loading, Mutating and Persisting Aggregates and Events.

What doesn't it solve?

It is not a framework that manages your application end to end. It doesn't enforce ports and adapters pattern or any of the application level concerns. The aim is to do one thing (Manage aggregate lifecycle) and do that well. If you need to implement command and event handlers you can have a look at something like SimpleMediator or Brighter and it will complement them nicely.

Before you start

  • The library targets .NET Standard 2.1
  • Optional: Installation of EventStore - https://geteventstore.com/ (You can use the in memory event and snapshot providers when developing)

Using It

Define the events. They are simple pocos that will be serialized and stored in EventStorage when changes are saved. Events use Guid for Id by default but they can be changed to use any data type as Id. See Event<TAggregateKey, TEventKey> for reference.

    public class ScheduleCreatedEvent : Event<Schedule>
    {
        public string ScheduleName { get; set; }

        public ScheduleCreatedEvent(Guid scheduleId, string scheduleName) : base(Guid.NewGuid(), scheduleId)
        {
            ScheduleName = scheduleName;
        }
    }

    public class TodoCreatedEvent : Event<Schedule>
    {
        public Guid TodoId { get; set; }
        public string Text { get; set; }

        public TodoCreatedEvent(Guid aggregateId, int targetVersion, Guid todoId, string text) : base(Guid.NewGuid(), aggregateId, targetVersion)
        {
            TodoId = todoId;
            Text = text;
        }
    }

Define the Aggregate (Schedule.cs in the sample). Aggregates use Guid as Id by default but just like the events, they can be changed to the Id type of your choice. See AggregateRoot<TAggregateKey> for reference.

    public class Schedule : AggregateRoot
    {
        public IList<Todo> Todos { get; private set; }
        public string ScheduleName { get; private set; }

        // To create or mutate, call the constructor or methods. 
        
        // Constructor
        public Schedule(string scheduleName)
        {
            // Pattern: Create an event and apply it
            var newScheduleId = Guid.NewGuid();
            var @event = new ScheduleCreatedEvent(newScheduleId, scheduleName);
            ApplyEvent(@event);
        }

        public Guid AddTodo(string text)
        {
            // Pattern: Create an event and apply it
            var newTodoId = Guid.NewGuid();
            var @event = new TodoCreatedEvent(Id, CurrentVersion, newTodoId, text);
            ApplyEvent(@event);
            return newTodoId;
        }
        
        // ** Mutations happen through applying events **
        // The library identifies the internal event handler methods though a special method attribute.
        
        [InternalEventHandler]
        public void OnScheduleCreated(ScheduleCreatedEvent @event)
        {
            ScheduleName = @event.ScheduleName;
            Todos = new List<Todo>();
        }

        [InternalEventHandler]
        public void OnTodoCreated(TodoCreatedEvent @event)
        {
            var todo = new Todo(@event.TodoId, @event.Text);
            Todos.Add(todo);
        }
    }

Using the built in Session and Repository implementations to manage the Aggregate lifecycle.

// We recommend using a DI container to inject the Session. Keep it scoped (per request in a web application) 

    public class CreateScheduleHandler
    {
        private readonly ISession<Schedule> _session;

        public CreateScheduleCommandHandler(ISession<Schedule> session)
        {
            _session = session;
        }

        public async Task<Guid> HandleAsync()
        {
            var schedule = new Schedule("my new schedule");
            _session.Attach(schedule);
            await _session.SaveAsync();
            return schedule.Id;
        }
    }

    public class AddTodoHandler
    {
        private readonly ISession<Schedule> _session;

        public AddTodoCommandHandler(ISession<Schedule> session)
        {
            _session = session;
        }

        public async Task<Guid> HandleAsync(Guid scheduleId)
        {
            var schedule = await _session.GetByIdAsync(scheduleId);
            var id = schedule.AddTodo("test todo 1");
            await _session.SaveAsync();
            return id;
        }
    }

Storage providers

The library contains storage provider implementation for EventSore and we plan to include a few more in the future. We have also included an in memory event and snapshot storage provider to get you up and running faster.

It's very easy to implement your own as well. Implement IEventStorageProvider and ISnapshotStorageProvider interfaces an you're good to plug them in. If you need help look at the Algorist.EventSourcing.Storage.EventStore project in the repository.

    public interface IEventStorageProvider<TAggregate, TAggregateKey, TEventKey> where TAggregate : AggregateRoot<TAggregateKey, TEventKey>
    {
        Task<IEnumerable<IEvent<AggregateRoot<TAggregateKey, TEventKey>, TAggregateKey, TEventKey>>> GetEventsAsync(TAggregateKey aggregateId, int start, int count);
        Task<IEvent<AggregateRoot<TAggregateKey, TEventKey>, TAggregateKey, TEventKey>> GetLastEventAsync(TAggregateKey aggregateId);
        Task SaveAsync(AggregateRoot<TAggregateKey, TEventKey> aggregate);
    }

    public interface ISnapshotStorageProvider<TSnapshot, in TAggregateKey, TSnapshotKey> where TSnapshot: ISnapshot<TAggregateKey, TSnapshotKey>
    {
        int SnapshotFrequency { get; }
        Task<TSnapshot> GetSnapshotAsync(TAggregateKey aggregateId);
        Task SaveSnapshotAsync(TSnapshot snapshot);
    }

About

An event sourcing building block for .net core

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages