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/AggregateRoot.cs b/AggregateRoot.cs new file mode 100644 index 0000000..68b0153 --- /dev/null +++ b/AggregateRoot.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace EventSourcing.Demo +{ + public abstract class AggregateRoot + { + private readonly IList _uncommittedEvents = new List(); + + public IList GetUncommittedEvents() + { + return _uncommittedEvents; + } + + public void ClearUncommittedEvents() + { + _uncommittedEvents.Clear(); + } + + protected void Add(IEvent evnt) + { + _uncommittedEvents.Add(evnt); + } + + public abstract void Load(IEnumerable events); + } +} \ 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/AggregateRoot.cs b/AggregateRoot.cs new file mode 100644 index 0000000..68b0153 --- /dev/null +++ b/AggregateRoot.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace EventSourcing.Demo +{ + public abstract class AggregateRoot + { + private readonly IList _uncommittedEvents = new List(); + + public IList GetUncommittedEvents() + { + return _uncommittedEvents; + } + + public void ClearUncommittedEvents() + { + _uncommittedEvents.Clear(); + } + + protected void Add(IEvent evnt) + { + _uncommittedEvents.Add(evnt); + } + + public abstract void Load(IEnumerable events); + } +} \ No newline at end of file diff --git a/AggregateTests.cs b/AggregateTests.cs new file mode 100644 index 0000000..5c0173e --- /dev/null +++ b/AggregateTests.cs @@ -0,0 +1,50 @@ +using System; +using System.Linq; +using Shouldly; + +namespace EventSourcing.Demo +{ + public abstract class AggregateTests where TAggregate : AggregateRoot + { + private readonly TAggregate _aggregateRoot; + + protected AggregateTests(TAggregate aggregateRoot) + { + _aggregateRoot = aggregateRoot; + } + + protected void Given(params IEvent[] events) + { + if (events != null) + { + _aggregateRoot.Load(events); + } + } + + protected void When(Action command) + { + command(_aggregateRoot); + } + + protected void Then(params Action[] conditions) + { + var events = _aggregateRoot.GetUncommittedEvents(); + events.Count.ShouldBe(1); + var evnt = events.First(); + evnt.ShouldBeOfType(); + if (conditions != null) + { + ((TEvent)evnt).ShouldSatisfyAllConditions(conditions); + } + } + + protected void Throws(Action command, params Action[] conditions) where TException : Exception + { + var ex = Should.Throw(() => command(_aggregateRoot)); + if (conditions != null) + { + ex.ShouldSatisfyAllConditions(conditions); + } + } + } +} \ 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/AggregateRoot.cs b/AggregateRoot.cs new file mode 100644 index 0000000..68b0153 --- /dev/null +++ b/AggregateRoot.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace EventSourcing.Demo +{ + public abstract class AggregateRoot + { + private readonly IList _uncommittedEvents = new List(); + + public IList GetUncommittedEvents() + { + return _uncommittedEvents; + } + + public void ClearUncommittedEvents() + { + _uncommittedEvents.Clear(); + } + + protected void Add(IEvent evnt) + { + _uncommittedEvents.Add(evnt); + } + + public abstract void Load(IEnumerable events); + } +} \ No newline at end of file diff --git a/AggregateTests.cs b/AggregateTests.cs new file mode 100644 index 0000000..5c0173e --- /dev/null +++ b/AggregateTests.cs @@ -0,0 +1,50 @@ +using System; +using System.Linq; +using Shouldly; + +namespace EventSourcing.Demo +{ + public abstract class AggregateTests where TAggregate : AggregateRoot + { + private readonly TAggregate _aggregateRoot; + + protected AggregateTests(TAggregate aggregateRoot) + { + _aggregateRoot = aggregateRoot; + } + + protected void Given(params IEvent[] events) + { + if (events != null) + { + _aggregateRoot.Load(events); + } + } + + protected void When(Action command) + { + command(_aggregateRoot); + } + + protected void Then(params Action[] conditions) + { + var events = _aggregateRoot.GetUncommittedEvents(); + events.Count.ShouldBe(1); + var evnt = events.First(); + evnt.ShouldBeOfType(); + if (conditions != null) + { + ((TEvent)evnt).ShouldSatisfyAllConditions(conditions); + } + } + + protected void Throws(Action command, params Action[] conditions) where TException : Exception + { + var ex = Should.Throw(() => command(_aggregateRoot)); + if (conditions != null) + { + ex.ShouldSatisfyAllConditions(conditions); + } + } + } +} \ No newline at end of file diff --git a/EventSourcing.Demo.csproj b/EventSourcing.Demo.csproj new file mode 100644 index 0000000..1b30db0 --- /dev/null +++ b/EventSourcing.Demo.csproj @@ -0,0 +1,14 @@ + + + + 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/AggregateRoot.cs b/AggregateRoot.cs new file mode 100644 index 0000000..68b0153 --- /dev/null +++ b/AggregateRoot.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace EventSourcing.Demo +{ + public abstract class AggregateRoot + { + private readonly IList _uncommittedEvents = new List(); + + public IList GetUncommittedEvents() + { + return _uncommittedEvents; + } + + public void ClearUncommittedEvents() + { + _uncommittedEvents.Clear(); + } + + protected void Add(IEvent evnt) + { + _uncommittedEvents.Add(evnt); + } + + public abstract void Load(IEnumerable events); + } +} \ No newline at end of file diff --git a/AggregateTests.cs b/AggregateTests.cs new file mode 100644 index 0000000..5c0173e --- /dev/null +++ b/AggregateTests.cs @@ -0,0 +1,50 @@ +using System; +using System.Linq; +using Shouldly; + +namespace EventSourcing.Demo +{ + public abstract class AggregateTests where TAggregate : AggregateRoot + { + private readonly TAggregate _aggregateRoot; + + protected AggregateTests(TAggregate aggregateRoot) + { + _aggregateRoot = aggregateRoot; + } + + protected void Given(params IEvent[] events) + { + if (events != null) + { + _aggregateRoot.Load(events); + } + } + + protected void When(Action command) + { + command(_aggregateRoot); + } + + protected void Then(params Action[] conditions) + { + var events = _aggregateRoot.GetUncommittedEvents(); + events.Count.ShouldBe(1); + var evnt = events.First(); + evnt.ShouldBeOfType(); + if (conditions != null) + { + ((TEvent)evnt).ShouldSatisfyAllConditions(conditions); + } + } + + protected void Throws(Action command, params Action[] conditions) where TException : Exception + { + var ex = Should.Throw(() => command(_aggregateRoot)); + if (conditions != null) + { + ex.ShouldSatisfyAllConditions(conditions); + } + } + } +} \ No newline at end of file diff --git a/EventSourcing.Demo.csproj b/EventSourcing.Demo.csproj new file mode 100644 index 0000000..1b30db0 --- /dev/null +++ b/EventSourcing.Demo.csproj @@ -0,0 +1,14 @@ + + + + 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/AggregateRoot.cs b/AggregateRoot.cs new file mode 100644 index 0000000..68b0153 --- /dev/null +++ b/AggregateRoot.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace EventSourcing.Demo +{ + public abstract class AggregateRoot + { + private readonly IList _uncommittedEvents = new List(); + + public IList GetUncommittedEvents() + { + return _uncommittedEvents; + } + + public void ClearUncommittedEvents() + { + _uncommittedEvents.Clear(); + } + + protected void Add(IEvent evnt) + { + _uncommittedEvents.Add(evnt); + } + + public abstract void Load(IEnumerable events); + } +} \ No newline at end of file diff --git a/AggregateTests.cs b/AggregateTests.cs new file mode 100644 index 0000000..5c0173e --- /dev/null +++ b/AggregateTests.cs @@ -0,0 +1,50 @@ +using System; +using System.Linq; +using Shouldly; + +namespace EventSourcing.Demo +{ + public abstract class AggregateTests where TAggregate : AggregateRoot + { + private readonly TAggregate _aggregateRoot; + + protected AggregateTests(TAggregate aggregateRoot) + { + _aggregateRoot = aggregateRoot; + } + + protected void Given(params IEvent[] events) + { + if (events != null) + { + _aggregateRoot.Load(events); + } + } + + protected void When(Action command) + { + command(_aggregateRoot); + } + + protected void Then(params Action[] conditions) + { + var events = _aggregateRoot.GetUncommittedEvents(); + events.Count.ShouldBe(1); + var evnt = events.First(); + evnt.ShouldBeOfType(); + if (conditions != null) + { + ((TEvent)evnt).ShouldSatisfyAllConditions(conditions); + } + } + + protected void Throws(Action command, params Action[] conditions) where TException : Exception + { + var ex = Should.Throw(() => command(_aggregateRoot)); + if (conditions != null) + { + ex.ShouldSatisfyAllConditions(conditions); + } + } + } +} \ No newline at end of file diff --git a/EventSourcing.Demo.csproj b/EventSourcing.Demo.csproj new file mode 100644 index 0000000..1b30db0 --- /dev/null +++ b/EventSourcing.Demo.csproj @@ -0,0 +1,14 @@ + + + + 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/Folder.DotSettings b/Folder.DotSettings new file mode 100644 index 0000000..408a942 --- /dev/null +++ b/Folder.DotSettings @@ -0,0 +1,2 @@ + + True \ 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/AggregateRoot.cs b/AggregateRoot.cs new file mode 100644 index 0000000..68b0153 --- /dev/null +++ b/AggregateRoot.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace EventSourcing.Demo +{ + public abstract class AggregateRoot + { + private readonly IList _uncommittedEvents = new List(); + + public IList GetUncommittedEvents() + { + return _uncommittedEvents; + } + + public void ClearUncommittedEvents() + { + _uncommittedEvents.Clear(); + } + + protected void Add(IEvent evnt) + { + _uncommittedEvents.Add(evnt); + } + + public abstract void Load(IEnumerable events); + } +} \ No newline at end of file diff --git a/AggregateTests.cs b/AggregateTests.cs new file mode 100644 index 0000000..5c0173e --- /dev/null +++ b/AggregateTests.cs @@ -0,0 +1,50 @@ +using System; +using System.Linq; +using Shouldly; + +namespace EventSourcing.Demo +{ + public abstract class AggregateTests where TAggregate : AggregateRoot + { + private readonly TAggregate _aggregateRoot; + + protected AggregateTests(TAggregate aggregateRoot) + { + _aggregateRoot = aggregateRoot; + } + + protected void Given(params IEvent[] events) + { + if (events != null) + { + _aggregateRoot.Load(events); + } + } + + protected void When(Action command) + { + command(_aggregateRoot); + } + + protected void Then(params Action[] conditions) + { + var events = _aggregateRoot.GetUncommittedEvents(); + events.Count.ShouldBe(1); + var evnt = events.First(); + evnt.ShouldBeOfType(); + if (conditions != null) + { + ((TEvent)evnt).ShouldSatisfyAllConditions(conditions); + } + } + + protected void Throws(Action command, params Action[] conditions) where TException : Exception + { + var ex = Should.Throw(() => command(_aggregateRoot)); + if (conditions != null) + { + ex.ShouldSatisfyAllConditions(conditions); + } + } + } +} \ No newline at end of file diff --git a/EventSourcing.Demo.csproj b/EventSourcing.Demo.csproj new file mode 100644 index 0000000..1b30db0 --- /dev/null +++ b/EventSourcing.Demo.csproj @@ -0,0 +1,14 @@ + + + + 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/Folder.DotSettings b/Folder.DotSettings new file mode 100644 index 0000000..408a942 --- /dev/null +++ b/Folder.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/README.md b/README.md index 710ceea..65c5cde 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -BusinessRules +EventSourcingTesting =============== 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/AggregateRoot.cs b/AggregateRoot.cs new file mode 100644 index 0000000..68b0153 --- /dev/null +++ b/AggregateRoot.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace EventSourcing.Demo +{ + public abstract class AggregateRoot + { + private readonly IList _uncommittedEvents = new List(); + + public IList GetUncommittedEvents() + { + return _uncommittedEvents; + } + + public void ClearUncommittedEvents() + { + _uncommittedEvents.Clear(); + } + + protected void Add(IEvent evnt) + { + _uncommittedEvents.Add(evnt); + } + + public abstract void Load(IEnumerable events); + } +} \ No newline at end of file diff --git a/AggregateTests.cs b/AggregateTests.cs new file mode 100644 index 0000000..5c0173e --- /dev/null +++ b/AggregateTests.cs @@ -0,0 +1,50 @@ +using System; +using System.Linq; +using Shouldly; + +namespace EventSourcing.Demo +{ + public abstract class AggregateTests where TAggregate : AggregateRoot + { + private readonly TAggregate _aggregateRoot; + + protected AggregateTests(TAggregate aggregateRoot) + { + _aggregateRoot = aggregateRoot; + } + + protected void Given(params IEvent[] events) + { + if (events != null) + { + _aggregateRoot.Load(events); + } + } + + protected void When(Action command) + { + command(_aggregateRoot); + } + + protected void Then(params Action[] conditions) + { + var events = _aggregateRoot.GetUncommittedEvents(); + events.Count.ShouldBe(1); + var evnt = events.First(); + evnt.ShouldBeOfType(); + if (conditions != null) + { + ((TEvent)evnt).ShouldSatisfyAllConditions(conditions); + } + } + + protected void Throws(Action command, params Action[] conditions) where TException : Exception + { + var ex = Should.Throw(() => command(_aggregateRoot)); + if (conditions != null) + { + ex.ShouldSatisfyAllConditions(conditions); + } + } + } +} \ No newline at end of file diff --git a/EventSourcing.Demo.csproj b/EventSourcing.Demo.csproj new file mode 100644 index 0000000..1b30db0 --- /dev/null +++ b/EventSourcing.Demo.csproj @@ -0,0 +1,14 @@ + + + + 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/Folder.DotSettings b/Folder.DotSettings new file mode 100644 index 0000000..408a942 --- /dev/null +++ b/Folder.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/README.md b/README.md index 710ceea..65c5cde 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -BusinessRules +EventSourcingTesting =============== diff --git a/WarehouseProduct.cs b/WarehouseProduct.cs new file mode 100644 index 0000000..f403796 --- /dev/null +++ b/WarehouseProduct.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Threading; + +namespace EventSourcing.Demo +{ + public class WarehouseProductState + { + public int QuantityOnHand { get; set; } + } + + public record Quantity + { + private int Value { get; } + + public Quantity(int value) + { + if (value <= 0) + { + throw new InvalidOperationException("Quantity must be greater than zero."); + } + + Value = value; + } + + public static implicit operator int(Quantity quantity) => quantity.Value; + } + + public class WarehouseProduct : AggregateRoot + { + public string Sku { get; } + + private readonly WarehouseProductState _warehouseProductState = new(); + + public WarehouseProduct(string sku) + { + Sku = sku; + } + + public override void Load(IEnumerable events) + { + foreach (var evnt in events) + { + Apply(evnt as dynamic); + } + } + + public static WarehouseProduct Load(string sku, IEnumerable events) + { + var warehouseProduct = new WarehouseProduct(sku); + warehouseProduct.Load(events); + return warehouseProduct; + } + + public void ShipProduct(Quantity quantity) + { + // Business Rule + if (quantity > _warehouseProductState.QuantityOnHand) + { + throw new InvalidDomainException("Cannot Ship to a negative Quantity on Hand."); + } + + var productShipped = new ProductShipped(Sku, quantity, DateTime.UtcNow); + + Apply(productShipped); + Add(productShipped); + } + + private void Apply(ProductShipped evnt) + { + _warehouseProductState.QuantityOnHand -= evnt.Quantity; + } + + public void ReceiveProduct(Quantity quantity) + { + var productReceived = new ProductReceived(Sku, quantity, DateTime.UtcNow); + + Apply(productReceived); + Add(productReceived); + } + + private void Apply(ProductReceived evnt) + { + _warehouseProductState.QuantityOnHand += evnt.Quantity; + } + + public void AdjustInventory(int quantity, string reason) + { + if (_warehouseProductState.QuantityOnHand + quantity < 0) + { + throw new InvalidDomainException("Cannot adjust to a negative Quantity on Hand."); + } + + var inventoryAdjusted = new InventoryAdjusted(Sku, quantity, reason, DateTime.UtcNow); + + Apply(inventoryAdjusted); + Add(inventoryAdjusted); + } + + private void Apply(InventoryAdjusted evnt) + { + _warehouseProductState.QuantityOnHand += evnt.Quantity; + } + + public WarehouseProductState GetState() + { + return _warehouseProductState; + } + + 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/AggregateRoot.cs b/AggregateRoot.cs new file mode 100644 index 0000000..68b0153 --- /dev/null +++ b/AggregateRoot.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace EventSourcing.Demo +{ + public abstract class AggregateRoot + { + private readonly IList _uncommittedEvents = new List(); + + public IList GetUncommittedEvents() + { + return _uncommittedEvents; + } + + public void ClearUncommittedEvents() + { + _uncommittedEvents.Clear(); + } + + protected void Add(IEvent evnt) + { + _uncommittedEvents.Add(evnt); + } + + public abstract void Load(IEnumerable events); + } +} \ No newline at end of file diff --git a/AggregateTests.cs b/AggregateTests.cs new file mode 100644 index 0000000..5c0173e --- /dev/null +++ b/AggregateTests.cs @@ -0,0 +1,50 @@ +using System; +using System.Linq; +using Shouldly; + +namespace EventSourcing.Demo +{ + public abstract class AggregateTests where TAggregate : AggregateRoot + { + private readonly TAggregate _aggregateRoot; + + protected AggregateTests(TAggregate aggregateRoot) + { + _aggregateRoot = aggregateRoot; + } + + protected void Given(params IEvent[] events) + { + if (events != null) + { + _aggregateRoot.Load(events); + } + } + + protected void When(Action command) + { + command(_aggregateRoot); + } + + protected void Then(params Action[] conditions) + { + var events = _aggregateRoot.GetUncommittedEvents(); + events.Count.ShouldBe(1); + var evnt = events.First(); + evnt.ShouldBeOfType(); + if (conditions != null) + { + ((TEvent)evnt).ShouldSatisfyAllConditions(conditions); + } + } + + protected void Throws(Action command, params Action[] conditions) where TException : Exception + { + var ex = Should.Throw(() => command(_aggregateRoot)); + if (conditions != null) + { + ex.ShouldSatisfyAllConditions(conditions); + } + } + } +} \ No newline at end of file diff --git a/EventSourcing.Demo.csproj b/EventSourcing.Demo.csproj new file mode 100644 index 0000000..1b30db0 --- /dev/null +++ b/EventSourcing.Demo.csproj @@ -0,0 +1,14 @@ + + + + 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/Folder.DotSettings b/Folder.DotSettings new file mode 100644 index 0000000..408a942 --- /dev/null +++ b/Folder.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/README.md b/README.md index 710ceea..65c5cde 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -BusinessRules +EventSourcingTesting =============== diff --git a/WarehouseProduct.cs b/WarehouseProduct.cs new file mode 100644 index 0000000..f403796 --- /dev/null +++ b/WarehouseProduct.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Threading; + +namespace EventSourcing.Demo +{ + public class WarehouseProductState + { + public int QuantityOnHand { get; set; } + } + + public record Quantity + { + private int Value { get; } + + public Quantity(int value) + { + if (value <= 0) + { + throw new InvalidOperationException("Quantity must be greater than zero."); + } + + Value = value; + } + + public static implicit operator int(Quantity quantity) => quantity.Value; + } + + public class WarehouseProduct : AggregateRoot + { + public string Sku { get; } + + private readonly WarehouseProductState _warehouseProductState = new(); + + public WarehouseProduct(string sku) + { + Sku = sku; + } + + public override void Load(IEnumerable events) + { + foreach (var evnt in events) + { + Apply(evnt as dynamic); + } + } + + public static WarehouseProduct Load(string sku, IEnumerable events) + { + var warehouseProduct = new WarehouseProduct(sku); + warehouseProduct.Load(events); + return warehouseProduct; + } + + public void ShipProduct(Quantity quantity) + { + // Business Rule + if (quantity > _warehouseProductState.QuantityOnHand) + { + throw new InvalidDomainException("Cannot Ship to a negative Quantity on Hand."); + } + + var productShipped = new ProductShipped(Sku, quantity, DateTime.UtcNow); + + Apply(productShipped); + Add(productShipped); + } + + private void Apply(ProductShipped evnt) + { + _warehouseProductState.QuantityOnHand -= evnt.Quantity; + } + + public void ReceiveProduct(Quantity quantity) + { + var productReceived = new ProductReceived(Sku, quantity, DateTime.UtcNow); + + Apply(productReceived); + Add(productReceived); + } + + private void Apply(ProductReceived evnt) + { + _warehouseProductState.QuantityOnHand += evnt.Quantity; + } + + public void AdjustInventory(int quantity, string reason) + { + if (_warehouseProductState.QuantityOnHand + quantity < 0) + { + throw new InvalidDomainException("Cannot adjust to a negative Quantity on Hand."); + } + + var inventoryAdjusted = new InventoryAdjusted(Sku, quantity, reason, DateTime.UtcNow); + + Apply(inventoryAdjusted); + Add(inventoryAdjusted); + } + + private void Apply(InventoryAdjusted evnt) + { + _warehouseProductState.QuantityOnHand += evnt.Quantity; + } + + public WarehouseProductState GetState() + { + return _warehouseProductState; + } + + 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/WarehouseProductAggregateRootTests.cs b/WarehouseProductAggregateRootTests.cs new file mode 100644 index 0000000..1c13d4c --- /dev/null +++ b/WarehouseProductAggregateRootTests.cs @@ -0,0 +1,88 @@ +using System; +using AutoFixture; +using Shouldly; +using Xunit; + +namespace EventSourcing.Demo +{ + public class WarehouseProductAggregateRootTests : AggregateTests + { + private readonly Fixture _fixture; + private readonly string _sku = "abc123"; + private readonly int _initialQuantity; + + public WarehouseProductAggregateRootTests() : base(new WarehouseProduct("abc123")) + { + _fixture = new Fixture(); + _fixture.Customizations.Add(new Int32SequenceGenerator()); + _initialQuantity = (int)_fixture.Create(); + } + + [Fact] + public void ShipProductShouldRaiseProductShipped() + { + Given(new ProductReceived(_sku, _initialQuantity, DateTime.UtcNow)); + + var quantityToShip = _fixture.Create(); + When(x => x.ShipProduct(new Quantity(quantityToShip))); + + Then( + x => x.Quantity.ShouldBe(quantityToShip), + x => x.Sku.ShouldBe(_sku), + x => x.EventType.ShouldBe("ProductShipped")); + } + + [Fact] + public void ShipProductShouldThrowIfNoQuantityOnHand() + { + Given(); + + Throws( + x => x.ShipProduct(new Quantity(1)), + x => x.Message.ShouldBe("Cannot Ship to a negative Quantity on Hand.")); + } + + [Fact] + public void ReceiveProductShouldRaiseProductReceived() + { + Given(new ProductReceived(_sku, _initialQuantity, DateTime.UtcNow)); + + var quantityToReceive = _fixture.Create(); + When(x => x.ReceiveProduct(new Quantity(quantityToReceive))); + + Then( + x => x.Quantity.ShouldBe(quantityToReceive), + x => x.Sku.ShouldBe(_sku), + x => x.EventType.ShouldBe("ProductReceived")); + } + + [Fact] + public void AdjustInventoryShouldRaiseProductAdjusted() + { + Given(new ProductReceived(_sku, _initialQuantity, DateTime.UtcNow)); + + var quantityToAdjust = _fixture.Create(); + var reason = _fixture.Create(); + + When(x => x.AdjustInventory(quantityToAdjust, reason)); + + Then( + x => x.Quantity.ShouldBe(quantityToAdjust), + x => x.Sku.ShouldBe(_sku), + x => x.Reason.ShouldBe(reason), + x => x.EventType.ShouldBe("InventoryAdjusted")); + } + + [Fact] + public void AdjustInventoryShouldThrowIfNoQuantityOnHand() + { + Given(); + + var reason = _fixture.Create(); + + Throws( + x => x.AdjustInventory(-1, reason), + x => x.Message.ShouldBe("Cannot adjust to a negative Quantity on Hand.")); + } + } +} \ 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/AggregateRoot.cs b/AggregateRoot.cs new file mode 100644 index 0000000..68b0153 --- /dev/null +++ b/AggregateRoot.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace EventSourcing.Demo +{ + public abstract class AggregateRoot + { + private readonly IList _uncommittedEvents = new List(); + + public IList GetUncommittedEvents() + { + return _uncommittedEvents; + } + + public void ClearUncommittedEvents() + { + _uncommittedEvents.Clear(); + } + + protected void Add(IEvent evnt) + { + _uncommittedEvents.Add(evnt); + } + + public abstract void Load(IEnumerable events); + } +} \ No newline at end of file diff --git a/AggregateTests.cs b/AggregateTests.cs new file mode 100644 index 0000000..5c0173e --- /dev/null +++ b/AggregateTests.cs @@ -0,0 +1,50 @@ +using System; +using System.Linq; +using Shouldly; + +namespace EventSourcing.Demo +{ + public abstract class AggregateTests where TAggregate : AggregateRoot + { + private readonly TAggregate _aggregateRoot; + + protected AggregateTests(TAggregate aggregateRoot) + { + _aggregateRoot = aggregateRoot; + } + + protected void Given(params IEvent[] events) + { + if (events != null) + { + _aggregateRoot.Load(events); + } + } + + protected void When(Action command) + { + command(_aggregateRoot); + } + + protected void Then(params Action[] conditions) + { + var events = _aggregateRoot.GetUncommittedEvents(); + events.Count.ShouldBe(1); + var evnt = events.First(); + evnt.ShouldBeOfType(); + if (conditions != null) + { + ((TEvent)evnt).ShouldSatisfyAllConditions(conditions); + } + } + + protected void Throws(Action command, params Action[] conditions) where TException : Exception + { + var ex = Should.Throw(() => command(_aggregateRoot)); + if (conditions != null) + { + ex.ShouldSatisfyAllConditions(conditions); + } + } + } +} \ No newline at end of file diff --git a/EventSourcing.Demo.csproj b/EventSourcing.Demo.csproj new file mode 100644 index 0000000..1b30db0 --- /dev/null +++ b/EventSourcing.Demo.csproj @@ -0,0 +1,14 @@ + + + + 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/Folder.DotSettings b/Folder.DotSettings new file mode 100644 index 0000000..408a942 --- /dev/null +++ b/Folder.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/README.md b/README.md index 710ceea..65c5cde 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -BusinessRules +EventSourcingTesting =============== diff --git a/WarehouseProduct.cs b/WarehouseProduct.cs new file mode 100644 index 0000000..f403796 --- /dev/null +++ b/WarehouseProduct.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Threading; + +namespace EventSourcing.Demo +{ + public class WarehouseProductState + { + public int QuantityOnHand { get; set; } + } + + public record Quantity + { + private int Value { get; } + + public Quantity(int value) + { + if (value <= 0) + { + throw new InvalidOperationException("Quantity must be greater than zero."); + } + + Value = value; + } + + public static implicit operator int(Quantity quantity) => quantity.Value; + } + + public class WarehouseProduct : AggregateRoot + { + public string Sku { get; } + + private readonly WarehouseProductState _warehouseProductState = new(); + + public WarehouseProduct(string sku) + { + Sku = sku; + } + + public override void Load(IEnumerable events) + { + foreach (var evnt in events) + { + Apply(evnt as dynamic); + } + } + + public static WarehouseProduct Load(string sku, IEnumerable events) + { + var warehouseProduct = new WarehouseProduct(sku); + warehouseProduct.Load(events); + return warehouseProduct; + } + + public void ShipProduct(Quantity quantity) + { + // Business Rule + if (quantity > _warehouseProductState.QuantityOnHand) + { + throw new InvalidDomainException("Cannot Ship to a negative Quantity on Hand."); + } + + var productShipped = new ProductShipped(Sku, quantity, DateTime.UtcNow); + + Apply(productShipped); + Add(productShipped); + } + + private void Apply(ProductShipped evnt) + { + _warehouseProductState.QuantityOnHand -= evnt.Quantity; + } + + public void ReceiveProduct(Quantity quantity) + { + var productReceived = new ProductReceived(Sku, quantity, DateTime.UtcNow); + + Apply(productReceived); + Add(productReceived); + } + + private void Apply(ProductReceived evnt) + { + _warehouseProductState.QuantityOnHand += evnt.Quantity; + } + + public void AdjustInventory(int quantity, string reason) + { + if (_warehouseProductState.QuantityOnHand + quantity < 0) + { + throw new InvalidDomainException("Cannot adjust to a negative Quantity on Hand."); + } + + var inventoryAdjusted = new InventoryAdjusted(Sku, quantity, reason, DateTime.UtcNow); + + Apply(inventoryAdjusted); + Add(inventoryAdjusted); + } + + private void Apply(InventoryAdjusted evnt) + { + _warehouseProductState.QuantityOnHand += evnt.Quantity; + } + + public WarehouseProductState GetState() + { + return _warehouseProductState; + } + + 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/WarehouseProductAggregateRootTests.cs b/WarehouseProductAggregateRootTests.cs new file mode 100644 index 0000000..1c13d4c --- /dev/null +++ b/WarehouseProductAggregateRootTests.cs @@ -0,0 +1,88 @@ +using System; +using AutoFixture; +using Shouldly; +using Xunit; + +namespace EventSourcing.Demo +{ + public class WarehouseProductAggregateRootTests : AggregateTests + { + private readonly Fixture _fixture; + private readonly string _sku = "abc123"; + private readonly int _initialQuantity; + + public WarehouseProductAggregateRootTests() : base(new WarehouseProduct("abc123")) + { + _fixture = new Fixture(); + _fixture.Customizations.Add(new Int32SequenceGenerator()); + _initialQuantity = (int)_fixture.Create(); + } + + [Fact] + public void ShipProductShouldRaiseProductShipped() + { + Given(new ProductReceived(_sku, _initialQuantity, DateTime.UtcNow)); + + var quantityToShip = _fixture.Create(); + When(x => x.ShipProduct(new Quantity(quantityToShip))); + + Then( + x => x.Quantity.ShouldBe(quantityToShip), + x => x.Sku.ShouldBe(_sku), + x => x.EventType.ShouldBe("ProductShipped")); + } + + [Fact] + public void ShipProductShouldThrowIfNoQuantityOnHand() + { + Given(); + + Throws( + x => x.ShipProduct(new Quantity(1)), + x => x.Message.ShouldBe("Cannot Ship to a negative Quantity on Hand.")); + } + + [Fact] + public void ReceiveProductShouldRaiseProductReceived() + { + Given(new ProductReceived(_sku, _initialQuantity, DateTime.UtcNow)); + + var quantityToReceive = _fixture.Create(); + When(x => x.ReceiveProduct(new Quantity(quantityToReceive))); + + Then( + x => x.Quantity.ShouldBe(quantityToReceive), + x => x.Sku.ShouldBe(_sku), + x => x.EventType.ShouldBe("ProductReceived")); + } + + [Fact] + public void AdjustInventoryShouldRaiseProductAdjusted() + { + Given(new ProductReceived(_sku, _initialQuantity, DateTime.UtcNow)); + + var quantityToAdjust = _fixture.Create(); + var reason = _fixture.Create(); + + When(x => x.AdjustInventory(quantityToAdjust, reason)); + + Then( + x => x.Quantity.ShouldBe(quantityToAdjust), + x => x.Sku.ShouldBe(_sku), + x => x.Reason.ShouldBe(reason), + x => x.EventType.ShouldBe("InventoryAdjusted")); + } + + [Fact] + public void AdjustInventoryShouldThrowIfNoQuantityOnHand() + { + Given(); + + var reason = _fixture.Create(); + + Throws( + x => x.AdjustInventory(-1, reason), + x => x.Message.ShouldBe("Cannot adjust to a negative Quantity on Hand.")); + } + } +} \ No newline at end of file diff --git a/WarehouseProductEntity.cs b/WarehouseProductEntity.cs new file mode 100644 index 0000000..255f7b0 --- /dev/null +++ b/WarehouseProductEntity.cs @@ -0,0 +1,46 @@ +using System; + +namespace EventSourcing.Demo +{ + public class WarehouseProductEntity + { + public string Sku { get; set; } + public int QuantityOnHand { get; set; } + public DateTime Shipped { get; set; } + public DateTime Received { get; set; } + + private readonly WarehouseProductState _warehouseProductState = new(); + + public WarehouseProductEntity(string sku) + { + Sku = sku; + } + + public void ShipProduct(int quantity) + { + if (quantity > _warehouseProductState.QuantityOnHand) + { + throw new InvalidDomainException("Cannot Ship to a negative Quantity on Hand."); + } + + Shipped = DateTime.UtcNow; + QuantityOnHand -= quantity; + } + + public void ReceiveProduct(int quantity) + { + Received = DateTime.UtcNow; + QuantityOnHand += quantity; + } + + public void AdjustInventory(int quantity) + { + if (_warehouseProductState.QuantityOnHand + quantity < 0) + { + throw new InvalidDomainException("Cannot adjust to a negative Quantity on Hand."); + } + + QuantityOnHand += quantity; + } + } +} \ 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/AggregateRoot.cs b/AggregateRoot.cs new file mode 100644 index 0000000..68b0153 --- /dev/null +++ b/AggregateRoot.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace EventSourcing.Demo +{ + public abstract class AggregateRoot + { + private readonly IList _uncommittedEvents = new List(); + + public IList GetUncommittedEvents() + { + return _uncommittedEvents; + } + + public void ClearUncommittedEvents() + { + _uncommittedEvents.Clear(); + } + + protected void Add(IEvent evnt) + { + _uncommittedEvents.Add(evnt); + } + + public abstract void Load(IEnumerable events); + } +} \ No newline at end of file diff --git a/AggregateTests.cs b/AggregateTests.cs new file mode 100644 index 0000000..5c0173e --- /dev/null +++ b/AggregateTests.cs @@ -0,0 +1,50 @@ +using System; +using System.Linq; +using Shouldly; + +namespace EventSourcing.Demo +{ + public abstract class AggregateTests where TAggregate : AggregateRoot + { + private readonly TAggregate _aggregateRoot; + + protected AggregateTests(TAggregate aggregateRoot) + { + _aggregateRoot = aggregateRoot; + } + + protected void Given(params IEvent[] events) + { + if (events != null) + { + _aggregateRoot.Load(events); + } + } + + protected void When(Action command) + { + command(_aggregateRoot); + } + + protected void Then(params Action[] conditions) + { + var events = _aggregateRoot.GetUncommittedEvents(); + events.Count.ShouldBe(1); + var evnt = events.First(); + evnt.ShouldBeOfType(); + if (conditions != null) + { + ((TEvent)evnt).ShouldSatisfyAllConditions(conditions); + } + } + + protected void Throws(Action command, params Action[] conditions) where TException : Exception + { + var ex = Should.Throw(() => command(_aggregateRoot)); + if (conditions != null) + { + ex.ShouldSatisfyAllConditions(conditions); + } + } + } +} \ No newline at end of file diff --git a/EventSourcing.Demo.csproj b/EventSourcing.Demo.csproj new file mode 100644 index 0000000..1b30db0 --- /dev/null +++ b/EventSourcing.Demo.csproj @@ -0,0 +1,14 @@ + + + + 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/Folder.DotSettings b/Folder.DotSettings new file mode 100644 index 0000000..408a942 --- /dev/null +++ b/Folder.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/README.md b/README.md index 710ceea..65c5cde 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -BusinessRules +EventSourcingTesting =============== diff --git a/WarehouseProduct.cs b/WarehouseProduct.cs new file mode 100644 index 0000000..f403796 --- /dev/null +++ b/WarehouseProduct.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Threading; + +namespace EventSourcing.Demo +{ + public class WarehouseProductState + { + public int QuantityOnHand { get; set; } + } + + public record Quantity + { + private int Value { get; } + + public Quantity(int value) + { + if (value <= 0) + { + throw new InvalidOperationException("Quantity must be greater than zero."); + } + + Value = value; + } + + public static implicit operator int(Quantity quantity) => quantity.Value; + } + + public class WarehouseProduct : AggregateRoot + { + public string Sku { get; } + + private readonly WarehouseProductState _warehouseProductState = new(); + + public WarehouseProduct(string sku) + { + Sku = sku; + } + + public override void Load(IEnumerable events) + { + foreach (var evnt in events) + { + Apply(evnt as dynamic); + } + } + + public static WarehouseProduct Load(string sku, IEnumerable events) + { + var warehouseProduct = new WarehouseProduct(sku); + warehouseProduct.Load(events); + return warehouseProduct; + } + + public void ShipProduct(Quantity quantity) + { + // Business Rule + if (quantity > _warehouseProductState.QuantityOnHand) + { + throw new InvalidDomainException("Cannot Ship to a negative Quantity on Hand."); + } + + var productShipped = new ProductShipped(Sku, quantity, DateTime.UtcNow); + + Apply(productShipped); + Add(productShipped); + } + + private void Apply(ProductShipped evnt) + { + _warehouseProductState.QuantityOnHand -= evnt.Quantity; + } + + public void ReceiveProduct(Quantity quantity) + { + var productReceived = new ProductReceived(Sku, quantity, DateTime.UtcNow); + + Apply(productReceived); + Add(productReceived); + } + + private void Apply(ProductReceived evnt) + { + _warehouseProductState.QuantityOnHand += evnt.Quantity; + } + + public void AdjustInventory(int quantity, string reason) + { + if (_warehouseProductState.QuantityOnHand + quantity < 0) + { + throw new InvalidDomainException("Cannot adjust to a negative Quantity on Hand."); + } + + var inventoryAdjusted = new InventoryAdjusted(Sku, quantity, reason, DateTime.UtcNow); + + Apply(inventoryAdjusted); + Add(inventoryAdjusted); + } + + private void Apply(InventoryAdjusted evnt) + { + _warehouseProductState.QuantityOnHand += evnt.Quantity; + } + + public WarehouseProductState GetState() + { + return _warehouseProductState; + } + + 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/WarehouseProductAggregateRootTests.cs b/WarehouseProductAggregateRootTests.cs new file mode 100644 index 0000000..1c13d4c --- /dev/null +++ b/WarehouseProductAggregateRootTests.cs @@ -0,0 +1,88 @@ +using System; +using AutoFixture; +using Shouldly; +using Xunit; + +namespace EventSourcing.Demo +{ + public class WarehouseProductAggregateRootTests : AggregateTests + { + private readonly Fixture _fixture; + private readonly string _sku = "abc123"; + private readonly int _initialQuantity; + + public WarehouseProductAggregateRootTests() : base(new WarehouseProduct("abc123")) + { + _fixture = new Fixture(); + _fixture.Customizations.Add(new Int32SequenceGenerator()); + _initialQuantity = (int)_fixture.Create(); + } + + [Fact] + public void ShipProductShouldRaiseProductShipped() + { + Given(new ProductReceived(_sku, _initialQuantity, DateTime.UtcNow)); + + var quantityToShip = _fixture.Create(); + When(x => x.ShipProduct(new Quantity(quantityToShip))); + + Then( + x => x.Quantity.ShouldBe(quantityToShip), + x => x.Sku.ShouldBe(_sku), + x => x.EventType.ShouldBe("ProductShipped")); + } + + [Fact] + public void ShipProductShouldThrowIfNoQuantityOnHand() + { + Given(); + + Throws( + x => x.ShipProduct(new Quantity(1)), + x => x.Message.ShouldBe("Cannot Ship to a negative Quantity on Hand.")); + } + + [Fact] + public void ReceiveProductShouldRaiseProductReceived() + { + Given(new ProductReceived(_sku, _initialQuantity, DateTime.UtcNow)); + + var quantityToReceive = _fixture.Create(); + When(x => x.ReceiveProduct(new Quantity(quantityToReceive))); + + Then( + x => x.Quantity.ShouldBe(quantityToReceive), + x => x.Sku.ShouldBe(_sku), + x => x.EventType.ShouldBe("ProductReceived")); + } + + [Fact] + public void AdjustInventoryShouldRaiseProductAdjusted() + { + Given(new ProductReceived(_sku, _initialQuantity, DateTime.UtcNow)); + + var quantityToAdjust = _fixture.Create(); + var reason = _fixture.Create(); + + When(x => x.AdjustInventory(quantityToAdjust, reason)); + + Then( + x => x.Quantity.ShouldBe(quantityToAdjust), + x => x.Sku.ShouldBe(_sku), + x => x.Reason.ShouldBe(reason), + x => x.EventType.ShouldBe("InventoryAdjusted")); + } + + [Fact] + public void AdjustInventoryShouldThrowIfNoQuantityOnHand() + { + Given(); + + var reason = _fixture.Create(); + + Throws( + x => x.AdjustInventory(-1, reason), + x => x.Message.ShouldBe("Cannot adjust to a negative Quantity on Hand.")); + } + } +} \ No newline at end of file diff --git a/WarehouseProductEntity.cs b/WarehouseProductEntity.cs new file mode 100644 index 0000000..255f7b0 --- /dev/null +++ b/WarehouseProductEntity.cs @@ -0,0 +1,46 @@ +using System; + +namespace EventSourcing.Demo +{ + public class WarehouseProductEntity + { + public string Sku { get; set; } + public int QuantityOnHand { get; set; } + public DateTime Shipped { get; set; } + public DateTime Received { get; set; } + + private readonly WarehouseProductState _warehouseProductState = new(); + + public WarehouseProductEntity(string sku) + { + Sku = sku; + } + + public void ShipProduct(int quantity) + { + if (quantity > _warehouseProductState.QuantityOnHand) + { + throw new InvalidDomainException("Cannot Ship to a negative Quantity on Hand."); + } + + Shipped = DateTime.UtcNow; + QuantityOnHand -= quantity; + } + + public void ReceiveProduct(int quantity) + { + Received = DateTime.UtcNow; + QuantityOnHand += quantity; + } + + public void AdjustInventory(int quantity) + { + if (_warehouseProductState.QuantityOnHand + quantity < 0) + { + throw new InvalidDomainException("Cannot adjust to a negative Quantity on Hand."); + } + + QuantityOnHand += quantity; + } + } +} \ No newline at end of file diff --git a/WarehouseProductTests.cs b/WarehouseProductTests.cs new file mode 100644 index 0000000..42a2373 --- /dev/null +++ b/WarehouseProductTests.cs @@ -0,0 +1,109 @@ +using System; +using System.Linq; +using AutoFixture; +using Shouldly; +using Xunit; + +namespace EventSourcing.Demo +{ + public class WarehouseProductTests + { + private readonly string _sku; + private readonly int _initialQuantity; + private readonly WarehouseProduct _sut; + private readonly Fixture _fixture; + + public WarehouseProductTests() + { + _fixture = new Fixture(); + _fixture.Customizations.Add(new Int32SequenceGenerator()); + _sku = _fixture.Create(); + _initialQuantity = (int)_fixture.Create(); + + _sut = WarehouseProduct.Load(_sku, new [] { + new ProductReceived(_sku, _initialQuantity, DateTime.UtcNow) + }); + } + + [Fact] + public void ShipProductShouldRaiseProductShipped() + { + var quantityToShip = _fixture.Create(); + _sut.ShipProduct(new Quantity(quantityToShip)); + + var outEvents = _sut.GetUncommittedEvents(); + outEvents.Count.ShouldBe(1); + var outEvent = outEvents.Single(); + outEvent.ShouldBeOfType(); + + var productShipped = (ProductShipped)outEvent; + productShipped.ShouldSatisfyAllConditions( + x => x.Quantity.ShouldBe(quantityToShip), + x => x.Sku.ShouldBe(_sku), + x => x.EventType.ShouldBe("ProductShipped") + ); + } + + + + [Fact] + public void ShipProductShouldThrowIfNoQuantityOnHand() + { + var ex = Should.Throw(() => _sut.ShipProduct(new Quantity(_initialQuantity + 1))); + ex.Message.ShouldBe("Cannot Ship to a negative Quantity on Hand."); + } + + [Fact] + public void ReceiveProductShouldRaiseProductReceived() + { + var quantityToReceive = _fixture.Create(); + _sut.ReceiveProduct(new Quantity(quantityToReceive)); + + var outEvents = _sut.GetUncommittedEvents(); + outEvents.Count.ShouldBe(1); + var outEvent = outEvents.Single(); + outEvent.ShouldBeOfType(); + + var productReceived = (ProductReceived)outEvent; + productReceived.ShouldSatisfyAllConditions( + x => x.Quantity.ShouldBe(quantityToReceive), + x => x.Sku.ShouldBe(_sku), + x => x.EventType.ShouldBe("ProductReceived") + ); + } + + [Fact] + public void AdjustInventoryShouldRaiseProductAdjusted() + { + var quantityToAdjust = _fixture.Create(); + var reason = _fixture.Create(); + _sut.AdjustInventory(new Quantity(quantityToAdjust), reason); + + var outEvents = _sut.GetUncommittedEvents(); + outEvents.Count.ShouldBe(1); + var outEvent = outEvents.Single(); + outEvent.ShouldBeOfType(); + + var productShipped = (InventoryAdjusted)outEvent; + productShipped.ShouldSatisfyAllConditions( + x => x.Quantity.ShouldBe(quantityToAdjust), + x => x.Sku.ShouldBe(_sku), + x => x.Reason.ShouldBe(reason), + x => x.EventType.ShouldBe("InventoryAdjusted") + ); + } + + [Fact] + public void AdjustInventoryShouldThrowIfNoQuantityOnHand() + { + var ex = Should.Throw(() => _sut.AdjustInventory(new Quantity((_initialQuantity + 1) * -1), string.Empty)); + ex.Message.ShouldBe("Cannot adjust to a negative Quantity on Hand."); + } + + [Fact] + public void ThrowInvalidQuantity() + { + Should.Throw(() => new Quantity(0)); + } + } +} \ No newline at end of file