Skip to content

Commit

Permalink
Exercise 05 - Getting the current entity state from events using Marten
Browse files Browse the repository at this point in the history
  • Loading branch information
oskardudycz committed Mar 13, 2022
1 parent b2e3faa commit 00c974b
Show file tree
Hide file tree
Showing 20 changed files with 802 additions and 31 deletions.
23 changes: 20 additions & 3 deletions EventSourcing.NetCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IntroductionToEventSourcing
Workshops\IntroductionToEventSourcing\README.md = Workshops\IntroductionToEventSourcing\README.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{FCB69316-5DF2-46B7-B17F-A9112C83D4E3}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solved", "Solved", "{65F6E2BE-B2D4-4E56-B0CB-3062C4882B9E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "01-EventsDefinition", "Workshops\IntroductionToEventSourcing\01-EventsDefinition\01-EventsDefinition.csproj", "{DC57AF14-B7E7-4029-98B7-B598E1EE11A9}"
Expand All @@ -280,6 +278,15 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "04-AppendingEvents.EventSto
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "04-AppendingEvents.EventStoreDB", "Workshops\IntroductionToEventSourcing\Solved\04-AppendingEvents.EventStoreDB\04-AppendingEvents.EventStoreDB.csproj", "{00EEBD09-6194-44EA-9040-CA06D95F41C8}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{A7061674-C351-4FB1-BEB1-595C79657CDB}"
ProjectSection(SolutionItems) = preProject
Workshops\IntroductionToEventSourcing\docker-compose.yml = Workshops\IntroductionToEventSourcing\docker-compose.yml
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "05-GettingStateFromEvents.Marten", "Workshops\IntroductionToEventSourcing\Solved\05-GettingStateFromEvents.Marten\05-GettingStateFromEvents.Marten.csproj", "{84B63522-B331-4ECC-9D5B-CDCF4A48181D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "05-GettingStateFromEvents.Marten", "Workshops\IntroductionToEventSourcing\05-GettingStateFromEvents.Marten\05-GettingStateFromEvents.Marten.csproj", "{A0E61C5B-F1BB-462D-B83D-1FA00F7B8B97}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -614,6 +621,14 @@ Global
{00EEBD09-6194-44EA-9040-CA06D95F41C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{00EEBD09-6194-44EA-9040-CA06D95F41C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{00EEBD09-6194-44EA-9040-CA06D95F41C8}.Release|Any CPU.Build.0 = Release|Any CPU
{84B63522-B331-4ECC-9D5B-CDCF4A48181D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{84B63522-B331-4ECC-9D5B-CDCF4A48181D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{84B63522-B331-4ECC-9D5B-CDCF4A48181D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{84B63522-B331-4ECC-9D5B-CDCF4A48181D}.Release|Any CPU.Build.0 = Release|Any CPU
{A0E61C5B-F1BB-462D-B83D-1FA00F7B8B97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A0E61C5B-F1BB-462D-B83D-1FA00F7B8B97}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A0E61C5B-F1BB-462D-B83D-1FA00F7B8B97}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A0E61C5B-F1BB-462D-B83D-1FA00F7B8B97}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -714,7 +729,6 @@ Global
{C6383AC1-2D24-4E7A-810F-642920C857EA} = {107B18E3-65C8-4A6C-976D-7EEED6A6D318}
{25D0A470-4A59-49F3-9148-942F88E401B1} = {58104871-E205-45F1-9E24-BED4967E602B}
{14C7B928-9D6C-441A-8A1F-0C49173E73EB} = {CEEE5F74-121E-437F-B3B4-4E7C65482644}
{FCB69316-5DF2-46B7-B17F-A9112C83D4E3} = {14C7B928-9D6C-441A-8A1F-0C49173E73EB}
{65F6E2BE-B2D4-4E56-B0CB-3062C4882B9E} = {14C7B928-9D6C-441A-8A1F-0C49173E73EB}
{DC57AF14-B7E7-4029-98B7-B598E1EE11A9} = {14C7B928-9D6C-441A-8A1F-0C49173E73EB}
{E89E1405-33F0-4A12-BE7D-6982F23A8EA2} = {65F6E2BE-B2D4-4E56-B0CB-3062C4882B9E}
Expand All @@ -724,6 +738,9 @@ Global
{CA18779A-8070-4E8D-BB3A-0BA5A228666E} = {14C7B928-9D6C-441A-8A1F-0C49173E73EB}
{D107F07C-02DD-4DEB-BD9C-2368124E3C95} = {14C7B928-9D6C-441A-8A1F-0C49173E73EB}
{00EEBD09-6194-44EA-9040-CA06D95F41C8} = {65F6E2BE-B2D4-4E56-B0CB-3062C4882B9E}
{A7061674-C351-4FB1-BEB1-595C79657CDB} = {14C7B928-9D6C-441A-8A1F-0C49173E73EB}
{84B63522-B331-4ECC-9D5B-CDCF4A48181D} = {65F6E2BE-B2D4-4E56-B0CB-3062C4882B9E}
{A0E61C5B-F1BB-462D-B83D-1FA00F7B8B97} = {14C7B928-9D6C-441A-8A1F-0C49173E73EB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A5F55604-2FF3-43B7-B657-4F18E6E95D3B}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public record ShoppingCartConfirmed(
DateTime ConfirmedAt
);

public record ShoppingCartCancelled(
public record ShoppingCartCanceled(
Guid ShoppingCartId,
DateTime CanceledAt
);
Expand Down Expand Up @@ -76,7 +76,7 @@ public async Task GettingState_ForSequenceOfEvents_ShouldSucceed()
new ProductItemAddedToShoppingCart(shoppingCartId, tShirt),
new ProductItemRemovedFromShoppingCart(shoppingCartId, pairOfShoes),
new ShoppingCartConfirmed(shoppingCartId, DateTime.UtcNow),
new ShoppingCartCancelled(shoppingCartId, DateTime.UtcNow)
new ShoppingCartCanceled(shoppingCartId, DateTime.UtcNow)
};

var options = new StoreOptions();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public record ShoppingCartConfirmed(
DateTime ConfirmedAt
);

public record ShoppingCartCancelled(
public record ShoppingCartCanceled(
Guid ShoppingCartId,
DateTime CanceledAt
);
Expand Down Expand Up @@ -76,7 +76,7 @@ public async Task GettingState_ForSequenceOfEvents_ShouldSucceed()
new ProductItemAddedToShoppingCart(shoppingCartId, tShirt),
new ProductItemRemovedFromShoppingCart(shoppingCartId, pairOfShoes),
new ShoppingCartConfirmed(shoppingCartId, DateTime.UtcNow),
new ShoppingCartCancelled(shoppingCartId, DateTime.UtcNow)
new ShoppingCartCanceled(shoppingCartId, DateTime.UtcNow)
};

await using var eventStore =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>IntroductionToEventSourcing.GettingStateFromEvents</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.5.1" />
<PackageReference Include="Marten" Version="5.0.0-alpha.6" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using FluentAssertions;
using IntroductionToEventSourcing.GettingStateFromEvents.Tools;
using Marten;
using Xunit;

namespace IntroductionToEventSourcing.GettingStateFromEvents.Immutable;

// EVENTS
public record ShoppingCartOpened(
Guid ShoppingCartId,
Guid ClientId
);

public record ProductItemAddedToShoppingCart(
Guid ShoppingCartId,
PricedProductItem ProductItem
);

public record ProductItemRemovedFromShoppingCart(
Guid ShoppingCartId,
PricedProductItem ProductItem
);

public record ShoppingCartConfirmed(
Guid ShoppingCartId,
DateTime ConfirmedAt
);

public record ShoppingCartCanceled(
Guid ShoppingCartId,
DateTime CanceledAt
);

// VALUE OBJECTS
public record PricedProductItem(
Guid ProductId,
int Quantity,
decimal UnitPrice
);

// ENTITY
public record ShoppingCart(
Guid Id,
Guid ClientId,
ShoppingCartStatus Status,
PricedProductItem[] ProductItems,
DateTime? ConfirmedAt = null,
DateTime? CanceledAt = null
);

public enum ShoppingCartStatus
{
Pending = 1,
Confirmed = 2,
Canceled = 3
}

public class GettingStateFromEventsTests: MartenTest
{
/// <summary>
/// Solution - Mutable entity with When method
/// </summary>
/// <param name="documentSession"></param>
/// <param name="shoppingCartId"></param>
/// <returns></returns>
private static Task<ShoppingCart> GetShoppingCart(IDocumentSession documentSession, Guid shoppingCartId)
{
// 1. Add logic here
throw new NotImplementedException();
}

[Fact]
[Trait("Category", "SkipCI")]
public async Task GettingState_ForSequenceOfEvents_ShouldSucceed()
{
var shoppingCartId = Guid.NewGuid();
var clientId = Guid.NewGuid();
var shoesId = Guid.NewGuid();
var tShirtId = Guid.NewGuid();
var twoPairsOfShoes = new PricedProductItem(shoesId, 2, 100);
var pairOfShoes = new PricedProductItem(shoesId, 1, 100);
var tShirt = new PricedProductItem(tShirtId, 1, 50);

var events = new object[]
{
new ShoppingCartOpened(shoppingCartId, clientId),
new ProductItemAddedToShoppingCart(shoppingCartId, twoPairsOfShoes),
new ProductItemAddedToShoppingCart(shoppingCartId, tShirt),
new ProductItemRemovedFromShoppingCart(shoppingCartId, pairOfShoes),
new ShoppingCartConfirmed(shoppingCartId, DateTime.UtcNow),
new ShoppingCartCanceled(shoppingCartId, DateTime.UtcNow)
};

await AppendEvents(shoppingCartId, events, CancellationToken.None);

var shoppingCart = await GetShoppingCart(DocumentSession, shoppingCartId);

shoppingCart.Id.Should().Be(shoppingCartId);
shoppingCart.ClientId.Should().Be(clientId);
shoppingCart.ProductItems.Should().HaveCount(2);

shoppingCart.ProductItems[0].ProductId.Should().Be(shoesId);
shoppingCart.ProductItems[0].Quantity.Should().Be(pairOfShoes.Quantity);
shoppingCart.ProductItems[0].UnitPrice.Should().Be(pairOfShoes.UnitPrice);

shoppingCart.ProductItems[1].ProductId.Should().Be(tShirtId);
shoppingCart.ProductItems[1].Quantity.Should().Be(tShirt.Quantity);
shoppingCart.ProductItems[1].UnitPrice.Should().Be(tShirt.UnitPrice);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using FluentAssertions;
using IntroductionToEventSourcing.GettingStateFromEvents.Tools;
using Marten;
using Xunit;

namespace IntroductionToEventSourcing.GettingStateFromEvents.Mutable;

// EVENTS
public record ShoppingCartOpened(
Guid ShoppingCartId,
Guid ClientId
);

public record ProductItemAddedToShoppingCart(
Guid ShoppingCartId,
PricedProductItem ProductItem
);

public record ProductItemRemovedFromShoppingCart(
Guid ShoppingCartId,
PricedProductItem ProductItem
);

public record ShoppingCartConfirmed(
Guid ShoppingCartId,
DateTime ConfirmedAt
);

public record ShoppingCartCanceled(
Guid ShoppingCartId,
DateTime CanceledAt
);

// VALUE OBJECTS
public class PricedProductItem
{
public Guid ProductId { get; set; }
public decimal UnitPrice { get; set; }

public int Quantity { get; set; }

public decimal TotalPrice => Quantity * UnitPrice;
}

// ENTITY
public class ShoppingCart
{
public Guid Id { get; private set; }
public Guid ClientId { get; private set; }
public ShoppingCartStatus Status { get; private set; }
public IList<PricedProductItem> ProductItems { get; } = new List<PricedProductItem>();
public DateTime? ConfirmedAt { get; private set; }
public DateTime? CanceledAt { get; private set; }
}

public enum ShoppingCartStatus
{
Pending = 1,
Confirmed = 2,
Canceled = 3
}

public class GettingStateFromEventsTests: MartenTest
{
/// <summary>
/// Solution - Mutable entity with When method
/// </summary>
/// <param name="documentSession"></param>
/// <param name="shoppingCartId"></param>
/// <returns></returns>
private static Task<ShoppingCart> GetShoppingCart(IDocumentSession documentSession, Guid shoppingCartId)
{
// 1. Add logic here
throw new NotImplementedException();
}

[Fact]
[Trait("Category", "SkipCI")]
public async Task GettingState_ForSequenceOfEvents_ShouldSucceed()
{
var shoppingCartId = Guid.NewGuid();
var clientId = Guid.NewGuid();
var shoesId = Guid.NewGuid();
var tShirtId = Guid.NewGuid();
var twoPairsOfShoes =
new PricedProductItem
{
ProductId = shoesId, Quantity = 2, UnitPrice = 100
};
var pairOfShoes =
new PricedProductItem
{
ProductId = shoesId, Quantity = 1, UnitPrice = 100
};
var tShirt =
new PricedProductItem
{
ProductId = tShirtId, Quantity = 1, UnitPrice = 50
};

var events = new object[]
{
new ShoppingCartOpened(shoppingCartId, clientId),
new ProductItemAddedToShoppingCart(shoppingCartId, twoPairsOfShoes),
new ProductItemAddedToShoppingCart(shoppingCartId, tShirt),
new ProductItemRemovedFromShoppingCart(shoppingCartId, pairOfShoes),
new ShoppingCartConfirmed(shoppingCartId, DateTime.UtcNow),
new ShoppingCartCanceled(shoppingCartId, DateTime.UtcNow)
};

await AppendEvents(shoppingCartId, events, CancellationToken.None);

var shoppingCart = await GetShoppingCart(DocumentSession, shoppingCartId);

shoppingCart.Id.Should().Be(shoppingCartId);
shoppingCart.ClientId.Should().Be(clientId);
shoppingCart.ProductItems.Should().HaveCount(2);

shoppingCart.ProductItems[0].ProductId.Should().Be(shoesId);
shoppingCart.ProductItems[0].Quantity.Should().Be(pairOfShoes.Quantity);
shoppingCart.ProductItems[0].UnitPrice.Should().Be(pairOfShoes.UnitPrice);

shoppingCart.ProductItems[1].ProductId.Should().Be(tShirtId);
shoppingCart.ProductItems[1].Quantity.Should().Be(tShirt.Quantity);
shoppingCart.ProductItems[1].UnitPrice.Should().Be(tShirt.UnitPrice);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Exercise 05 - Getting the current entity state from events using Marten

Having a defined structure of events and an entity representing the shopping cart from the [previous exercise](../01-EventsDefinition), fill a `GetShoppingCart` function that will rebuild the current state from events.

If needed you can modify the events or entity structure.

There are two variations:
- using mutable entities: [Mutable/GettingStateFromEventsTests.cs](./Mutable/GettingStateFromEventsTests.cs),
- using fully immutable structures: [Immutable/Solution1/GettingStateFromEventsTests.cs](./Immutable/GettingStateFromEventsTests.cs).

Select your preferred approach (or both) to solve this use case. If needed you can modify entities or events.

## Prerequisites
Run [docker-compose](../docker-compose.yml) script from the main workshop repository to start Postgres instance.

```shell
docker-compose up
```

After that you can use PG admin (IDE for Postgres) to see how tables and data look like. It's available at: http://localhost:5050.
- Login: `admin@pgadmin.org`, Password: `admin`
- To connect to server click right mouse on Servers, then Register Server and use host: `postgres`, user: `postgres`, password: `Password12!`
Loading

0 comments on commit 00c974b

Please sign in to comment.