├── .gitignore
├── ComplexProjections
├── ComplexProjections.csproj
├── Misc.cs
├── Program.cs
├── Properties
│ └── launchSettings.json
├── appsettings.Development.json
└── appsettings.json
├── Core
├── Core.csproj
├── Events.cs
├── Order.cs
└── playground.linq
├── EventSourcing.sln
├── README.md
├── Snapshotter
├── Linqpad
│ ├── listen.linq
│ ├── read.linq
│ └── write.linq
├── Misc.cs
├── Program.cs
├── Properties
│ └── launchSettings.json
├── Snapshotter.csproj
├── appsettings.Development.json
├── appsettings.json
└── setup.sql
└── WhenToSnapshot
├── Misc.cs
├── Program.cs
├── Properties
└── launchSettings.json
├── WhenToSnapshot.csproj
├── appsettings.Development.json
└── appsettings.json
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | bin/
3 | obj/
--------------------------------------------------------------------------------
/ComplexProjections/ComplexProjections.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/ComplexProjections/Misc.cs:
--------------------------------------------------------------------------------
1 | namespace ComplexProjections;
2 |
3 | # region BASE
4 |
5 | public record Event;
6 |
7 | public interface IStore
8 | {
9 | T GetAsync(string id) where T : class;
10 | T ListAsync(string[] ids) where T : class;
11 | List AllAsync() where T : class;
12 | Task Update(T v) where T : class;
13 | }
14 |
15 | # endregion
16 |
17 | #region Hard Read
18 |
19 | public record Posted : Event;
20 |
21 | public record UpdatedAvatar : Event;
22 |
23 | public record Post(string Id, string Content, string UserId);
24 |
25 | public record User(string Id, string Username, string Avatar);
26 |
27 | #endregion
28 |
29 | #region Hard Write
30 |
31 | public record Created : Event;
32 |
33 | public record Viewed : Event;
34 |
35 | public record Commented : Event;
36 |
37 | public record Preview(string Id, string Title, int Views);
38 |
39 | public record Full(string Id, string Title, string Description);
40 |
41 | public record AdminPreview(string Id, string Title, string Description, int Status, int Comments);
42 |
43 | #endregion
--------------------------------------------------------------------------------
/ComplexProjections/Program.cs:
--------------------------------------------------------------------------------
1 | using ComplexProjections;
2 |
3 | var builder = WebApplication.CreateBuilder(args);
4 | var app = builder.Build();
5 |
6 | #region Hard Read
7 |
8 | app.MapGet("/posts", (IStore store) =>
9 | {
10 | var posts = store.AllAsync();
11 | var users = store.ListAsync(posts.Select(x => x.UserId).ToArray());
12 | return new { posts, users };
13 | });
14 |
15 | #endregion
16 |
17 | #region Hard Write
18 |
19 | app.MapPost("/comment", async (string videoId, string comment, IStore store) =>
20 | {
21 | var e = new Commented();
22 | // projections to update when event ...
23 |
24 |
25 | var adminPreview = store.GetAsync(videoId);
26 | adminPreview = adminPreview with { Comments = adminPreview.Comments + 1 };
27 | await store.Update(adminPreview);
28 | });
29 |
30 | #endregion
31 |
32 | app.Run();
--------------------------------------------------------------------------------
/ComplexProjections/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:39331",
7 | "sslPort": 44358
8 | }
9 | },
10 | "profiles": {
11 | "ComplexProjections": {
12 | "commandName": "Project",
13 | "dotnetRunMessages": true,
14 | "launchBrowser": true,
15 | "applicationUrl": "https://localhost:7178;http://localhost:5178",
16 | "environmentVariables": {
17 | "ASPNETCORE_ENVIRONMENT": "Development"
18 | }
19 | },
20 | "IIS Express": {
21 | "commandName": "IISExpress",
22 | "launchBrowser": true,
23 | "environmentVariables": {
24 | "ASPNETCORE_ENVIRONMENT": "Development"
25 | }
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/ComplexProjections/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/ComplexProjections/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "AllowedHosts": "*"
9 | }
10 |
--------------------------------------------------------------------------------
/Core/Core.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/Core/Events.cs:
--------------------------------------------------------------------------------
1 | namespace Core;
2 |
3 | public record Event
4 | {
5 | public string Name => GetType().Name;
6 | }
7 |
8 |
9 | public record AddedCart(string UserId) : Event;
10 | public record AddedItemToCart(int ProductId, int Qty) : Event;
11 | public record RemovedItemFromCart(int ProductId, int Qty) : Event;
12 | public record AddedShippingInformationCart(string Address, string PhoneNumber) : Event;
--------------------------------------------------------------------------------
/Core/Order.cs:
--------------------------------------------------------------------------------
1 | namespace Core;
2 |
3 | public class Order
4 | {
5 | public string UserId { get; set; }
6 | public List Products { get; set; } = new();
7 | public string Address { get; set; }
8 | public string PhoneNumber { get; set; }
9 | }
10 |
11 | public class Product
12 | {
13 | public int Id { get; set; }
14 | }
--------------------------------------------------------------------------------
/Core/playground.linq:
--------------------------------------------------------------------------------
1 |
2 | D:\WS\Rider\EventSourcing\Core\bin\Debug\net6.0\Core.dll
3 | Core
4 |
5 |
6 | void Main()
7 | {
8 | var events = new List {
9 | new AddedCart("userId"),
10 | new AddedItemToCart(1, 2),
11 | new AddedItemToCart(7, 1),
12 | new AddedItemToCart(9, 2),
13 | new AddedShippingInformationCart("42 Road Lane", "12343218764"),
14 | };
15 |
16 | var orderAggregate = (Order order, Event e) =>
17 | {
18 | if (e is AddedCart ac)
19 | {
20 | order.UserId = ac.UserId;
21 | }
22 | else if (e is AddedItemToCart ai)
23 | {
24 | for (int i = 0; i < ai.Qty; i++)
25 | {
26 | order.Products.Add(new Product() { Id = ai.ProductId });
27 | }
28 | }
29 | else if (e is RemovedItemFromCart ri)
30 | {
31 | for (int i = 0; i < ri.Qty; i++)
32 | {
33 | var productToRemove = order.Products.FirstOrDefault(x => x.Id == ri.ProductId);
34 | order.Products.Remove(productToRemove);
35 | }
36 | }
37 | else if (e is AddedShippingInformationCart asi)
38 | {
39 | order.Address = asi.Address;
40 | order.PhoneNumber = asi.PhoneNumber;
41 | }
42 | return order;
43 | };
44 |
45 | var order1 = events.Aggregate(new Order(), orderAggregate);
46 |
47 | order1.Dump();
48 |
49 |
50 | var events2 = new List {
51 | new RemovedItemFromCart(1, 1),
52 | };
53 |
54 | var order2 = events2.Aggregate(order1, orderAggregate);
55 |
56 | order2.Dump();
57 | }
--------------------------------------------------------------------------------
/EventSourcing.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "Core\Core.csproj", "{2C500A8E-1BBA-4C19-8287-B6169247E4E7}"
4 | EndProject
5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WhenToSnapshot", "WhenToSnapshot\WhenToSnapshot.csproj", "{76E6A6DA-441B-4A81-8C02-71B5C17CA223}"
6 | EndProject
7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ComplexProjections", "ComplexProjections\ComplexProjections.csproj", "{FECE9566-14EC-4FFD-887E-338942FCD547}"
8 | EndProject
9 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Snapshotter", "Snapshotter\Snapshotter.csproj", "{52F29BA1-08D4-41DA-9F21-CD198B6625BC}"
10 | EndProject
11 | Global
12 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
13 | Debug|Any CPU = Debug|Any CPU
14 | Release|Any CPU = Release|Any CPU
15 | EndGlobalSection
16 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
17 | {2C500A8E-1BBA-4C19-8287-B6169247E4E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
18 | {2C500A8E-1BBA-4C19-8287-B6169247E4E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
19 | {2C500A8E-1BBA-4C19-8287-B6169247E4E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
20 | {2C500A8E-1BBA-4C19-8287-B6169247E4E7}.Release|Any CPU.Build.0 = Release|Any CPU
21 | {76E6A6DA-441B-4A81-8C02-71B5C17CA223}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
22 | {76E6A6DA-441B-4A81-8C02-71B5C17CA223}.Debug|Any CPU.Build.0 = Debug|Any CPU
23 | {76E6A6DA-441B-4A81-8C02-71B5C17CA223}.Release|Any CPU.ActiveCfg = Release|Any CPU
24 | {76E6A6DA-441B-4A81-8C02-71B5C17CA223}.Release|Any CPU.Build.0 = Release|Any CPU
25 | {FECE9566-14EC-4FFD-887E-338942FCD547}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
26 | {FECE9566-14EC-4FFD-887E-338942FCD547}.Debug|Any CPU.Build.0 = Debug|Any CPU
27 | {FECE9566-14EC-4FFD-887E-338942FCD547}.Release|Any CPU.ActiveCfg = Release|Any CPU
28 | {FECE9566-14EC-4FFD-887E-338942FCD547}.Release|Any CPU.Build.0 = Release|Any CPU
29 | {52F29BA1-08D4-41DA-9F21-CD198B6625BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
30 | {52F29BA1-08D4-41DA-9F21-CD198B6625BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
31 | {52F29BA1-08D4-41DA-9F21-CD198B6625BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
32 | {52F29BA1-08D4-41DA-9F21-CD198B6625BC}.Release|Any CPU.Build.0 = Release|Any CPU
33 | EndGlobalSection
34 | EndGlobal
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Video: https://youtu.be/EGYMNsI_Opo
--------------------------------------------------------------------------------
/Snapshotter/Linqpad/listen.linq:
--------------------------------------------------------------------------------
1 |
2 | D:\WS\Rider\EventSourcing\Snapshotter\bin\Debug\net6.0\Npgsql.dll
3 | D:\WS\Rider\EventSourcing\Snapshotter\bin\Debug\net6.0\Snapshotter.dll
4 | Npgsql
5 | System.Threading.Tasks
6 | System.Text.Json
7 |
8 |
9 | // https://www.npgsql.org/doc/wait.html
10 | // https://www.postgresql.org/docs/9.1/sql-listen.html
11 | async Task Main()
12 | {
13 | await using var conn = new NpgsqlConnection(Connection.ConnectionString);
14 | await conn.OpenAsync();
15 | conn.Notification += async (o, e) =>
16 | {
17 | await HandleEvent(new Guid(e.Payload));
18 | };
19 |
20 | await using (var cmd = new NpgsqlCommand($"LISTEN {Connection.NewEventChannel}", conn))
21 | {
22 | cmd.ExecuteNonQuery();
23 | }
24 |
25 | while (true)
26 | {
27 | await conn.WaitAsync();
28 | "Recieved Event!".Dump("Recieved Event!");
29 | }
30 | }
31 |
32 | public async Task HandleEvent(Guid eventId)
33 | {
34 | await using var conn = new NpgsqlConnection(Connection.ConnectionString);
35 | await conn.OpenAsync();
36 |
37 | await using var cmd = new NpgsqlCommand($"SELECT * FROM events where Id = '{eventId}'", conn);
38 | await using var reader = await cmd.ExecuteReaderAsync();
39 |
40 | await reader.ReadAsync();
41 | var payload = reader.GetString(1);
42 | var type = reader.GetString(2);
43 |
44 | var obj = Payload.Parse(type, payload);
45 | obj.Dump();
46 |
47 | await reader.DisposeAsync();
48 | await cmd.DisposeAsync();
49 |
50 | var userId = "c31e05e7-f966-46a7-9bb8-eb0ca3bcc95a";
51 | await using var cmd2 = new NpgsqlCommand($"SELECT payload FROM projections where Id = '{userId}' and type = 'Cart'", conn);
52 | await using var reader2 = await cmd2.ExecuteReaderAsync();
53 |
54 | var exists = await reader2.ReadAsync();
55 | var cart = exists ? JsonSerializer.Deserialize(reader2.GetString(0)) : new Cart();
56 |
57 | await reader2.DisposeAsync();
58 | await cmd2.DisposeAsync();
59 |
60 | if (obj is AddedCart ac)
61 | {
62 | cart.UserId = ac.UserId;
63 | }
64 | else if (obj is AddedToCart atc)
65 | {
66 | cart.Products.Add(new Product
67 | {
68 | Id = atc.ProductId,
69 | Name = atc.ProductName,
70 | Qty = atc.Qty
71 | });
72 | }
73 | else if (obj is AddedShippingInformationCart si)
74 | {
75 | cart.Address = si.Address;
76 | cart.PhoneNumber = si.PhoneNumber;
77 | }
78 |
79 | var newPayload = JsonSerializer.Serialize