diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea5ebf9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,205 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.sln.docstates
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+x64/
+bld/
+[Bb]in/
+[Oo]bj/
+
+# Roslyn cache directories
+*.ide/
+
+# AWS
+*.aws-sam/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+#NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+*.cachefile
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding addin-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+## TODO: Comment the next line if you want to checkin your
+## web deploy settings but do note that will include unencrypted
+## passwords
+#*.pubxml
+
+# NuGet Packages Directory
+#packages/*
+## TODO: If the tool you use requires repositories.config
+## uncomment the next line
+#!packages/repositories.config
+
+# Enable "build/" folder in the NuGet Packages folder since
+# NuGet packages use it for MSBuild targets.
+# This line needs to be after the ignore of the build folder
+# (and the packages folder if the line above has been uncommented)
+#!packages/build/
+
+!src/packages/**/*.dll
+!src/packages/**/*.pdb
+
+# Windows Azure Build Output
+csx/
+*.build.csdef
+
+# Windows Store app package directory
+AppPackages/
+
+# Others
+*.Cache
+ClientBin/
+[Ss]tyle[Cc]op.*
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.pfx
+*.publishsettings
+node_modules/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# LightSwitch generated files
+GeneratedArtifacts/
+_Pvt_Extensions/
+ModelManifest.xml
+src/packages/EventStore.Client.3.3.1/lib/net40/EventStore.ClientAPI.xml
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# Vagrant VM files
+.vagrant
+vagrant/dbv/data/meta/revision
+
+# Visual Studio 2015 cache/options directory
+.vs/
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea5ebf9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,205 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.sln.docstates
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+x64/
+bld/
+[Bb]in/
+[Oo]bj/
+
+# Roslyn cache directories
+*.ide/
+
+# AWS
+*.aws-sam/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+#NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+*.cachefile
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding addin-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+## TODO: Comment the next line if you want to checkin your
+## web deploy settings but do note that will include unencrypted
+## passwords
+#*.pubxml
+
+# NuGet Packages Directory
+#packages/*
+## TODO: If the tool you use requires repositories.config
+## uncomment the next line
+#!packages/repositories.config
+
+# Enable "build/" folder in the NuGet Packages folder since
+# NuGet packages use it for MSBuild targets.
+# This line needs to be after the ignore of the build folder
+# (and the packages folder if the line above has been uncommented)
+#!packages/build/
+
+!src/packages/**/*.dll
+!src/packages/**/*.pdb
+
+# Windows Azure Build Output
+csx/
+*.build.csdef
+
+# Windows Store app package directory
+AppPackages/
+
+# Others
+*.Cache
+ClientBin/
+[Ss]tyle[Cc]op.*
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.pfx
+*.publishsettings
+node_modules/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# LightSwitch generated files
+GeneratedArtifacts/
+_Pvt_Extensions/
+ModelManifest.xml
+src/packages/EventStore.Client.3.3.1/lib/net40/EventStore.ClientAPI.xml
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# Vagrant VM files
+.vagrant
+vagrant/dbv/data/meta/revision
+
+# Visual Studio 2015 cache/options directory
+.vs/
\ No newline at end of file
diff --git a/EventSourcingSnapshots.csproj b/EventSourcingSnapshots.csproj
new file mode 100644
index 0000000..2a9a875
--- /dev/null
+++ b/EventSourcingSnapshots.csproj
@@ -0,0 +1,13 @@
+
+
+
+ Exe
+ net5.0
+
+
+
+
+
+
+
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea5ebf9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,205 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.sln.docstates
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+x64/
+bld/
+[Bb]in/
+[Oo]bj/
+
+# Roslyn cache directories
+*.ide/
+
+# AWS
+*.aws-sam/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+#NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+*.cachefile
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding addin-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+## TODO: Comment the next line if you want to checkin your
+## web deploy settings but do note that will include unencrypted
+## passwords
+#*.pubxml
+
+# NuGet Packages Directory
+#packages/*
+## TODO: If the tool you use requires repositories.config
+## uncomment the next line
+#!packages/repositories.config
+
+# Enable "build/" folder in the NuGet Packages folder since
+# NuGet packages use it for MSBuild targets.
+# This line needs to be after the ignore of the build folder
+# (and the packages folder if the line above has been uncommented)
+#!packages/build/
+
+!src/packages/**/*.dll
+!src/packages/**/*.pdb
+
+# Windows Azure Build Output
+csx/
+*.build.csdef
+
+# Windows Store app package directory
+AppPackages/
+
+# Others
+*.Cache
+ClientBin/
+[Ss]tyle[Cc]op.*
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.pfx
+*.publishsettings
+node_modules/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# LightSwitch generated files
+GeneratedArtifacts/
+_Pvt_Extensions/
+ModelManifest.xml
+src/packages/EventStore.Client.3.3.1/lib/net40/EventStore.ClientAPI.xml
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# Vagrant VM files
+.vagrant
+vagrant/dbv/data/meta/revision
+
+# Visual Studio 2015 cache/options directory
+.vs/
\ No newline at end of file
diff --git a/EventSourcingSnapshots.csproj b/EventSourcingSnapshots.csproj
new file mode 100644
index 0000000..2a9a875
--- /dev/null
+++ b/EventSourcingSnapshots.csproj
@@ -0,0 +1,13 @@
+
+
+
+ Exe
+ net5.0
+
+
+
+
+
+
+
+
diff --git a/Events.cs b/Events.cs
new file mode 100644
index 0000000..018c062
--- /dev/null
+++ b/Events.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace EventSourcing.Demo
+{
+ public interface IEvent
+ {
+ string EventType { get; }
+ }
+
+ public record ProductShipped(string Sku, int Quantity, DateTime DateTime) : IEvent
+ {
+ public string EventType { get; } = "ProductShipped";
+ }
+
+ public record ProductReceived(string Sku, int Quantity, DateTime DateTime) : IEvent
+ {
+ public string EventType { get; } = "ProductReceived";
+ }
+
+ public record InventoryAdjusted(string Sku, int Quantity, string Reason, DateTime DateTime) : IEvent
+ {
+ public string EventType { get; } = "InventoryAdjusted";
+ }
+}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea5ebf9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,205 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.sln.docstates
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+x64/
+bld/
+[Bb]in/
+[Oo]bj/
+
+# Roslyn cache directories
+*.ide/
+
+# AWS
+*.aws-sam/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+#NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+*.cachefile
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding addin-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+## TODO: Comment the next line if you want to checkin your
+## web deploy settings but do note that will include unencrypted
+## passwords
+#*.pubxml
+
+# NuGet Packages Directory
+#packages/*
+## TODO: If the tool you use requires repositories.config
+## uncomment the next line
+#!packages/repositories.config
+
+# Enable "build/" folder in the NuGet Packages folder since
+# NuGet packages use it for MSBuild targets.
+# This line needs to be after the ignore of the build folder
+# (and the packages folder if the line above has been uncommented)
+#!packages/build/
+
+!src/packages/**/*.dll
+!src/packages/**/*.pdb
+
+# Windows Azure Build Output
+csx/
+*.build.csdef
+
+# Windows Store app package directory
+AppPackages/
+
+# Others
+*.Cache
+ClientBin/
+[Ss]tyle[Cc]op.*
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.pfx
+*.publishsettings
+node_modules/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# LightSwitch generated files
+GeneratedArtifacts/
+_Pvt_Extensions/
+ModelManifest.xml
+src/packages/EventStore.Client.3.3.1/lib/net40/EventStore.ClientAPI.xml
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# Vagrant VM files
+.vagrant
+vagrant/dbv/data/meta/revision
+
+# Visual Studio 2015 cache/options directory
+.vs/
\ No newline at end of file
diff --git a/EventSourcingSnapshots.csproj b/EventSourcingSnapshots.csproj
new file mode 100644
index 0000000..2a9a875
--- /dev/null
+++ b/EventSourcingSnapshots.csproj
@@ -0,0 +1,13 @@
+
+
+
+ Exe
+ net5.0
+
+
+
+
+
+
+
+
diff --git a/Events.cs b/Events.cs
new file mode 100644
index 0000000..018c062
--- /dev/null
+++ b/Events.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace EventSourcing.Demo
+{
+ public interface IEvent
+ {
+ string EventType { get; }
+ }
+
+ public record ProductShipped(string Sku, int Quantity, DateTime DateTime) : IEvent
+ {
+ public string EventType { get; } = "ProductShipped";
+ }
+
+ public record ProductReceived(string Sku, int Quantity, DateTime DateTime) : IEvent
+ {
+ public string EventType { get; } = "ProductReceived";
+ }
+
+ public record InventoryAdjusted(string Sku, int Quantity, string Reason, DateTime DateTime) : IEvent
+ {
+ public string EventType { get; } = "InventoryAdjusted";
+ }
+}
\ No newline at end of file
diff --git a/Program.cs b/Program.cs
new file mode 100644
index 0000000..d641d35
--- /dev/null
+++ b/Program.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Threading.Tasks;
+
+namespace EventSourcing.Demo
+{
+ class Program
+ {
+ static async Task Main()
+ {
+ var warehouseProductRepository = await WarehouseProductEventStoreStream.Factory();
+
+ var key = string.Empty;
+ while (key != "X")
+ {
+ Console.WriteLine("R: Receive Inventory");
+ Console.WriteLine("S: Ship Inventory");
+ Console.WriteLine("A: Inventory Adjustment");
+ Console.WriteLine("Q: Quantity On Hand");
+ Console.WriteLine("E: Events since Snapshot");
+ Console.Write("> ");
+ key = Console.ReadLine()?.ToUpperInvariant();
+ Console.WriteLine();
+
+ var sku = GetSkuFromConsole();
+ var warehouseProduct = await warehouseProductRepository.Get(sku);
+
+ switch (key)
+ {
+ case "R":
+ var receiveInput = GetQuantity();
+ if (receiveInput.IsValid)
+ {
+ warehouseProduct.ReceiveProduct(receiveInput.Quantity);
+ Console.WriteLine($"{sku} Received: {receiveInput.Quantity}");
+ }
+ break;
+ case "S":
+ var shipInput = GetQuantity();
+ if (shipInput.IsValid)
+ {
+ warehouseProduct.ShipProduct(shipInput.Quantity);
+ Console.WriteLine($"{sku} Shipped: {shipInput.Quantity}");
+ }
+ break;
+ case "A":
+ var adjustmentInput = GetQuantity();
+ if (adjustmentInput.IsValid)
+ {
+ var reason = GetAdjustmentReason();
+ warehouseProduct.AdjustInventory(adjustmentInput.Quantity, reason);
+ Console.WriteLine($"{sku} Adjusted: {adjustmentInput.Quantity} {reason}");
+ }
+ break;
+ case "Q":
+ var currentQuantityOnHand = warehouseProduct.GetQuantityOnHand();
+ Console.WriteLine($"{sku} Quantity On Hand: {currentQuantityOnHand}");
+ break;
+ case "E":
+ Console.WriteLine($"Events: {sku}");
+ foreach (var evnt in warehouseProduct.GetAllEvents())
+ {
+ switch (evnt)
+ {
+ case ProductShipped shipProduct:
+ Console.WriteLine($"{shipProduct.DateTime:u} {sku} Shipped: {shipProduct.Quantity}");
+ break;
+ case ProductReceived receiveProduct:
+ Console.WriteLine($"{receiveProduct.DateTime:u} {sku} Received: {receiveProduct.Quantity}");
+ break;
+ case InventoryAdjusted inventoryAdjusted:
+ Console.WriteLine($"{inventoryAdjusted.DateTime:u} {sku} Adjusted: {inventoryAdjusted.Quantity} {inventoryAdjusted.Reason}");
+ break;
+ }
+
+ }
+ break;
+ }
+
+ await warehouseProductRepository.Save(warehouseProduct);
+
+ Console.ReadLine();
+ Console.WriteLine();
+ }
+ }
+
+
+ private static string GetSkuFromConsole()
+ {
+ Console.Write("SKU: ");
+ return Console.ReadLine();
+ }
+
+ private static string GetAdjustmentReason()
+ {
+ Console.Write("Reason: ");
+ return Console.ReadLine();
+ }
+
+ private static (int Quantity, bool IsValid) GetQuantity()
+ {
+ Console.Write("Quantity: ");
+ if (int.TryParse(Console.ReadLine(), out var quantity))
+ {
+ return (quantity, true);
+ }
+ else
+ {
+ Console.WriteLine("Invalid Quantity.");
+ return (0, false);
+ }
+ }
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea5ebf9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,205 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.sln.docstates
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+x64/
+bld/
+[Bb]in/
+[Oo]bj/
+
+# Roslyn cache directories
+*.ide/
+
+# AWS
+*.aws-sam/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+#NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+*.cachefile
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding addin-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+## TODO: Comment the next line if you want to checkin your
+## web deploy settings but do note that will include unencrypted
+## passwords
+#*.pubxml
+
+# NuGet Packages Directory
+#packages/*
+## TODO: If the tool you use requires repositories.config
+## uncomment the next line
+#!packages/repositories.config
+
+# Enable "build/" folder in the NuGet Packages folder since
+# NuGet packages use it for MSBuild targets.
+# This line needs to be after the ignore of the build folder
+# (and the packages folder if the line above has been uncommented)
+#!packages/build/
+
+!src/packages/**/*.dll
+!src/packages/**/*.pdb
+
+# Windows Azure Build Output
+csx/
+*.build.csdef
+
+# Windows Store app package directory
+AppPackages/
+
+# Others
+*.Cache
+ClientBin/
+[Ss]tyle[Cc]op.*
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.pfx
+*.publishsettings
+node_modules/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# LightSwitch generated files
+GeneratedArtifacts/
+_Pvt_Extensions/
+ModelManifest.xml
+src/packages/EventStore.Client.3.3.1/lib/net40/EventStore.ClientAPI.xml
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# Vagrant VM files
+.vagrant
+vagrant/dbv/data/meta/revision
+
+# Visual Studio 2015 cache/options directory
+.vs/
\ No newline at end of file
diff --git a/EventSourcingSnapshots.csproj b/EventSourcingSnapshots.csproj
new file mode 100644
index 0000000..2a9a875
--- /dev/null
+++ b/EventSourcingSnapshots.csproj
@@ -0,0 +1,13 @@
+
+
+
+ Exe
+ net5.0
+
+
+
+
+
+
+
+
diff --git a/Events.cs b/Events.cs
new file mode 100644
index 0000000..018c062
--- /dev/null
+++ b/Events.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace EventSourcing.Demo
+{
+ public interface IEvent
+ {
+ string EventType { get; }
+ }
+
+ public record ProductShipped(string Sku, int Quantity, DateTime DateTime) : IEvent
+ {
+ public string EventType { get; } = "ProductShipped";
+ }
+
+ public record ProductReceived(string Sku, int Quantity, DateTime DateTime) : IEvent
+ {
+ public string EventType { get; } = "ProductReceived";
+ }
+
+ public record InventoryAdjusted(string Sku, int Quantity, string Reason, DateTime DateTime) : IEvent
+ {
+ public string EventType { get; } = "InventoryAdjusted";
+ }
+}
\ No newline at end of file
diff --git a/Program.cs b/Program.cs
new file mode 100644
index 0000000..d641d35
--- /dev/null
+++ b/Program.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Threading.Tasks;
+
+namespace EventSourcing.Demo
+{
+ class Program
+ {
+ static async Task Main()
+ {
+ var warehouseProductRepository = await WarehouseProductEventStoreStream.Factory();
+
+ var key = string.Empty;
+ while (key != "X")
+ {
+ Console.WriteLine("R: Receive Inventory");
+ Console.WriteLine("S: Ship Inventory");
+ Console.WriteLine("A: Inventory Adjustment");
+ Console.WriteLine("Q: Quantity On Hand");
+ Console.WriteLine("E: Events since Snapshot");
+ Console.Write("> ");
+ key = Console.ReadLine()?.ToUpperInvariant();
+ Console.WriteLine();
+
+ var sku = GetSkuFromConsole();
+ var warehouseProduct = await warehouseProductRepository.Get(sku);
+
+ switch (key)
+ {
+ case "R":
+ var receiveInput = GetQuantity();
+ if (receiveInput.IsValid)
+ {
+ warehouseProduct.ReceiveProduct(receiveInput.Quantity);
+ Console.WriteLine($"{sku} Received: {receiveInput.Quantity}");
+ }
+ break;
+ case "S":
+ var shipInput = GetQuantity();
+ if (shipInput.IsValid)
+ {
+ warehouseProduct.ShipProduct(shipInput.Quantity);
+ Console.WriteLine($"{sku} Shipped: {shipInput.Quantity}");
+ }
+ break;
+ case "A":
+ var adjustmentInput = GetQuantity();
+ if (adjustmentInput.IsValid)
+ {
+ var reason = GetAdjustmentReason();
+ warehouseProduct.AdjustInventory(adjustmentInput.Quantity, reason);
+ Console.WriteLine($"{sku} Adjusted: {adjustmentInput.Quantity} {reason}");
+ }
+ break;
+ case "Q":
+ var currentQuantityOnHand = warehouseProduct.GetQuantityOnHand();
+ Console.WriteLine($"{sku} Quantity On Hand: {currentQuantityOnHand}");
+ break;
+ case "E":
+ Console.WriteLine($"Events: {sku}");
+ foreach (var evnt in warehouseProduct.GetAllEvents())
+ {
+ switch (evnt)
+ {
+ case ProductShipped shipProduct:
+ Console.WriteLine($"{shipProduct.DateTime:u} {sku} Shipped: {shipProduct.Quantity}");
+ break;
+ case ProductReceived receiveProduct:
+ Console.WriteLine($"{receiveProduct.DateTime:u} {sku} Received: {receiveProduct.Quantity}");
+ break;
+ case InventoryAdjusted inventoryAdjusted:
+ Console.WriteLine($"{inventoryAdjusted.DateTime:u} {sku} Adjusted: {inventoryAdjusted.Quantity} {inventoryAdjusted.Reason}");
+ break;
+ }
+
+ }
+ break;
+ }
+
+ await warehouseProductRepository.Save(warehouseProduct);
+
+ Console.ReadLine();
+ Console.WriteLine();
+ }
+ }
+
+
+ private static string GetSkuFromConsole()
+ {
+ Console.Write("SKU: ");
+ return Console.ReadLine();
+ }
+
+ private static string GetAdjustmentReason()
+ {
+ Console.Write("Reason: ");
+ return Console.ReadLine();
+ }
+
+ private static (int Quantity, bool IsValid) GetQuantity()
+ {
+ Console.Write("Quantity: ");
+ if (int.TryParse(Console.ReadLine(), out var quantity))
+ {
+ return (quantity, true);
+ }
+ else
+ {
+ Console.WriteLine("Invalid Quantity.");
+ return (0, false);
+ }
+ }
+ }
+}
diff --git a/README.md b/README.md
index 9062fa3..1e629c6 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,8 @@
-EventSourcingSnapshots
+Event Sourcing Snapshots
===============
+
+EventStoreDB is required to be running locally to run this sample.
+
+If you have Docker, I've included a docker-compose file.
+
+`docker-compose up`
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea5ebf9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,205 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.sln.docstates
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+x64/
+bld/
+[Bb]in/
+[Oo]bj/
+
+# Roslyn cache directories
+*.ide/
+
+# AWS
+*.aws-sam/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+#NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+*.cachefile
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding addin-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+## TODO: Comment the next line if you want to checkin your
+## web deploy settings but do note that will include unencrypted
+## passwords
+#*.pubxml
+
+# NuGet Packages Directory
+#packages/*
+## TODO: If the tool you use requires repositories.config
+## uncomment the next line
+#!packages/repositories.config
+
+# Enable "build/" folder in the NuGet Packages folder since
+# NuGet packages use it for MSBuild targets.
+# This line needs to be after the ignore of the build folder
+# (and the packages folder if the line above has been uncommented)
+#!packages/build/
+
+!src/packages/**/*.dll
+!src/packages/**/*.pdb
+
+# Windows Azure Build Output
+csx/
+*.build.csdef
+
+# Windows Store app package directory
+AppPackages/
+
+# Others
+*.Cache
+ClientBin/
+[Ss]tyle[Cc]op.*
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.pfx
+*.publishsettings
+node_modules/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# LightSwitch generated files
+GeneratedArtifacts/
+_Pvt_Extensions/
+ModelManifest.xml
+src/packages/EventStore.Client.3.3.1/lib/net40/EventStore.ClientAPI.xml
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# Vagrant VM files
+.vagrant
+vagrant/dbv/data/meta/revision
+
+# Visual Studio 2015 cache/options directory
+.vs/
\ No newline at end of file
diff --git a/EventSourcingSnapshots.csproj b/EventSourcingSnapshots.csproj
new file mode 100644
index 0000000..2a9a875
--- /dev/null
+++ b/EventSourcingSnapshots.csproj
@@ -0,0 +1,13 @@
+
+
+
+ Exe
+ net5.0
+
+
+
+
+
+
+
+
diff --git a/Events.cs b/Events.cs
new file mode 100644
index 0000000..018c062
--- /dev/null
+++ b/Events.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace EventSourcing.Demo
+{
+ public interface IEvent
+ {
+ string EventType { get; }
+ }
+
+ public record ProductShipped(string Sku, int Quantity, DateTime DateTime) : IEvent
+ {
+ public string EventType { get; } = "ProductShipped";
+ }
+
+ public record ProductReceived(string Sku, int Quantity, DateTime DateTime) : IEvent
+ {
+ public string EventType { get; } = "ProductReceived";
+ }
+
+ public record InventoryAdjusted(string Sku, int Quantity, string Reason, DateTime DateTime) : IEvent
+ {
+ public string EventType { get; } = "InventoryAdjusted";
+ }
+}
\ No newline at end of file
diff --git a/Program.cs b/Program.cs
new file mode 100644
index 0000000..d641d35
--- /dev/null
+++ b/Program.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Threading.Tasks;
+
+namespace EventSourcing.Demo
+{
+ class Program
+ {
+ static async Task Main()
+ {
+ var warehouseProductRepository = await WarehouseProductEventStoreStream.Factory();
+
+ var key = string.Empty;
+ while (key != "X")
+ {
+ Console.WriteLine("R: Receive Inventory");
+ Console.WriteLine("S: Ship Inventory");
+ Console.WriteLine("A: Inventory Adjustment");
+ Console.WriteLine("Q: Quantity On Hand");
+ Console.WriteLine("E: Events since Snapshot");
+ Console.Write("> ");
+ key = Console.ReadLine()?.ToUpperInvariant();
+ Console.WriteLine();
+
+ var sku = GetSkuFromConsole();
+ var warehouseProduct = await warehouseProductRepository.Get(sku);
+
+ switch (key)
+ {
+ case "R":
+ var receiveInput = GetQuantity();
+ if (receiveInput.IsValid)
+ {
+ warehouseProduct.ReceiveProduct(receiveInput.Quantity);
+ Console.WriteLine($"{sku} Received: {receiveInput.Quantity}");
+ }
+ break;
+ case "S":
+ var shipInput = GetQuantity();
+ if (shipInput.IsValid)
+ {
+ warehouseProduct.ShipProduct(shipInput.Quantity);
+ Console.WriteLine($"{sku} Shipped: {shipInput.Quantity}");
+ }
+ break;
+ case "A":
+ var adjustmentInput = GetQuantity();
+ if (adjustmentInput.IsValid)
+ {
+ var reason = GetAdjustmentReason();
+ warehouseProduct.AdjustInventory(adjustmentInput.Quantity, reason);
+ Console.WriteLine($"{sku} Adjusted: {adjustmentInput.Quantity} {reason}");
+ }
+ break;
+ case "Q":
+ var currentQuantityOnHand = warehouseProduct.GetQuantityOnHand();
+ Console.WriteLine($"{sku} Quantity On Hand: {currentQuantityOnHand}");
+ break;
+ case "E":
+ Console.WriteLine($"Events: {sku}");
+ foreach (var evnt in warehouseProduct.GetAllEvents())
+ {
+ switch (evnt)
+ {
+ case ProductShipped shipProduct:
+ Console.WriteLine($"{shipProduct.DateTime:u} {sku} Shipped: {shipProduct.Quantity}");
+ break;
+ case ProductReceived receiveProduct:
+ Console.WriteLine($"{receiveProduct.DateTime:u} {sku} Received: {receiveProduct.Quantity}");
+ break;
+ case InventoryAdjusted inventoryAdjusted:
+ Console.WriteLine($"{inventoryAdjusted.DateTime:u} {sku} Adjusted: {inventoryAdjusted.Quantity} {inventoryAdjusted.Reason}");
+ break;
+ }
+
+ }
+ break;
+ }
+
+ await warehouseProductRepository.Save(warehouseProduct);
+
+ Console.ReadLine();
+ Console.WriteLine();
+ }
+ }
+
+
+ private static string GetSkuFromConsole()
+ {
+ Console.Write("SKU: ");
+ return Console.ReadLine();
+ }
+
+ private static string GetAdjustmentReason()
+ {
+ Console.Write("Reason: ");
+ return Console.ReadLine();
+ }
+
+ private static (int Quantity, bool IsValid) GetQuantity()
+ {
+ Console.Write("Quantity: ");
+ if (int.TryParse(Console.ReadLine(), out var quantity))
+ {
+ return (quantity, true);
+ }
+ else
+ {
+ Console.WriteLine("Invalid Quantity.");
+ return (0, false);
+ }
+ }
+ }
+}
diff --git a/README.md b/README.md
index 9062fa3..1e629c6 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,8 @@
-EventSourcingSnapshots
+Event Sourcing Snapshots
===============
+
+EventStoreDB is required to be running locally to run this sample.
+
+If you have Docker, I've included a docker-compose file.
+
+`docker-compose up`
\ No newline at end of file
diff --git a/WarehouseProduct.cs b/WarehouseProduct.cs
new file mode 100644
index 0000000..d52ee43
--- /dev/null
+++ b/WarehouseProduct.cs
@@ -0,0 +1,125 @@
+using System;
+using System.Collections.Generic;
+
+namespace EventSourcing.Demo
+{
+ public class WarehouseProductState
+ {
+ public int QuantityOnHand { get; set; }
+ }
+
+ public class WarehouseProduct
+ {
+ public string Sku { get; }
+ private readonly IList _allEvents = new List();
+ private readonly IList _uncommittedEvents = new List();
+
+ // Projection (Current State)
+ private readonly WarehouseProductState _warehouseProductState;
+
+ public WarehouseProduct(string sku, WarehouseProductState state)
+ {
+ Sku = sku;
+ _warehouseProductState = state;
+ }
+
+ public WarehouseProductState GetState()
+ {
+ return _warehouseProductState;
+ }
+
+ public void ShipProduct(int quantity)
+ {
+ if (quantity > _warehouseProductState.QuantityOnHand)
+ {
+ throw new InvalidDomainException("Ah... we don't have enough product to ship?");
+ }
+
+ AddEvent(new ProductShipped(Sku, quantity, DateTime.UtcNow));
+ }
+
+ public void ReceiveProduct(int quantity)
+ {
+ AddEvent(new ProductReceived(Sku, quantity, DateTime.UtcNow));
+ }
+
+ public void AdjustInventory(int quantity, string reason)
+ {
+ if (_warehouseProductState.QuantityOnHand + quantity < 0)
+ {
+ throw new InvalidDomainException("Cannot adjust to a negative quantity on hand.");
+ }
+
+ AddEvent(new InventoryAdjusted(Sku, quantity, reason, DateTime.UtcNow));
+ }
+
+ private void Apply(ProductShipped evnt)
+ {
+ _warehouseProductState.QuantityOnHand -= evnt.Quantity;
+ }
+
+ private void Apply(ProductReceived evnt)
+ {
+ _warehouseProductState.QuantityOnHand += evnt.Quantity;
+ }
+
+ private void Apply(InventoryAdjusted evnt)
+ {
+ _warehouseProductState.QuantityOnHand += evnt.Quantity;
+ }
+
+ public void ApplyEvent(IEvent evnt)
+ {
+ switch (evnt)
+ {
+ case ProductShipped shipProduct:
+ Apply(shipProduct);
+ break;
+ case ProductReceived receiveProduct:
+ Apply(receiveProduct);
+ break;
+ case InventoryAdjusted inventoryAdjusted:
+ Apply(inventoryAdjusted);
+ break;
+ default:
+ throw new InvalidOperationException("Unsupported Event.");
+ }
+
+ _allEvents.Add(evnt);
+ }
+
+ public void AddEvent(IEvent evnt)
+ {
+ ApplyEvent(evnt);
+ _uncommittedEvents.Add(evnt);
+ }
+
+ public IList GetUncommittedEvents()
+ {
+ return new List(_uncommittedEvents);
+ }
+
+ public IList GetAllEvents()
+ {
+ return new List(_allEvents);
+ }
+
+ public void EventsCommitted()
+ {
+ _uncommittedEvents.Clear();
+ }
+
+ public int GetQuantityOnHand()
+ {
+ return _warehouseProductState.QuantityOnHand;
+ }
+ }
+
+ public class InvalidDomainException : Exception
+ {
+ public InvalidDomainException(string message) : base(message)
+ {
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea5ebf9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,205 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.sln.docstates
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+x64/
+bld/
+[Bb]in/
+[Oo]bj/
+
+# Roslyn cache directories
+*.ide/
+
+# AWS
+*.aws-sam/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+#NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+*.cachefile
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding addin-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+## TODO: Comment the next line if you want to checkin your
+## web deploy settings but do note that will include unencrypted
+## passwords
+#*.pubxml
+
+# NuGet Packages Directory
+#packages/*
+## TODO: If the tool you use requires repositories.config
+## uncomment the next line
+#!packages/repositories.config
+
+# Enable "build/" folder in the NuGet Packages folder since
+# NuGet packages use it for MSBuild targets.
+# This line needs to be after the ignore of the build folder
+# (and the packages folder if the line above has been uncommented)
+#!packages/build/
+
+!src/packages/**/*.dll
+!src/packages/**/*.pdb
+
+# Windows Azure Build Output
+csx/
+*.build.csdef
+
+# Windows Store app package directory
+AppPackages/
+
+# Others
+*.Cache
+ClientBin/
+[Ss]tyle[Cc]op.*
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.pfx
+*.publishsettings
+node_modules/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# LightSwitch generated files
+GeneratedArtifacts/
+_Pvt_Extensions/
+ModelManifest.xml
+src/packages/EventStore.Client.3.3.1/lib/net40/EventStore.ClientAPI.xml
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# Vagrant VM files
+.vagrant
+vagrant/dbv/data/meta/revision
+
+# Visual Studio 2015 cache/options directory
+.vs/
\ No newline at end of file
diff --git a/EventSourcingSnapshots.csproj b/EventSourcingSnapshots.csproj
new file mode 100644
index 0000000..2a9a875
--- /dev/null
+++ b/EventSourcingSnapshots.csproj
@@ -0,0 +1,13 @@
+
+
+
+ Exe
+ net5.0
+
+
+
+
+
+
+
+
diff --git a/Events.cs b/Events.cs
new file mode 100644
index 0000000..018c062
--- /dev/null
+++ b/Events.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace EventSourcing.Demo
+{
+ public interface IEvent
+ {
+ string EventType { get; }
+ }
+
+ public record ProductShipped(string Sku, int Quantity, DateTime DateTime) : IEvent
+ {
+ public string EventType { get; } = "ProductShipped";
+ }
+
+ public record ProductReceived(string Sku, int Quantity, DateTime DateTime) : IEvent
+ {
+ public string EventType { get; } = "ProductReceived";
+ }
+
+ public record InventoryAdjusted(string Sku, int Quantity, string Reason, DateTime DateTime) : IEvent
+ {
+ public string EventType { get; } = "InventoryAdjusted";
+ }
+}
\ No newline at end of file
diff --git a/Program.cs b/Program.cs
new file mode 100644
index 0000000..d641d35
--- /dev/null
+++ b/Program.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Threading.Tasks;
+
+namespace EventSourcing.Demo
+{
+ class Program
+ {
+ static async Task Main()
+ {
+ var warehouseProductRepository = await WarehouseProductEventStoreStream.Factory();
+
+ var key = string.Empty;
+ while (key != "X")
+ {
+ Console.WriteLine("R: Receive Inventory");
+ Console.WriteLine("S: Ship Inventory");
+ Console.WriteLine("A: Inventory Adjustment");
+ Console.WriteLine("Q: Quantity On Hand");
+ Console.WriteLine("E: Events since Snapshot");
+ Console.Write("> ");
+ key = Console.ReadLine()?.ToUpperInvariant();
+ Console.WriteLine();
+
+ var sku = GetSkuFromConsole();
+ var warehouseProduct = await warehouseProductRepository.Get(sku);
+
+ switch (key)
+ {
+ case "R":
+ var receiveInput = GetQuantity();
+ if (receiveInput.IsValid)
+ {
+ warehouseProduct.ReceiveProduct(receiveInput.Quantity);
+ Console.WriteLine($"{sku} Received: {receiveInput.Quantity}");
+ }
+ break;
+ case "S":
+ var shipInput = GetQuantity();
+ if (shipInput.IsValid)
+ {
+ warehouseProduct.ShipProduct(shipInput.Quantity);
+ Console.WriteLine($"{sku} Shipped: {shipInput.Quantity}");
+ }
+ break;
+ case "A":
+ var adjustmentInput = GetQuantity();
+ if (adjustmentInput.IsValid)
+ {
+ var reason = GetAdjustmentReason();
+ warehouseProduct.AdjustInventory(adjustmentInput.Quantity, reason);
+ Console.WriteLine($"{sku} Adjusted: {adjustmentInput.Quantity} {reason}");
+ }
+ break;
+ case "Q":
+ var currentQuantityOnHand = warehouseProduct.GetQuantityOnHand();
+ Console.WriteLine($"{sku} Quantity On Hand: {currentQuantityOnHand}");
+ break;
+ case "E":
+ Console.WriteLine($"Events: {sku}");
+ foreach (var evnt in warehouseProduct.GetAllEvents())
+ {
+ switch (evnt)
+ {
+ case ProductShipped shipProduct:
+ Console.WriteLine($"{shipProduct.DateTime:u} {sku} Shipped: {shipProduct.Quantity}");
+ break;
+ case ProductReceived receiveProduct:
+ Console.WriteLine($"{receiveProduct.DateTime:u} {sku} Received: {receiveProduct.Quantity}");
+ break;
+ case InventoryAdjusted inventoryAdjusted:
+ Console.WriteLine($"{inventoryAdjusted.DateTime:u} {sku} Adjusted: {inventoryAdjusted.Quantity} {inventoryAdjusted.Reason}");
+ break;
+ }
+
+ }
+ break;
+ }
+
+ await warehouseProductRepository.Save(warehouseProduct);
+
+ Console.ReadLine();
+ Console.WriteLine();
+ }
+ }
+
+
+ private static string GetSkuFromConsole()
+ {
+ Console.Write("SKU: ");
+ return Console.ReadLine();
+ }
+
+ private static string GetAdjustmentReason()
+ {
+ Console.Write("Reason: ");
+ return Console.ReadLine();
+ }
+
+ private static (int Quantity, bool IsValid) GetQuantity()
+ {
+ Console.Write("Quantity: ");
+ if (int.TryParse(Console.ReadLine(), out var quantity))
+ {
+ return (quantity, true);
+ }
+ else
+ {
+ Console.WriteLine("Invalid Quantity.");
+ return (0, false);
+ }
+ }
+ }
+}
diff --git a/README.md b/README.md
index 9062fa3..1e629c6 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,8 @@
-EventSourcingSnapshots
+Event Sourcing Snapshots
===============
+
+EventStoreDB is required to be running locally to run this sample.
+
+If you have Docker, I've included a docker-compose file.
+
+`docker-compose up`
\ No newline at end of file
diff --git a/WarehouseProduct.cs b/WarehouseProduct.cs
new file mode 100644
index 0000000..d52ee43
--- /dev/null
+++ b/WarehouseProduct.cs
@@ -0,0 +1,125 @@
+using System;
+using System.Collections.Generic;
+
+namespace EventSourcing.Demo
+{
+ public class WarehouseProductState
+ {
+ public int QuantityOnHand { get; set; }
+ }
+
+ public class WarehouseProduct
+ {
+ public string Sku { get; }
+ private readonly IList _allEvents = new List();
+ private readonly IList _uncommittedEvents = new List();
+
+ // Projection (Current State)
+ private readonly WarehouseProductState _warehouseProductState;
+
+ public WarehouseProduct(string sku, WarehouseProductState state)
+ {
+ Sku = sku;
+ _warehouseProductState = state;
+ }
+
+ public WarehouseProductState GetState()
+ {
+ return _warehouseProductState;
+ }
+
+ public void ShipProduct(int quantity)
+ {
+ if (quantity > _warehouseProductState.QuantityOnHand)
+ {
+ throw new InvalidDomainException("Ah... we don't have enough product to ship?");
+ }
+
+ AddEvent(new ProductShipped(Sku, quantity, DateTime.UtcNow));
+ }
+
+ public void ReceiveProduct(int quantity)
+ {
+ AddEvent(new ProductReceived(Sku, quantity, DateTime.UtcNow));
+ }
+
+ public void AdjustInventory(int quantity, string reason)
+ {
+ if (_warehouseProductState.QuantityOnHand + quantity < 0)
+ {
+ throw new InvalidDomainException("Cannot adjust to a negative quantity on hand.");
+ }
+
+ AddEvent(new InventoryAdjusted(Sku, quantity, reason, DateTime.UtcNow));
+ }
+
+ private void Apply(ProductShipped evnt)
+ {
+ _warehouseProductState.QuantityOnHand -= evnt.Quantity;
+ }
+
+ private void Apply(ProductReceived evnt)
+ {
+ _warehouseProductState.QuantityOnHand += evnt.Quantity;
+ }
+
+ private void Apply(InventoryAdjusted evnt)
+ {
+ _warehouseProductState.QuantityOnHand += evnt.Quantity;
+ }
+
+ public void ApplyEvent(IEvent evnt)
+ {
+ switch (evnt)
+ {
+ case ProductShipped shipProduct:
+ Apply(shipProduct);
+ break;
+ case ProductReceived receiveProduct:
+ Apply(receiveProduct);
+ break;
+ case InventoryAdjusted inventoryAdjusted:
+ Apply(inventoryAdjusted);
+ break;
+ default:
+ throw new InvalidOperationException("Unsupported Event.");
+ }
+
+ _allEvents.Add(evnt);
+ }
+
+ public void AddEvent(IEvent evnt)
+ {
+ ApplyEvent(evnt);
+ _uncommittedEvents.Add(evnt);
+ }
+
+ public IList GetUncommittedEvents()
+ {
+ return new List(_uncommittedEvents);
+ }
+
+ public IList GetAllEvents()
+ {
+ return new List(_allEvents);
+ }
+
+ public void EventsCommitted()
+ {
+ _uncommittedEvents.Clear();
+ }
+
+ public int GetQuantityOnHand()
+ {
+ return _warehouseProductState.QuantityOnHand;
+ }
+ }
+
+ public class InvalidDomainException : Exception
+ {
+ public InvalidDomainException(string message) : base(message)
+ {
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/WarehouseProductRepository.cs b/WarehouseProductRepository.cs
new file mode 100644
index 0000000..ea2234d
--- /dev/null
+++ b/WarehouseProductRepository.cs
@@ -0,0 +1,155 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+using EventStore.ClientAPI;
+using EventStore.ClientAPI.SystemData;
+using Newtonsoft.Json;
+
+namespace EventSourcing.Demo
+{
+ public class WarehouseProductEventStoreStream
+ {
+ private const int SnapshotInterval = 1;
+ private readonly IEventStoreConnection _connection;
+
+ public static async Task Factory()
+ {
+ var connectionSettings = ConnectionSettings.Create()
+ .KeepReconnecting()
+ .KeepRetrying()
+ .SetHeartbeatTimeout(TimeSpan.FromMinutes(5))
+ .SetHeartbeatInterval(TimeSpan.FromMinutes(1))
+ .DisableTls()
+ .DisableServerCertificateValidation()
+ .SetDefaultUserCredentials(new UserCredentials("admin", "changeit"))
+ .Build();
+
+ var conn = EventStoreConnection.Create(connectionSettings, new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1113));
+ await conn.ConnectAsync();
+
+ return new WarehouseProductEventStoreStream(conn);
+ }
+
+ private WarehouseProductEventStoreStream(IEventStoreConnection connection)
+ {
+ _connection = connection;
+ }
+
+ private string GetStreamName(string sku)
+ {
+ return $"WarehouseProduct-{sku}";
+ }
+
+ private string GetSnapshotStreamName(string sku)
+ {
+ return $"WarehouseProduct-Snapshot-{sku}";
+ }
+
+ public async Task Get(string sku)
+ {
+ var streamName = GetStreamName(sku);
+
+ var snapshot = await GetSnapshot(sku);
+ var warehouseProduct = new WarehouseProduct(sku, snapshot.State);
+
+ StreamEventsSlice currentSlice;
+ var nextSliceStart = snapshot.Version + 1;
+ do
+ {
+ currentSlice = await _connection.ReadStreamEventsForwardAsync(
+ streamName,
+ nextSliceStart,
+ 200,
+ false
+ );
+
+ nextSliceStart = currentSlice.NextEventNumber;
+
+ foreach (var evnt in currentSlice.Events)
+ {
+ var eventObj = DeserializeEvent(evnt);
+ warehouseProduct.ApplyEvent(eventObj);
+ }
+ } while (!currentSlice.IsEndOfStream);
+
+ return warehouseProduct;
+ }
+
+ private async Task GetSnapshot(string sku)
+ {
+ var streamName = GetSnapshotStreamName(sku);
+ var slice = await _connection.ReadStreamEventsBackwardAsync(streamName, (long)StreamPosition.End, 1, false);
+ if (slice.Events.Any())
+ {
+ var evnt = slice.Events.First();
+ var json = Encoding.UTF8.GetString(evnt.Event.Data);
+ return JsonConvert.DeserializeObject(json);
+ }
+
+ return new Snapshot();
+ }
+
+ public async Task Save(WarehouseProduct warehouseProduct)
+ {
+ var streamName = GetStreamName(warehouseProduct.Sku);
+
+ var newEvents = warehouseProduct.GetUncommittedEvents();
+ foreach (var evnt in newEvents)
+ {
+ var data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(evnt));
+ var metadata = Encoding.UTF8.GetBytes("{}");
+ var evt = new EventData(Guid.NewGuid(), evnt.EventType, true, data, metadata);
+ var result = await _connection.AppendToStreamAsync(streamName, ExpectedVersion.Any, evt);
+
+ if ((result.NextExpectedVersion - 1) % SnapshotInterval == 0)
+ {
+ await AppendSnapshot(warehouseProduct, result.NextExpectedVersion);
+ }
+ }
+
+ }
+
+ private IEvent DeserializeEvent(ResolvedEvent evnt)
+ {
+ var json = Encoding.UTF8.GetString(evnt.Event.Data);
+ return evnt.Event.EventType switch
+ {
+ "InventoryAdjusted" => JsonConvert.DeserializeObject(json),
+ "ProductShipped" => JsonConvert.DeserializeObject(json),
+ "ProductReceived" => JsonConvert.DeserializeObject(json),
+ _ => throw new InvalidOperationException($"Unknown Event: {evnt.Event.EventType}")
+ };
+ }
+
+ private async Task AppendSnapshot(WarehouseProduct warehouseProduct, long version)
+ {
+ var streamName = GetSnapshotStreamName(warehouseProduct.Sku);
+ var state = warehouseProduct.GetState();
+
+ var snapshot = new Snapshot
+ {
+ State = state,
+ Version = version
+ };
+
+ var metadata = Encoding.UTF8.GetBytes("{}");
+ var data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(snapshot));
+ var evt = new EventData(Guid.NewGuid(), "snapshot", true, data, metadata);
+ await _connection.AppendToStreamAsync(streamName, ExpectedVersion.Any, evt);
+ }
+
+ public void Dispose()
+ {
+ _connection?.Dispose();
+ }
+ }
+
+ public class Snapshot
+ {
+ public long Version { get; set; } = 0;
+ public WarehouseProductState State { get; set; } = new();
+ }
+}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea5ebf9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,205 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.sln.docstates
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+x64/
+bld/
+[Bb]in/
+[Oo]bj/
+
+# Roslyn cache directories
+*.ide/
+
+# AWS
+*.aws-sam/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+#NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+*.cachefile
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding addin-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+## TODO: Comment the next line if you want to checkin your
+## web deploy settings but do note that will include unencrypted
+## passwords
+#*.pubxml
+
+# NuGet Packages Directory
+#packages/*
+## TODO: If the tool you use requires repositories.config
+## uncomment the next line
+#!packages/repositories.config
+
+# Enable "build/" folder in the NuGet Packages folder since
+# NuGet packages use it for MSBuild targets.
+# This line needs to be after the ignore of the build folder
+# (and the packages folder if the line above has been uncommented)
+#!packages/build/
+
+!src/packages/**/*.dll
+!src/packages/**/*.pdb
+
+# Windows Azure Build Output
+csx/
+*.build.csdef
+
+# Windows Store app package directory
+AppPackages/
+
+# Others
+*.Cache
+ClientBin/
+[Ss]tyle[Cc]op.*
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.pfx
+*.publishsettings
+node_modules/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# LightSwitch generated files
+GeneratedArtifacts/
+_Pvt_Extensions/
+ModelManifest.xml
+src/packages/EventStore.Client.3.3.1/lib/net40/EventStore.ClientAPI.xml
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# Vagrant VM files
+.vagrant
+vagrant/dbv/data/meta/revision
+
+# Visual Studio 2015 cache/options directory
+.vs/
\ No newline at end of file
diff --git a/EventSourcingSnapshots.csproj b/EventSourcingSnapshots.csproj
new file mode 100644
index 0000000..2a9a875
--- /dev/null
+++ b/EventSourcingSnapshots.csproj
@@ -0,0 +1,13 @@
+
+
+
+ Exe
+ net5.0
+
+
+
+
+
+
+
+
diff --git a/Events.cs b/Events.cs
new file mode 100644
index 0000000..018c062
--- /dev/null
+++ b/Events.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace EventSourcing.Demo
+{
+ public interface IEvent
+ {
+ string EventType { get; }
+ }
+
+ public record ProductShipped(string Sku, int Quantity, DateTime DateTime) : IEvent
+ {
+ public string EventType { get; } = "ProductShipped";
+ }
+
+ public record ProductReceived(string Sku, int Quantity, DateTime DateTime) : IEvent
+ {
+ public string EventType { get; } = "ProductReceived";
+ }
+
+ public record InventoryAdjusted(string Sku, int Quantity, string Reason, DateTime DateTime) : IEvent
+ {
+ public string EventType { get; } = "InventoryAdjusted";
+ }
+}
\ No newline at end of file
diff --git a/Program.cs b/Program.cs
new file mode 100644
index 0000000..d641d35
--- /dev/null
+++ b/Program.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Threading.Tasks;
+
+namespace EventSourcing.Demo
+{
+ class Program
+ {
+ static async Task Main()
+ {
+ var warehouseProductRepository = await WarehouseProductEventStoreStream.Factory();
+
+ var key = string.Empty;
+ while (key != "X")
+ {
+ Console.WriteLine("R: Receive Inventory");
+ Console.WriteLine("S: Ship Inventory");
+ Console.WriteLine("A: Inventory Adjustment");
+ Console.WriteLine("Q: Quantity On Hand");
+ Console.WriteLine("E: Events since Snapshot");
+ Console.Write("> ");
+ key = Console.ReadLine()?.ToUpperInvariant();
+ Console.WriteLine();
+
+ var sku = GetSkuFromConsole();
+ var warehouseProduct = await warehouseProductRepository.Get(sku);
+
+ switch (key)
+ {
+ case "R":
+ var receiveInput = GetQuantity();
+ if (receiveInput.IsValid)
+ {
+ warehouseProduct.ReceiveProduct(receiveInput.Quantity);
+ Console.WriteLine($"{sku} Received: {receiveInput.Quantity}");
+ }
+ break;
+ case "S":
+ var shipInput = GetQuantity();
+ if (shipInput.IsValid)
+ {
+ warehouseProduct.ShipProduct(shipInput.Quantity);
+ Console.WriteLine($"{sku} Shipped: {shipInput.Quantity}");
+ }
+ break;
+ case "A":
+ var adjustmentInput = GetQuantity();
+ if (adjustmentInput.IsValid)
+ {
+ var reason = GetAdjustmentReason();
+ warehouseProduct.AdjustInventory(adjustmentInput.Quantity, reason);
+ Console.WriteLine($"{sku} Adjusted: {adjustmentInput.Quantity} {reason}");
+ }
+ break;
+ case "Q":
+ var currentQuantityOnHand = warehouseProduct.GetQuantityOnHand();
+ Console.WriteLine($"{sku} Quantity On Hand: {currentQuantityOnHand}");
+ break;
+ case "E":
+ Console.WriteLine($"Events: {sku}");
+ foreach (var evnt in warehouseProduct.GetAllEvents())
+ {
+ switch (evnt)
+ {
+ case ProductShipped shipProduct:
+ Console.WriteLine($"{shipProduct.DateTime:u} {sku} Shipped: {shipProduct.Quantity}");
+ break;
+ case ProductReceived receiveProduct:
+ Console.WriteLine($"{receiveProduct.DateTime:u} {sku} Received: {receiveProduct.Quantity}");
+ break;
+ case InventoryAdjusted inventoryAdjusted:
+ Console.WriteLine($"{inventoryAdjusted.DateTime:u} {sku} Adjusted: {inventoryAdjusted.Quantity} {inventoryAdjusted.Reason}");
+ break;
+ }
+
+ }
+ break;
+ }
+
+ await warehouseProductRepository.Save(warehouseProduct);
+
+ Console.ReadLine();
+ Console.WriteLine();
+ }
+ }
+
+
+ private static string GetSkuFromConsole()
+ {
+ Console.Write("SKU: ");
+ return Console.ReadLine();
+ }
+
+ private static string GetAdjustmentReason()
+ {
+ Console.Write("Reason: ");
+ return Console.ReadLine();
+ }
+
+ private static (int Quantity, bool IsValid) GetQuantity()
+ {
+ Console.Write("Quantity: ");
+ if (int.TryParse(Console.ReadLine(), out var quantity))
+ {
+ return (quantity, true);
+ }
+ else
+ {
+ Console.WriteLine("Invalid Quantity.");
+ return (0, false);
+ }
+ }
+ }
+}
diff --git a/README.md b/README.md
index 9062fa3..1e629c6 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,8 @@
-EventSourcingSnapshots
+Event Sourcing Snapshots
===============
+
+EventStoreDB is required to be running locally to run this sample.
+
+If you have Docker, I've included a docker-compose file.
+
+`docker-compose up`
\ No newline at end of file
diff --git a/WarehouseProduct.cs b/WarehouseProduct.cs
new file mode 100644
index 0000000..d52ee43
--- /dev/null
+++ b/WarehouseProduct.cs
@@ -0,0 +1,125 @@
+using System;
+using System.Collections.Generic;
+
+namespace EventSourcing.Demo
+{
+ public class WarehouseProductState
+ {
+ public int QuantityOnHand { get; set; }
+ }
+
+ public class WarehouseProduct
+ {
+ public string Sku { get; }
+ private readonly IList _allEvents = new List();
+ private readonly IList _uncommittedEvents = new List();
+
+ // Projection (Current State)
+ private readonly WarehouseProductState _warehouseProductState;
+
+ public WarehouseProduct(string sku, WarehouseProductState state)
+ {
+ Sku = sku;
+ _warehouseProductState = state;
+ }
+
+ public WarehouseProductState GetState()
+ {
+ return _warehouseProductState;
+ }
+
+ public void ShipProduct(int quantity)
+ {
+ if (quantity > _warehouseProductState.QuantityOnHand)
+ {
+ throw new InvalidDomainException("Ah... we don't have enough product to ship?");
+ }
+
+ AddEvent(new ProductShipped(Sku, quantity, DateTime.UtcNow));
+ }
+
+ public void ReceiveProduct(int quantity)
+ {
+ AddEvent(new ProductReceived(Sku, quantity, DateTime.UtcNow));
+ }
+
+ public void AdjustInventory(int quantity, string reason)
+ {
+ if (_warehouseProductState.QuantityOnHand + quantity < 0)
+ {
+ throw new InvalidDomainException("Cannot adjust to a negative quantity on hand.");
+ }
+
+ AddEvent(new InventoryAdjusted(Sku, quantity, reason, DateTime.UtcNow));
+ }
+
+ private void Apply(ProductShipped evnt)
+ {
+ _warehouseProductState.QuantityOnHand -= evnt.Quantity;
+ }
+
+ private void Apply(ProductReceived evnt)
+ {
+ _warehouseProductState.QuantityOnHand += evnt.Quantity;
+ }
+
+ private void Apply(InventoryAdjusted evnt)
+ {
+ _warehouseProductState.QuantityOnHand += evnt.Quantity;
+ }
+
+ public void ApplyEvent(IEvent evnt)
+ {
+ switch (evnt)
+ {
+ case ProductShipped shipProduct:
+ Apply(shipProduct);
+ break;
+ case ProductReceived receiveProduct:
+ Apply(receiveProduct);
+ break;
+ case InventoryAdjusted inventoryAdjusted:
+ Apply(inventoryAdjusted);
+ break;
+ default:
+ throw new InvalidOperationException("Unsupported Event.");
+ }
+
+ _allEvents.Add(evnt);
+ }
+
+ public void AddEvent(IEvent evnt)
+ {
+ ApplyEvent(evnt);
+ _uncommittedEvents.Add(evnt);
+ }
+
+ public IList GetUncommittedEvents()
+ {
+ return new List(_uncommittedEvents);
+ }
+
+ public IList GetAllEvents()
+ {
+ return new List(_allEvents);
+ }
+
+ public void EventsCommitted()
+ {
+ _uncommittedEvents.Clear();
+ }
+
+ public int GetQuantityOnHand()
+ {
+ return _warehouseProductState.QuantityOnHand;
+ }
+ }
+
+ public class InvalidDomainException : Exception
+ {
+ public InvalidDomainException(string message) : base(message)
+ {
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/WarehouseProductRepository.cs b/WarehouseProductRepository.cs
new file mode 100644
index 0000000..ea2234d
--- /dev/null
+++ b/WarehouseProductRepository.cs
@@ -0,0 +1,155 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+using EventStore.ClientAPI;
+using EventStore.ClientAPI.SystemData;
+using Newtonsoft.Json;
+
+namespace EventSourcing.Demo
+{
+ public class WarehouseProductEventStoreStream
+ {
+ private const int SnapshotInterval = 1;
+ private readonly IEventStoreConnection _connection;
+
+ public static async Task Factory()
+ {
+ var connectionSettings = ConnectionSettings.Create()
+ .KeepReconnecting()
+ .KeepRetrying()
+ .SetHeartbeatTimeout(TimeSpan.FromMinutes(5))
+ .SetHeartbeatInterval(TimeSpan.FromMinutes(1))
+ .DisableTls()
+ .DisableServerCertificateValidation()
+ .SetDefaultUserCredentials(new UserCredentials("admin", "changeit"))
+ .Build();
+
+ var conn = EventStoreConnection.Create(connectionSettings, new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1113));
+ await conn.ConnectAsync();
+
+ return new WarehouseProductEventStoreStream(conn);
+ }
+
+ private WarehouseProductEventStoreStream(IEventStoreConnection connection)
+ {
+ _connection = connection;
+ }
+
+ private string GetStreamName(string sku)
+ {
+ return $"WarehouseProduct-{sku}";
+ }
+
+ private string GetSnapshotStreamName(string sku)
+ {
+ return $"WarehouseProduct-Snapshot-{sku}";
+ }
+
+ public async Task Get(string sku)
+ {
+ var streamName = GetStreamName(sku);
+
+ var snapshot = await GetSnapshot(sku);
+ var warehouseProduct = new WarehouseProduct(sku, snapshot.State);
+
+ StreamEventsSlice currentSlice;
+ var nextSliceStart = snapshot.Version + 1;
+ do
+ {
+ currentSlice = await _connection.ReadStreamEventsForwardAsync(
+ streamName,
+ nextSliceStart,
+ 200,
+ false
+ );
+
+ nextSliceStart = currentSlice.NextEventNumber;
+
+ foreach (var evnt in currentSlice.Events)
+ {
+ var eventObj = DeserializeEvent(evnt);
+ warehouseProduct.ApplyEvent(eventObj);
+ }
+ } while (!currentSlice.IsEndOfStream);
+
+ return warehouseProduct;
+ }
+
+ private async Task GetSnapshot(string sku)
+ {
+ var streamName = GetSnapshotStreamName(sku);
+ var slice = await _connection.ReadStreamEventsBackwardAsync(streamName, (long)StreamPosition.End, 1, false);
+ if (slice.Events.Any())
+ {
+ var evnt = slice.Events.First();
+ var json = Encoding.UTF8.GetString(evnt.Event.Data);
+ return JsonConvert.DeserializeObject(json);
+ }
+
+ return new Snapshot();
+ }
+
+ public async Task Save(WarehouseProduct warehouseProduct)
+ {
+ var streamName = GetStreamName(warehouseProduct.Sku);
+
+ var newEvents = warehouseProduct.GetUncommittedEvents();
+ foreach (var evnt in newEvents)
+ {
+ var data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(evnt));
+ var metadata = Encoding.UTF8.GetBytes("{}");
+ var evt = new EventData(Guid.NewGuid(), evnt.EventType, true, data, metadata);
+ var result = await _connection.AppendToStreamAsync(streamName, ExpectedVersion.Any, evt);
+
+ if ((result.NextExpectedVersion - 1) % SnapshotInterval == 0)
+ {
+ await AppendSnapshot(warehouseProduct, result.NextExpectedVersion);
+ }
+ }
+
+ }
+
+ private IEvent DeserializeEvent(ResolvedEvent evnt)
+ {
+ var json = Encoding.UTF8.GetString(evnt.Event.Data);
+ return evnt.Event.EventType switch
+ {
+ "InventoryAdjusted" => JsonConvert.DeserializeObject(json),
+ "ProductShipped" => JsonConvert.DeserializeObject(json),
+ "ProductReceived" => JsonConvert.DeserializeObject(json),
+ _ => throw new InvalidOperationException($"Unknown Event: {evnt.Event.EventType}")
+ };
+ }
+
+ private async Task AppendSnapshot(WarehouseProduct warehouseProduct, long version)
+ {
+ var streamName = GetSnapshotStreamName(warehouseProduct.Sku);
+ var state = warehouseProduct.GetState();
+
+ var snapshot = new Snapshot
+ {
+ State = state,
+ Version = version
+ };
+
+ var metadata = Encoding.UTF8.GetBytes("{}");
+ var data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(snapshot));
+ var evt = new EventData(Guid.NewGuid(), "snapshot", true, data, metadata);
+ await _connection.AppendToStreamAsync(streamName, ExpectedVersion.Any, evt);
+ }
+
+ public void Dispose()
+ {
+ _connection?.Dispose();
+ }
+ }
+
+ public class Snapshot
+ {
+ public long Version { get; set; } = 0;
+ public WarehouseProductState State { get; set; } = new();
+ }
+}
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..a4a0bb4
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,19 @@
+version: '3.7'
+
+services:
+
+ eventstore:
+ container_name: esdb
+ image: eventstore/eventstore:20.10.0-bionic
+ ports:
+ - '2113:2113'
+ - '1113:1113'
+ environment:
+ - EVENTSTORE_CLUSTER_SIZE=1
+ - EVENTSTORE_RUN_PROJECTIONS=All
+ - EVENTSTORE_START_STANDARD_PROJECTIONS=true
+ - EVENTSTORE_EXT_TCP_PORT=1113
+ - EVENTSTORE_EXT_HTTP_PORT=2113
+ - EVENTSTORE_INSECURE=true
+ - EVENTSTORE_ENABLE_EXTERNAL_TCP=true
+ - EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP=true
\ No newline at end of file