diff --git a/README.md b/README.md index de6d293..482e16c 100644 --- a/README.md +++ b/README.md @@ -190,4 +190,13 @@ giftCard.Debit(50); // ==> balance: 10 giftCard.Debit(20); // ==> invalid operation exception ``` +## How to programmatically initialize event store? + +See [this page](doc/StoreInitialization.md). + +## How to use snapshots to optimize performance? + +See [this page](doc/Snapshots.md). + +--- __Please feel free to download, fork and/or provide any feedback!__ diff --git a/doc/Snapshots.md b/doc/Snapshots.md new file mode 100644 index 0000000..55e9313 --- /dev/null +++ b/doc/Snapshots.md @@ -0,0 +1,94 @@ +# How to use snapshots to optimize performance? + +Performance can become an issue when an aggregate has a large amout of historical events, because all these events must be loaded and compiled to calculate the current state of the aggregate. Snapshot can help resolving this issue. + +The idea is simple: You take a snapshot of the aggregate when necessary and save it in another store (snapshot store). Then to rehydrate the aggregate again you can save IO and computation by only retrieving the snapshot and events that occurred after that snapshot. + +To enable this feature you need to implement the following: + +1. Add the following constructor to the domain aggregate: + ```csharp + public GiftCard( + Guid id, + IAggregateSnapshot snapshot, + IEnumerable> savedEvents) + : base(id, snapshot, savedEvents) + { } + ``` + +1. Create a class that represents the snapshot + ```csharp + public class GiftCardSnapshot : AggregateSnapshot + { + public GiftCardSnapshot(Guid aggregateId, int aggregateVersion, + decimal balance) + : base(aggregateId, aggregateVersion) + { + Balance = balance; + } + + public decimal Balance { get; } + } + ``` + +1. Override the `CreateSnapshot()` method in aggregate + ```csharp + protected override IAggregateSnapshot CreateSnapshot() + { + return new GiftCardSnapshot(Id, Version, Balance); + } + ``` + +1. Override the `ApplySnapshot()` method + ```csharp + protected override void ApplySnapshot(IAggregateSnapshot snapshot) + { + GiftCardSnapshot giftCardSnapshot = snapshot as GiftCardSnapshot + ?? throw new InvalidOperationException(); + + Balance = giftCardSnapshot.Balance; + } + ``` + + Note: + - It is your resposibility to ensure that your snapshot class contains all data needed to restore that aggregate to the corresponding version. + - To use built-in stores, make sure that your snapshot class can be serialized/deserialized by Json.NET + +1. Register snapshot store in `IEventSourcingBuilder`, in a similar way as registering the event stores, e.g., + + ```csharp + services.AddEventSourcing(builder => + builder + .UseTextFileEventStore(x => x.Folder = "C:/Temp/GiftcardEvents") + .UseTextFileSnapshotStore(x => x.Folder = "C:/Temp/GiftcardEvents")); + ``` + + Note: You can store snapshot in the same or in a different storage with events. For example: store events in DynamoDB and store snapshots in text files: + + ```csharp + services.AddEventSourcing(builder => + builder + .UseDynamoDBEventStore(x => x.TableName = "GiftcardEvents") + .UseTextFileSnapshotStore(x => x.Folder = "C:/Temp/GiftcardEvents")); + ``` + +1. Optionally you can programatically initialize snapshot store in a similar way with event store ([see here](StoreInitialization.md)): + ```csharp + public void Configure(IApplicationBuilder app, + IWebHostEnvironment env, + IEventStoreInitializer eventStoreInitializer, + ISnapshotStoreInitializer snapshotStoreInitializer) + { + eventStoreInitializer.EnsureCreatedAsync().Wait(); + snapshotStoreInitializer.EnsureCreatedAsync().Wait(); + + // configure http request pipeline + } + ``` + +1. It's totally up to you to decide when to take an snapshot by calling `aggregate.TakeSnapshot()`. The snapshot will be saved when invoked `AggregateRepository.SaveAggregateAsync()` + +1. By default when calling `AggregateRepository.FindAggregateAsync()` it automatically retrieve the last snapshot. You can force not loading any snapshot by calling: + ```csharp + FindAggregateAsync(aggregateId, useSnapshot: false) + ``` \ No newline at end of file diff --git a/doc/StoreInitialization.md b/doc/StoreInitialization.md new file mode 100644 index 0000000..f709ad5 --- /dev/null +++ b/doc/StoreInitialization.md @@ -0,0 +1,16 @@ +# How to programmatically initialize event store? + +Sometimes it's handy to programmatically initialize event store. Do do this you can resolve `IEventStoreInitializer` from DI service provider and call `EnsureCreatedAsync()`. + +For example to initialize event store when ASP.NET Core application is started: + +```csharp +public void Configure(IApplicationBuilder app, + IWebHostEnvironment env, + IEventStoreInitializer eventStoreInitializer) +{ + eventStoreInitializer.EnsureCreatedAsync().Wait(); + + // configure http request pipelines +} +``` \ No newline at end of file diff --git a/src/JKang.EventSourcing.TestingWebApp/Startup.cs b/src/JKang.EventSourcing.TestingWebApp/Startup.cs index 57a3561..62f9b50 100644 --- a/src/JKang.EventSourcing.TestingWebApp/Startup.cs +++ b/src/JKang.EventSourcing.TestingWebApp/Startup.cs @@ -107,15 +107,16 @@ public void ConfigureServicesForDynamoDB(IServiceCollection services) public void ConfigureServicesForEfCore(IServiceCollection services) { services - .AddDbContext(x => x.UseInMemoryDatabase("eventstore")) + .AddDbContext(x => x.UseInMemoryDatabase("eventstore")); + + services .AddEventSourcing(builder => { builder .UseEfCoreEventStore() .UseEfCoreSnapshotStore() ; - }) - ; + }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.