EventSourcing.NetCore

Examples and Tutorials of Event Sourcing in .NET

MIT License

Stars
3.2K
Committers
31

Bot releases are visible (Hide)

EventSourcing.NetCore - Introduction to Event Sourcing - Self Paced Kit

Published by oskardudycz over 2 years ago

Event Sourcing is perceived as a complex pattern. Some believe that it's like Nessie, everyone's heard about it, but rarely seen it. In fact, Event Sourcing is a pretty practical and straightforward concept. It helps build predictable applications closer to business. Nowadays, storage is cheap, and information is priceless. In Event Sourcing, no data is lost.

The workshop aims to build the knowledge of the general concept and its related patterns for the participants. The acquired knowledge will allow for the conscious design of architectural solutions and the analysis of associated risks.

The emphasis will be on a pragmatic understanding of architectures and applying it in practice using Marten and EventStoreDB.

  1. Introduction to Event-Driven Architectures. Differences from the classical approach are foundations and terminology (event, event streams, command, query).
  2. What is Event Sourcing, and how is it different from Event Streaming. Advantages and disadvantages.
  3. Write model, data consistency guarantees on examples from Marten and EventStoreDB.
  4. Various ways of handling business logic: Aggregates, Command Handlers, functional approach.
  5. Projections, best practices and concerns for building read models from events on the examples from Marten and EventStoreDB.
  6. Challenges in Event Sourcing and EDA: deliverability guarantees, sequence of event handling, idempotency, etc.
  7. Saga, Choreography, Process Manager, distributed processes in practice.
  8. Event Sourcing in the context of application architecture, integration with other approaches (CQRS, microservices, messaging, etc.).
  9. Good and bad practices in event modelling.
  10. Event Sourcing on production, evolution, events' schema versioning, etc.

You can do the workshop as a self-paced kit. That should give you a good foundation for starting your journey with Event Sourcing and learning tools like Marten and EventStoreDB. If you'd like to get full coverage with all nuances of the private workshop, feel free to contact me via email.

Exercises

  1. Events definition.
  2. Getting State from events.
  3. Appending Events:
  4. Getting State from events
  5. Business logic:
  6. Optimistic Concurrency:
  7. Projections:

Related PRs:

EventSourcing.NetCore - Fixed Kafka integration to correctly publish and get messages

Published by oskardudycz over 2 years ago

Fixed Kafka integration to correctly publish and get messages:

  • Fixed Kafka producer and consumer to correctly serialize/deserialize send Event with event envelope.
  • Upgraded Kafka images to the latest 7.0.1 version.
  • Added a basic test for Kafka Producer.
  • Switched Kafka UI from landoop/kafka-topics-ui to landoop/kafka-ui.

See details in https://github.com/oskardudycz/EventSourcing.NetCore/pull/120.

Besides that:

  • Removed obsolete IIS express run profiles.
  • Fixed async method signatures in tests.
  • Fixed copy/paste naming issues in Cancel Shopping Carts API tests.

See details in https://github.com/oskardudycz/EventSourcing.NetCore/pull/122.

EventSourcing.NetCore - Various improvements and bug fixes for Event Sourcing Samples

Published by oskardudycz over 2 years ago

  1. Made an alignment of ECommerce samples: https://github.com/oskardudycz/EventSourcing.NetCore/pull/118:
  • Renamed Initialize Shopping Cart to Open Shopping Cart to be more closer to real terminology,
  • Added Cancel Shopping Cart operation,
  • Aligned Product Items implementation.
  1. Updated MartenEventPublisher to publish events with metadata https://github.com/oskardudycz/EventSourcing.NetCore/pull/112
  2. Fixed Marten Append Scope registration: https://github.com/oskardudycz/EventSourcing.NetCore/pull/111
  3. Upgraded Marten to v5 release candidate 1: https://github.com/oskardudycz/EventSourcing.NetCore/pull/115
  4. Shift tenant to aggregateId part of streamId to properly handle EventStoreDB category projections: https://github.com/oskardudycz/EventSourcing.NetCore/pull/117
EventSourcing.NetCore - Added Correlation and CausationId support in samples

Published by oskardudycz over 2 years ago

Implemented correlation id and causation id for tracing operations fully.

Refactored previously existing CorrelationIdMiddleware by adding causation id handling and renamed it to more general TracingMiddleware. It takes correlation id and causation id from HTTP headers and caches them into TracingScope.

TracingScope is also setting logging scope internally to keep Correlation id and causation id in logs. Thanks to that, the logic for tracing setup can be reused in middleware and subscriptions. The new causation id is generated based on the event id in subscriptions. Thanks to that, we can build the tree with a history of event handling.

Updated Optimistic Concurrency Scopes and generalised into AppendScope. It wraps both tracing and expected version handling. Pushed tracing metadata into Marten and EventStoreDB storage.

Replaced custom EventBus instead of Mediator one to have more flexibility (e.g. be able to create new DI and logging scopes).

See more in PRs: #106 and #108.

EventSourcing.NetCore - Added Marten Outbox Pattern/Subscription with custom Projection

Published by oskardudycz over 2 years ago

Added Marten Outbox Pattern/Subscription with custom Projection Plugged it into samples removing calling EventBus from the repository, by that getting at-least-once processing guarantee.

Read more about that in: https://event-driven.io/en/integrating_Marten/.

Other changes:

  • Updated Marten to latest v5 alpha
  • Disabled API tests parallelisation until better test setup is provided

See more in PR: https://github.com/oskardudycz/EventSourcing.NetCore/pull/104

EventSourcing.NetCore - Removed not working correctly Github Action with Test Results.

Published by oskardudycz over 2 years ago

Removed not working correctly Github Action with Test Results.

Besides that:

  • Added Directory.Build.props and removed some commonly shared settings
  • Renamed project Core.Streaming.Kafka into Core.Kafka
EventSourcing.NetCore - Added full Optimistic Concurrency handling in all samples with integration to ETag

Published by oskardudycz over 2 years ago

Added optimistic concurrency to samples and did a huge all-around refactoring.

The most significant changes:

  • Added OptimisticConcurrencyMiddleware and related classes to support full flow based on the ETag. Applied both for Marten and EventStoreDB samples,
  • Aligned convention around Records (use them for DTOs with static factory method for validation),
  • Introduced StreamEvent for EventStoreDB subscriptions to gather information about stream revision and global position. That can be used for idempotency checks in projections.
  • Made MartenExternalProjection, EntityFrameworkProjection idempotent,
  • Merged EventStoreDB improvements from Simple EventStoreDB examples into Core project,
  • Added NoMediatorEventBus to not rely on the marker interfaces need etc.
  • Aligned ECommerce samples:
    • API structure and models
    • The same set of integration tests,
    • Naming and structure conventions
  • Added CorrelationIdMiddleware and plugged it initially. More changes will come in the follow-up PR,
  • Unified background processing for ESDB Subscriptions and Kafka Consumers

See more in: https://github.com/oskardudycz/EventSourcing.NetCore/pull/100.

EventSourcing.NetCore - Upgraded to Marten v5 and EventStoreDB client 22.0.0

Published by oskardudycz over 2 years ago

Upgraded packages to the latest version

  • Marten v5 alpha,
  • EventStoreDB to 22.0.0
  • and the rest (e.g. MediatR, RestSharp, etc.)

Upgraded also ESDB docker images to the latest LTS version.

EventSourcing.NetCore - Strongly-Typed ids with Marten

Published by oskardudycz over 2 years ago

Strongly typed ids (or, in general, a proper type system) can make your code more predictable. It reduces the chance of trivial mistakes, like accidentally changing parameters order of the same primitive type.

So for such code:

var reservationId = "RES/01";
var seatId = "SEAT/22";
var customerId = "CUS/291";

var reservation = new ReservationId (
    reservationId,
    seatId,
    customerId 
);

the compiler won't catch if you switch reservationId with seatId.

If you use strongly typed ids, then compile will catch that issue:

var reservationId = new ReservationId("RES/01");
var seatId = new SeatId("SEAT/22");
var customerId = new CustomerId("CUS/291");

var reservation = new ReservationId (
    reservationId,
    seatId,
    customerId 
);

They're not ideal, as they're usually not playing well with the storage engines. Typical issues are: serialisation, Linq queries, etc. For some cases they may be just overkill. You need to pick your poison.

To reduce tedious, copy/paste code, it's worth defining a strongly-typed id base class, like:

public class StronglyTypedValue<T>: IEquatable<StronglyTypedValue<T>> where T: IComparable<T>
{
    public T Value { get; }

    public StronglyTypedValue(T value)
    {
        Value = value;
    }

    public bool Equals(StronglyTypedValue<T>? other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return EqualityComparer<T>.Default.Equals(Value, other.Value);
    }

    public override bool Equals(object? obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != this.GetType()) return false;
        return Equals((StronglyTypedValue<T>)obj);
    }

    public override int GetHashCode()
    {
        return EqualityComparer<T>.Default.GetHashCode(Value);
    }

    public static bool operator ==(StronglyTypedValue<T>? left, StronglyTypedValue<T>? right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(StronglyTypedValue<T>? left, StronglyTypedValue<T>? right)
    {
        return !Equals(left, right);
    }
}

Then you can define specific id class as:

public class ReservationId: StronglyTypedValue<Guid>
{
    public ReservationId(Guid value) : base(value)
    {
    }
}

You can even add additional rules:

public class ReservationNumber: StronglyTypedValue<string>
{
    public ReservationNumber(string value) : base(value)
    {
        if (string.IsNullOrEmpty(value) || value.StartsWith("RES/") || value.Length <= 4)
            throw new ArgumentOutOfRangeException(nameof(value));
    }
}

The base class working with Marten, can be defined as:

public abstract class Aggregate<TKey, T>
    where TKey: StronglyTypedValue<T>
    where T : IComparable<T>
{
    public TKey Id { get; set; } = default!;
    
    [Identity]
    public T AggregateId    {
        get => Id.Value;
        set {} 
    }

    public int Version { get; protected set; }

    [JsonIgnore] private readonly Queue<object> uncommittedEvents = new(); 

    public object[] DequeueUncommittedEvents()
    {
        var dequeuedEvents = uncommittedEvents.ToArray();

        uncommittedEvents.Clear();

        return dequeuedEvents;
    }

    protected void Enqueue(object @event)
    {
        uncommittedEvents.Enqueue(@event);
    }
}

Marten requires the id with public setter and getter of string or Guid. We used the trick and added AggregateId with a strongly-typed backing field. We also informed Marten of the Identity attribute to use this field in its internals.

Example aggregate can look like:

public class Reservation : Aggregate<ReservationId, Guid>
{
    public CustomerId CustomerId { get; private set; } = default!;

    public SeatId SeatId { get; private set; } = default!;

    public ReservationNumber Number { get; private set; } = default!;

    public ReservationStatus Status { get; private set; }

    public static Reservation CreateTentative(
        SeatId seatId,
        CustomerId customerId)
    {
        return new Reservation(
            new ReservationId(Guid.NewGuid()),
            seatId,
            customerId,
            new ReservationNumber(Guid.NewGuid().ToString())
        );
    }

    // (...)
}

See the full sample here.

Read more in the article:

EventSourcing.NetCore - Added Event Schema Versioning samples

Published by oskardudycz almost 3 years ago

Shows how to handle basic event schema versioning scenarios using event and stream transformations (e.g. upcasting):

  • Simple mapping
    • New not required property
    • New required property
    • Renamed property
  • Upcasting
    • Changed Structure
    • New required property
  • Downcasters
  • Events Transformations
  • Stream Transformation
  • Summary

See the whole sample at: https://github.com/oskardudycz/EventSourcing.NetCore/tree/main/Sample/EventsVersioning
And details of changes in: https://github.com/oskardudycz/EventSourcing.NetCore/pull/75

EventSourcing.NetCore - Fixed serialisation issue in samples using Newtonsoft Json.NET

Published by oskardudycz almost 3 years ago

It seems that during the .NET upgrade, I broke Newtonsoft Json.NET serialisation. I removed the usage of JsonConstructor and NuGet that extended Json.NET with private constructors. Unfortunately, the test suite was not strong enough. That was strengthened up in this release.

I added ContractResolver which enables private non-default constructors.

Details

  • Added JsonObjectContractProvider that resolves Json object constructor, to support non-default, private constructor by default.
  • Moved JSONserialisationn to dedicated project to be able to not have a dependency on Core in the Test project,
  • Added default JsonSerializerSettings and used in Newtonsoft related projects,
  • Added NonDefaultConstructorContractResolver and NonDefaultConstructorMartenJsonNetContractResolver that use JsonObjectContractProvider internally
  • Added missing tests for Cart Confirmation and Adding Product in Marten ECommerce Sample.
  • Updated Requests classes to records in Marten ECommerce Sample.

Note: JsonObjectContractProvider will be added to Marten in the follow-up PR.

EventSourcing.NetCore - Event Pipelines samples

Published by oskardudycz almost 3 years ago

Event Pipelines

Shows how to compose event handlers in the processing pipelines to:

  • filter events,
  • transform them,
  • NOT requiring marker interfaces for events,
  • NOT requiring marker interfaces for handlers,
  • enables composition through regular functions,
  • allows using interfaces and classes if you want to,
  • can be used with Dependency Injection, but also without through builder,
  • integrates with MediatR if you want to.

Overview

Having UserAdded event:

public record UserAdded(
    string FirstName,
    string LastName,
    bool IsAdmin
);

We may want to create a pipeline, that will at first filter admin users:

public static bool IsAdmin(UserAdded @event) =>
    @event.IsAdmin;

Then map events to a dedicated AdminAdded event:

public record AdminAdded(
    string FirstName,
    string LastName
);

public static AdminAdded ToAdminAdded(UserAdded @event) =>
    new(@event.FirstName, @event.LastName);

Then handle mapped events storing information about new admins:

public static void Handle(AdminAdded @event) =>
    GlobalAdmins.Add(@event);

And distribute global admins to all tenants:

public static List<AdminGrantedInTenant> SendToTenants(UserAdded @event) =>
    TenantNames
        .Select(tenantName =>
            new AdminGrantedInTenant(@event.FirstName, @event.LastName, tenantName)
        )
        .ToList();

public record AdminGrantedInTenant(
    string FirstName,
    string LastName,
    string TenantName
);

public static void Handle(AdminGrantedInTenant @event) =>
    AdminsInTenants.Add(@event);
}

MediatR is great, but it doesn't enable such advanced pipelines. This sample shows how to construct event pipelines seamlessly. See EventBus implementation.

You can use it with Dependency Injection

serviceCollection
    .AddEventBus()
    .Filter<UserAdded>(AdminPipeline.IsAdmin)
    .Transform<UserAdded, AdminAdded>(AdminPipeline.ToAdminAdded)
    .Handle<AdminAdded>(AdminPipeline.Handle)
    .Transform<UserAdded, List<AdminGrantedInTenant>>(AdminPipeline.SendToTenants)
    .Handle<AdminGrantedInTenant>(AdminPipeline.Handle);

or without:

var builder = EventHandlersBuilder
    .Setup()
    .Filter<UserAdded>(AdminPipeline.IsAdmin)
    .Transform<UserAdded, AdminAdded>(AdminPipeline.ToAdminAdded)
    .Handle<AdminAdded>(AdminPipeline.Handle)
    .Transform<UserAdded, List<AdminGrantedInTenant>>(AdminPipeline.SendToTenants)
    .Handle<AdminGrantedInTenant>(AdminPipeline.Handle);

var eventBus = new EventBus(builder);

Samples

Check different ways of defining and integrating Event Handlers:

And how to integrate with MediatR:

EventSourcing.NetCore - Initial .NET 6 upgrade

Published by oskardudycz almost 3 years ago

  • Updated projects to .NET 6,
  • Updated packages to the latest versions,
  • Migrated to file-scoped namespaces.

More to come in the follow-up pull-request(s).

See details in: https://github.com/oskardudycz/EventSourcing.NetCore/pull/82

EventSourcing.NetCore - Updated Simple Event Sourcing with EventStoreDB sample

Published by oskardudycz almost 3 years ago

  1. Added Default object for ShoppingCart to get rid of nullability and forced not nulls with an exclamation mark,
  2. Replaced EventStoreDBRepository with extension methods, as there is no expectation of having other implementation.
  3. Added CommandHandlersBuilder to simplify the command handlers registration.
  4. Removed underscores from stream category name to not be misleading with dashes.
  5. Added Closed shopping cart status as bit flag of Confirmed and Cancelled to simplify the check in the business logic.
  6. Removed redundant Shopping Cart status in the ShoppingCartInitialized event (it was always the same).

Thanks, @bartelink for the feedback and suggestions!

EventSourcing.NetCore - Upgraded Marten to v4 and all other packages

Published by oskardudycz about 3 years ago

EventSourcing.NetCore - Simple, practical EventSourcing with EventStoreDB and EntityFramework

Published by oskardudycz about 3 years ago

Simple, practical EventSourcing with EventStoreDB and EntityFramework

The PR is adding a new sample that contains the simple Event Sourcing setup with EventStoreDB. For the Read Model, Postgres and Entity Framework are used.

You can watch the webinar on YouTube where I'm explaining the details of the implementation:

or read the article explaining the read model part: "How to build event-driven projections with Entity Framework"

Main assumptions:

  • explain basics of Event Sourcing, both from the write model (EventStoreDB) and read model part (Postgres and EntityFramework),
  • CQRS architecture sliced by business features, keeping code that changes together at the same place. Read more in How to slice the codebase effectively?
  • no aggregates, just data (records) and functions,
  • clean, composable (pure) functions for command, events, projections, query handling instead of marker interfaces (the only one used internally is IEventHandler). Thanks to that testability and easier maintenance.
  • easy to use and self-explanatory fluent API for registering commands and projections with possible fallbacks,
  • registering everything into regular DI containers to integrate with other application services.
  • pushing the type/signature enforcement on edge, so when plugging to DI.

Overview

It uses:

  • pure data entities, functions and handlers,
  • Stores events from the command handler result EventStoreDB,
  • Builds read models using Subscription to $all.
  • Read models are stored to Postgres relational tables with Entity Framework.
  • App has Swagger and predefined docker-compose to run and play with samples.

Write Model

Read Model

Tests

API integration tests for:

Prerequisities

  1. Install git - https://git-scm.com/downloads.
  2. Install .NET Core 5.0 - https://dotnet.microsoft.com/download/dotnet/5.0.
  3. Install Visual Studio 2019, Rider or VSCode.
  4. Install docker - https://docs.docker.com/docker-for-windows/install/.
  5. Open ECommerce.sln solution.

Running

  1. Go to docker and run: docker-compose up.
  2. Wait until all dockers got are downloaded and running.
  3. You should automatically get:
  4. Open, build and run ECommerce.sln solution.
EventSourcing.NetCore - Configure API acceptance tests and test results reporting in GitHub Actions

Published by oskardudycz about 3 years ago

  1. Added GitHub action test logger.
  2. Configured acceptance tests with docker running behind it.
  3. Refactored docker-compose configuration.
  4. Updated acceptance tests to be stable in the pipeline.

Besides that applied small refactorings and upgraded Marten to rc3.

See details in: https://github.com/oskardudycz/EventSourcing.NetCore/pull/57.

EventSourcing.NetCore - Added short descriptions and links to all samples in README

Published by oskardudycz over 3 years ago

Restructured README and added short descriptions and links to all samples.

EventSourcing.NetCore - Removed Bank Accounts samples

Published by oskardudycz over 3 years ago

Removed Bank Accounts example as it's far from:

  • the real-world handling in the banking domain (it's oversimplified),
  • it's lacking the proper description, so some of the assumptions used (like not using repositories, etc. to show the simplest flow) may be misleading and target to the wrong direction,
  • showing that you can if you want to store more than one aggregate is okay if it's explained. Without explanation, it may be wrongly interpreted that's the right approach.
  • there is already another, better ECommerce sample that's showing how to integrate EventSourcing with the traditional approach.
EventSourcing.NetCore - Refactored domain structure into slices (feature folders)

Published by oskardudycz over 3 years ago

Currently, samples modules were split by aggregate, but then into technical split like Events, Commands etc.
Inspired by my latest samples of simple CQRS I decided to split also other samples by feature folders.

Now Domain is split by the business operations (command and queries). Inside the folders are both contracts and handlers.

Sample structure:

obraz

Commands

Command folders contain:

  • file with command and handler, e.g. Carts/AddingProduct/AddProduct.cs
using System;
using System.Threading;
using System.Threading.Tasks;
using Ardalis.GuardClauses;
using Carts.Carts.Products;
using Carts.Pricing;
using Core.Commands;
using Core.Repositories;
using MediatR;

namespace Carts.Carts.AddingProduct
{
    public class AddProduct: ICommand
    {

        public Guid CartId { get; }

        public ProductItem ProductItem { get; }

        private AddProduct(Guid cartId, ProductItem productItem)
        {
            CartId = cartId;
            ProductItem = productItem;
        }
        public static AddProduct Create(Guid cartId, ProductItem productItem)
        {
            Guard.Against.Default(cartId, nameof(cartId));
            Guard.Against.Null(productItem, nameof(productItem));

            return new AddProduct(cartId, productItem);
        }
    }

    internal class HandleAddProduct:
        ICommandHandler<AddProduct>
    {
        private readonly IRepository<Cart> cartRepository;
        private readonly IProductPriceCalculator productPriceCalculator;

        public HandleAddProduct(
            IRepository<Cart> cartRepository,
            IProductPriceCalculator productPriceCalculator
        )
        {
            this.cartRepository = cartRepository;
            this.productPriceCalculator = productPriceCalculator;
        }

        public Task<Unit> Handle(AddProduct command, CancellationToken cancellationToken)
        {
            return cartRepository.GetAndUpdate(
                command.CartId,
                cart => cart.AddProduct(productPriceCalculator, command.ProductItem),
                cancellationToken);
        }
    }
}
  • file with an event command is creation, e.g. Carts/AddingProduct/ProductAdded.cs
using System;
using Ardalis.GuardClauses;
using Carts.Carts.Products;
using Core.Events;

namespace Carts.Carts.AddingProduct
{
    public class ProductAdded: IEvent
    {
        public Guid CartId { get; }

        public PricedProductItem ProductItem { get; }

        private ProductAdded(Guid cartId, PricedProductItem productItem)
        {
            CartId = cartId;
            ProductItem = productItem;
        }

        public static ProductAdded Create(Guid cartId, PricedProductItem productItem)
        {
            Guard.Against.Default(cartId, nameof(cartId));
            Guard.Against.Null(productItem, nameof(productItem));

            return new ProductAdded(cartId, productItem);
        }
    }
}

Queries

Query folders contain:

  • query with handler, e.g. Carts/GettingCartById/GetCartById.cs
using System;
using System.Threading;
using System.Threading.Tasks;
using Ardalis.GuardClauses;
using Core.Queries;
using Marten;

namespace Carts.Carts.GettingCartById
{
    public class GetCartById : IQuery<CartDetails>
    {
        public Guid CartId { get; }

        private GetCartById(Guid cartId)
        {
            CartId = cartId;
        }

        public static GetCartById Create(Guid cartId)
        {
            Guard.Against.Default(cartId, nameof(cartId));

            return new GetCartById(cartId);
        }
    }

    internal class HandleGetCartById :
        IQueryHandler<GetCartById, CartDetails?>
    {
        private readonly IDocumentSession querySession;

        public HandleGetCartById(IDocumentSession querySession)
        {
            this.querySession = querySession;
        }

        public Task<CartDetails?> Handle(GetCartById request, CancellationToken cancellationToken)
        {
            return querySession.LoadAsync<CartDetails>(request.CartId, cancellationToken);
        }
    }
}
  • read model with projection, e.g. Carts/GettingCartById/CartDetails.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Carts.Carts.AddingProduct;
using Carts.Carts.ConfirmingCart;
using Carts.Carts.InitializingCart;
using Carts.Carts.Products;
using Carts.Carts.RemovingProduct;
using Core.Extensions;
using Marten.Events.Aggregation;

namespace Carts.Carts.GettingCartById
{
    public class CartDetails
    {
        public Guid Id { get; set; }
        public Guid ClientId { get; set; }

        public CartStatus Status { get; set; }

        public IList<PricedProductItem> ProductItems { get; set; } = default!;

        public decimal TotalPrice => ProductItems.Sum(pi => pi.TotalPrice);

        public int Version { get; set; }

        public void Apply(CartInitialized @event)
        {
            Version++;

            Id = @event.CartId;
            ClientId = @event.ClientId;
            ProductItems = new List<PricedProductItem>();
            Status = @event.CartStatus;
        }

        public void Apply(ProductAdded @event)
        {
            Version++;

            var newProductItem = @event.ProductItem;

            var existingProductItem = FindProductItemMatchingWith(newProductItem);

            if (existingProductItem is null)
            {
                ProductItems.Add(newProductItem);
                return;
            }

            ProductItems.Replace(
                existingProductItem,
                existingProductItem.MergeWith(newProductItem)
            );
        }

        public void Apply(ProductRemoved @event)
        {
            Version++;

            var productItemToBeRemoved = @event.ProductItem;

            var existingProductItem = FindProductItemMatchingWith(@event.ProductItem);

            if(existingProductItem == null)
                return;

            if (existingProductItem.HasTheSameQuantity(productItemToBeRemoved))
            {
                ProductItems.Remove(existingProductItem);
                return;
            }

            ProductItems.Replace(
                existingProductItem,
                existingProductItem.Substract(productItemToBeRemoved)
            );
        }

        public void Apply(CartConfirmed @event)
        {
            Version++;

            Status = CartStatus.Confirmed;
        }

        private PricedProductItem? FindProductItemMatchingWith(PricedProductItem productItem)
        {
            return ProductItems
                .SingleOrDefault(pi => pi.MatchesProductAndPrice(productItem));
        }
    }

    public class CartDetailsProjection : AggregateProjection<CartDetails>
    {
        public CartDetailsProjection()
        {
            ProjectEvent<CartInitialized>((item, @event) => item.Apply(@event));

            ProjectEvent<ProductAdded>((item, @event) => item.Apply(@event));

            ProjectEvent<ProductRemoved>((item, @event) => item.Apply(@event));

            ProjectEvent<CartConfirmed>((item, @event) => item.Apply(@event));
        }
    }
}