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