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/ReservationPattern.sln b/ReservationPattern.sln new file mode 100644 index 0000000..9aa461c --- /dev/null +++ b/ReservationPattern.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReservationSaga", "ReservationSaga\ReservationSaga.csproj", "{9C666269-78A0-4369-AF47-7D3600F5540F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReserveSync", "ReserveSync\ReserveSync.csproj", "{B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.Build.0 = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal 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/ReservationPattern.sln b/ReservationPattern.sln new file mode 100644 index 0000000..9aa461c --- /dev/null +++ b/ReservationPattern.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReservationSaga", "ReservationSaga\ReservationSaga.csproj", "{9C666269-78A0-4369-AF47-7D3600F5540F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReserveSync", "ReserveSync\ReserveSync.csproj", "{B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.Build.0 = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ReservationSaga/ConfirmUsernameReservation.cs b/ReservationSaga/ConfirmUsernameReservation.cs new file mode 100644 index 0000000..e28aabc --- /dev/null +++ b/ReservationSaga/ConfirmUsernameReservation.cs @@ -0,0 +1,33 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ConfirmUsernameReservation : ICommand +{ + public string Username { get; set; } +} + +public class UsernameRegistered : IEvent +{ + public string Username { get; set; } +} + +public class ConfirmUsernameReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ConfirmUsernameReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public async Task Handle(ConfirmUsernameReservation message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Confirmed Reservation for {message.Username}"); + + if (_usernameReservation.Complete(message.Username)) + { + await context.Publish(new UsernameRegistered { Username = message.Username }); + } + } +} \ 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/ReservationPattern.sln b/ReservationPattern.sln new file mode 100644 index 0000000..9aa461c --- /dev/null +++ b/ReservationPattern.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReservationSaga", "ReservationSaga\ReservationSaga.csproj", "{9C666269-78A0-4369-AF47-7D3600F5540F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReserveSync", "ReserveSync\ReserveSync.csproj", "{B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.Build.0 = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ReservationSaga/ConfirmUsernameReservation.cs b/ReservationSaga/ConfirmUsernameReservation.cs new file mode 100644 index 0000000..e28aabc --- /dev/null +++ b/ReservationSaga/ConfirmUsernameReservation.cs @@ -0,0 +1,33 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ConfirmUsernameReservation : ICommand +{ + public string Username { get; set; } +} + +public class UsernameRegistered : IEvent +{ + public string Username { get; set; } +} + +public class ConfirmUsernameReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ConfirmUsernameReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public async Task Handle(ConfirmUsernameReservation message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Confirmed Reservation for {message.Username}"); + + if (_usernameReservation.Complete(message.Username)) + { + await context.Publish(new UsernameRegistered { Username = message.Username }); + } + } +} \ No newline at end of file diff --git a/ReservationSaga/CreateUserAccount.cs b/ReservationSaga/CreateUserAccount.cs new file mode 100644 index 0000000..a9e6822 --- /dev/null +++ b/ReservationSaga/CreateUserAccount.cs @@ -0,0 +1,23 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class CreateUserAccount : ICommand +{ + public string Username { get; set; } +} + +public class CreateUserAccountHandler : IHandleMessages +{ + public async Task Handle(CreateUserAccount message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Create User Account for {message.Username}"); + + await context.Publish(new UserAccountCreated { Username = message.Username }); + } +} + +public class UserAccountCreated : IEvent +{ + public string Username { get; set; } +} \ 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/ReservationPattern.sln b/ReservationPattern.sln new file mode 100644 index 0000000..9aa461c --- /dev/null +++ b/ReservationPattern.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReservationSaga", "ReservationSaga\ReservationSaga.csproj", "{9C666269-78A0-4369-AF47-7D3600F5540F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReserveSync", "ReserveSync\ReserveSync.csproj", "{B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.Build.0 = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ReservationSaga/ConfirmUsernameReservation.cs b/ReservationSaga/ConfirmUsernameReservation.cs new file mode 100644 index 0000000..e28aabc --- /dev/null +++ b/ReservationSaga/ConfirmUsernameReservation.cs @@ -0,0 +1,33 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ConfirmUsernameReservation : ICommand +{ + public string Username { get; set; } +} + +public class UsernameRegistered : IEvent +{ + public string Username { get; set; } +} + +public class ConfirmUsernameReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ConfirmUsernameReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public async Task Handle(ConfirmUsernameReservation message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Confirmed Reservation for {message.Username}"); + + if (_usernameReservation.Complete(message.Username)) + { + await context.Publish(new UsernameRegistered { Username = message.Username }); + } + } +} \ No newline at end of file diff --git a/ReservationSaga/CreateUserAccount.cs b/ReservationSaga/CreateUserAccount.cs new file mode 100644 index 0000000..a9e6822 --- /dev/null +++ b/ReservationSaga/CreateUserAccount.cs @@ -0,0 +1,23 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class CreateUserAccount : ICommand +{ + public string Username { get; set; } +} + +public class CreateUserAccountHandler : IHandleMessages +{ + public async Task Handle(CreateUserAccount message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Create User Account for {message.Username}"); + + await context.Publish(new UserAccountCreated { Username = message.Username }); + } +} + +public class UserAccountCreated : IEvent +{ + public string Username { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/ExpireReservation.cs b/ReservationSaga/ExpireReservation.cs new file mode 100644 index 0000000..6ce79e9 --- /dev/null +++ b/ReservationSaga/ExpireReservation.cs @@ -0,0 +1,28 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ExpireReservation : ICommand +{ + public string Username { get; set; } +} + +public class ExpireReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ExpireReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public Task Handle(ExpireReservation message, IMessageHandlerContext context) + { + if (_usernameReservation.IsReserved(message.Username)) + { + Console.WriteLine($"Async: Expire Reservation for {message.Username}"); + _usernameReservation.Expire(message.Username); + } + return Task.CompletedTask; + } +} \ 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/ReservationPattern.sln b/ReservationPattern.sln new file mode 100644 index 0000000..9aa461c --- /dev/null +++ b/ReservationPattern.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReservationSaga", "ReservationSaga\ReservationSaga.csproj", "{9C666269-78A0-4369-AF47-7D3600F5540F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReserveSync", "ReserveSync\ReserveSync.csproj", "{B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.Build.0 = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ReservationSaga/ConfirmUsernameReservation.cs b/ReservationSaga/ConfirmUsernameReservation.cs new file mode 100644 index 0000000..e28aabc --- /dev/null +++ b/ReservationSaga/ConfirmUsernameReservation.cs @@ -0,0 +1,33 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ConfirmUsernameReservation : ICommand +{ + public string Username { get; set; } +} + +public class UsernameRegistered : IEvent +{ + public string Username { get; set; } +} + +public class ConfirmUsernameReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ConfirmUsernameReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public async Task Handle(ConfirmUsernameReservation message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Confirmed Reservation for {message.Username}"); + + if (_usernameReservation.Complete(message.Username)) + { + await context.Publish(new UsernameRegistered { Username = message.Username }); + } + } +} \ No newline at end of file diff --git a/ReservationSaga/CreateUserAccount.cs b/ReservationSaga/CreateUserAccount.cs new file mode 100644 index 0000000..a9e6822 --- /dev/null +++ b/ReservationSaga/CreateUserAccount.cs @@ -0,0 +1,23 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class CreateUserAccount : ICommand +{ + public string Username { get; set; } +} + +public class CreateUserAccountHandler : IHandleMessages +{ + public async Task Handle(CreateUserAccount message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Create User Account for {message.Username}"); + + await context.Publish(new UserAccountCreated { Username = message.Username }); + } +} + +public class UserAccountCreated : IEvent +{ + public string Username { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/ExpireReservation.cs b/ReservationSaga/ExpireReservation.cs new file mode 100644 index 0000000..6ce79e9 --- /dev/null +++ b/ReservationSaga/ExpireReservation.cs @@ -0,0 +1,28 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ExpireReservation : ICommand +{ + public string Username { get; set; } +} + +public class ExpireReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ExpireReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public Task Handle(ExpireReservation message, IMessageHandlerContext context) + { + if (_usernameReservation.IsReserved(message.Username)) + { + Console.WriteLine($"Async: Expire Reservation for {message.Username}"); + _usernameReservation.Expire(message.Username); + } + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/ReservationSaga/FakeDatabase.cs b/ReservationSaga/FakeDatabase.cs new file mode 100644 index 0000000..1bfbebd --- /dev/null +++ b/ReservationSaga/FakeDatabase.cs @@ -0,0 +1,48 @@ +namespace Reservation; + +public class FakeDatabase +{ + public List RegisteredUsernames { get; set; } = new(); + public List ReservedUsernames { get; set; } = new(); + public List UserAccounts { get; set; } = new(); +} + +public record Account(string Username); + +public interface IUserRepository +{ + public void Add(Account account); + public void Remove(Account account); + void Save(); +} + +public class UserRepository : IUserRepository +{ + private readonly FakeDatabase _db; + private readonly List _unsaved = new(); + + public UserRepository(FakeDatabase db) + { + _db = db; + } + + public void Add(Account account) + { + _unsaved.Add(account); + } + + public void Remove(Account account) + { + var item = _db.UserAccounts.SingleOrDefault(x => x.Username == account.Username); + if (item != null) + { + _db.UserAccounts.Remove(item); + } + } + + public void Save() + { + _db.UserAccounts.AddRange(_unsaved); + _unsaved.Clear(); + } +} \ 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/ReservationPattern.sln b/ReservationPattern.sln new file mode 100644 index 0000000..9aa461c --- /dev/null +++ b/ReservationPattern.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReservationSaga", "ReservationSaga\ReservationSaga.csproj", "{9C666269-78A0-4369-AF47-7D3600F5540F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReserveSync", "ReserveSync\ReserveSync.csproj", "{B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.Build.0 = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ReservationSaga/ConfirmUsernameReservation.cs b/ReservationSaga/ConfirmUsernameReservation.cs new file mode 100644 index 0000000..e28aabc --- /dev/null +++ b/ReservationSaga/ConfirmUsernameReservation.cs @@ -0,0 +1,33 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ConfirmUsernameReservation : ICommand +{ + public string Username { get; set; } +} + +public class UsernameRegistered : IEvent +{ + public string Username { get; set; } +} + +public class ConfirmUsernameReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ConfirmUsernameReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public async Task Handle(ConfirmUsernameReservation message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Confirmed Reservation for {message.Username}"); + + if (_usernameReservation.Complete(message.Username)) + { + await context.Publish(new UsernameRegistered { Username = message.Username }); + } + } +} \ No newline at end of file diff --git a/ReservationSaga/CreateUserAccount.cs b/ReservationSaga/CreateUserAccount.cs new file mode 100644 index 0000000..a9e6822 --- /dev/null +++ b/ReservationSaga/CreateUserAccount.cs @@ -0,0 +1,23 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class CreateUserAccount : ICommand +{ + public string Username { get; set; } +} + +public class CreateUserAccountHandler : IHandleMessages +{ + public async Task Handle(CreateUserAccount message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Create User Account for {message.Username}"); + + await context.Publish(new UserAccountCreated { Username = message.Username }); + } +} + +public class UserAccountCreated : IEvent +{ + public string Username { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/ExpireReservation.cs b/ReservationSaga/ExpireReservation.cs new file mode 100644 index 0000000..6ce79e9 --- /dev/null +++ b/ReservationSaga/ExpireReservation.cs @@ -0,0 +1,28 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ExpireReservation : ICommand +{ + public string Username { get; set; } +} + +public class ExpireReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ExpireReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public Task Handle(ExpireReservation message, IMessageHandlerContext context) + { + if (_usernameReservation.IsReserved(message.Username)) + { + Console.WriteLine($"Async: Expire Reservation for {message.Username}"); + _usernameReservation.Expire(message.Username); + } + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/ReservationSaga/FakeDatabase.cs b/ReservationSaga/FakeDatabase.cs new file mode 100644 index 0000000..1bfbebd --- /dev/null +++ b/ReservationSaga/FakeDatabase.cs @@ -0,0 +1,48 @@ +namespace Reservation; + +public class FakeDatabase +{ + public List RegisteredUsernames { get; set; } = new(); + public List ReservedUsernames { get; set; } = new(); + public List UserAccounts { get; set; } = new(); +} + +public record Account(string Username); + +public interface IUserRepository +{ + public void Add(Account account); + public void Remove(Account account); + void Save(); +} + +public class UserRepository : IUserRepository +{ + private readonly FakeDatabase _db; + private readonly List _unsaved = new(); + + public UserRepository(FakeDatabase db) + { + _db = db; + } + + public void Add(Account account) + { + _unsaved.Add(account); + } + + public void Remove(Account account) + { + var item = _db.UserAccounts.SingleOrDefault(x => x.Username == account.Username); + if (item != null) + { + _db.UserAccounts.Remove(item); + } + } + + public void Save() + { + _db.UserAccounts.AddRange(_unsaved); + _unsaved.Clear(); + } +} \ No newline at end of file diff --git a/ReservationSaga/Program.cs b/ReservationSaga/Program.cs new file mode 100644 index 0000000..94ac295 --- /dev/null +++ b/ReservationSaga/Program.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NServiceBus; +using Reservation; +using ReservationSaga; + +static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseNServiceBus(context => + { + var endpointConfiguration = new EndpointConfiguration("Demo"); + var transport = endpointConfiguration.UseTransport(); + var persistence = endpointConfiguration.UsePersistence(); + + var routing = transport.Routing(); + + routing.RouteToEndpoint( + assembly: typeof(UserRegistration).Assembly, + destination: "Demo"); + + return endpointConfiguration; + }) + .ConfigureServices(services => + { + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + }); + +var host = CreateHostBuilder(args).Build(); +await host.StartAsync(); + +var session = host.Services.GetRequiredService(); +var usernameReservation = host.Services.GetRequiredService(); + +var username = string.Empty; +while (username != "q") +{ + Console.Write("Username: "); + username = Console.ReadLine(); + if (usernameReservation.IsAvailable(username)) + { + await session.Publish(new UserRegistrationStarted { Username = username }); + } + else + { + Console.WriteLine("Username already exists."); + } + Console.ReadLine(); +} \ 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/ReservationPattern.sln b/ReservationPattern.sln new file mode 100644 index 0000000..9aa461c --- /dev/null +++ b/ReservationPattern.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReservationSaga", "ReservationSaga\ReservationSaga.csproj", "{9C666269-78A0-4369-AF47-7D3600F5540F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReserveSync", "ReserveSync\ReserveSync.csproj", "{B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.Build.0 = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ReservationSaga/ConfirmUsernameReservation.cs b/ReservationSaga/ConfirmUsernameReservation.cs new file mode 100644 index 0000000..e28aabc --- /dev/null +++ b/ReservationSaga/ConfirmUsernameReservation.cs @@ -0,0 +1,33 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ConfirmUsernameReservation : ICommand +{ + public string Username { get; set; } +} + +public class UsernameRegistered : IEvent +{ + public string Username { get; set; } +} + +public class ConfirmUsernameReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ConfirmUsernameReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public async Task Handle(ConfirmUsernameReservation message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Confirmed Reservation for {message.Username}"); + + if (_usernameReservation.Complete(message.Username)) + { + await context.Publish(new UsernameRegistered { Username = message.Username }); + } + } +} \ No newline at end of file diff --git a/ReservationSaga/CreateUserAccount.cs b/ReservationSaga/CreateUserAccount.cs new file mode 100644 index 0000000..a9e6822 --- /dev/null +++ b/ReservationSaga/CreateUserAccount.cs @@ -0,0 +1,23 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class CreateUserAccount : ICommand +{ + public string Username { get; set; } +} + +public class CreateUserAccountHandler : IHandleMessages +{ + public async Task Handle(CreateUserAccount message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Create User Account for {message.Username}"); + + await context.Publish(new UserAccountCreated { Username = message.Username }); + } +} + +public class UserAccountCreated : IEvent +{ + public string Username { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/ExpireReservation.cs b/ReservationSaga/ExpireReservation.cs new file mode 100644 index 0000000..6ce79e9 --- /dev/null +++ b/ReservationSaga/ExpireReservation.cs @@ -0,0 +1,28 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ExpireReservation : ICommand +{ + public string Username { get; set; } +} + +public class ExpireReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ExpireReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public Task Handle(ExpireReservation message, IMessageHandlerContext context) + { + if (_usernameReservation.IsReserved(message.Username)) + { + Console.WriteLine($"Async: Expire Reservation for {message.Username}"); + _usernameReservation.Expire(message.Username); + } + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/ReservationSaga/FakeDatabase.cs b/ReservationSaga/FakeDatabase.cs new file mode 100644 index 0000000..1bfbebd --- /dev/null +++ b/ReservationSaga/FakeDatabase.cs @@ -0,0 +1,48 @@ +namespace Reservation; + +public class FakeDatabase +{ + public List RegisteredUsernames { get; set; } = new(); + public List ReservedUsernames { get; set; } = new(); + public List UserAccounts { get; set; } = new(); +} + +public record Account(string Username); + +public interface IUserRepository +{ + public void Add(Account account); + public void Remove(Account account); + void Save(); +} + +public class UserRepository : IUserRepository +{ + private readonly FakeDatabase _db; + private readonly List _unsaved = new(); + + public UserRepository(FakeDatabase db) + { + _db = db; + } + + public void Add(Account account) + { + _unsaved.Add(account); + } + + public void Remove(Account account) + { + var item = _db.UserAccounts.SingleOrDefault(x => x.Username == account.Username); + if (item != null) + { + _db.UserAccounts.Remove(item); + } + } + + public void Save() + { + _db.UserAccounts.AddRange(_unsaved); + _unsaved.Clear(); + } +} \ No newline at end of file diff --git a/ReservationSaga/Program.cs b/ReservationSaga/Program.cs new file mode 100644 index 0000000..94ac295 --- /dev/null +++ b/ReservationSaga/Program.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NServiceBus; +using Reservation; +using ReservationSaga; + +static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseNServiceBus(context => + { + var endpointConfiguration = new EndpointConfiguration("Demo"); + var transport = endpointConfiguration.UseTransport(); + var persistence = endpointConfiguration.UsePersistence(); + + var routing = transport.Routing(); + + routing.RouteToEndpoint( + assembly: typeof(UserRegistration).Assembly, + destination: "Demo"); + + return endpointConfiguration; + }) + .ConfigureServices(services => + { + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + }); + +var host = CreateHostBuilder(args).Build(); +await host.StartAsync(); + +var session = host.Services.GetRequiredService(); +var usernameReservation = host.Services.GetRequiredService(); + +var username = string.Empty; +while (username != "q") +{ + Console.Write("Username: "); + username = Console.ReadLine(); + if (usernameReservation.IsAvailable(username)) + { + await session.Publish(new UserRegistrationStarted { Username = username }); + } + else + { + Console.WriteLine("Username already exists."); + } + Console.ReadLine(); +} \ No newline at end of file diff --git a/ReservationSaga/ReservationSaga.csproj b/ReservationSaga/ReservationSaga.csproj new file mode 100644 index 0000000..48682eb --- /dev/null +++ b/ReservationSaga/ReservationSaga.csproj @@ -0,0 +1,15 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + 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/ReservationPattern.sln b/ReservationPattern.sln new file mode 100644 index 0000000..9aa461c --- /dev/null +++ b/ReservationPattern.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReservationSaga", "ReservationSaga\ReservationSaga.csproj", "{9C666269-78A0-4369-AF47-7D3600F5540F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReserveSync", "ReserveSync\ReserveSync.csproj", "{B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.Build.0 = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ReservationSaga/ConfirmUsernameReservation.cs b/ReservationSaga/ConfirmUsernameReservation.cs new file mode 100644 index 0000000..e28aabc --- /dev/null +++ b/ReservationSaga/ConfirmUsernameReservation.cs @@ -0,0 +1,33 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ConfirmUsernameReservation : ICommand +{ + public string Username { get; set; } +} + +public class UsernameRegistered : IEvent +{ + public string Username { get; set; } +} + +public class ConfirmUsernameReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ConfirmUsernameReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public async Task Handle(ConfirmUsernameReservation message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Confirmed Reservation for {message.Username}"); + + if (_usernameReservation.Complete(message.Username)) + { + await context.Publish(new UsernameRegistered { Username = message.Username }); + } + } +} \ No newline at end of file diff --git a/ReservationSaga/CreateUserAccount.cs b/ReservationSaga/CreateUserAccount.cs new file mode 100644 index 0000000..a9e6822 --- /dev/null +++ b/ReservationSaga/CreateUserAccount.cs @@ -0,0 +1,23 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class CreateUserAccount : ICommand +{ + public string Username { get; set; } +} + +public class CreateUserAccountHandler : IHandleMessages +{ + public async Task Handle(CreateUserAccount message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Create User Account for {message.Username}"); + + await context.Publish(new UserAccountCreated { Username = message.Username }); + } +} + +public class UserAccountCreated : IEvent +{ + public string Username { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/ExpireReservation.cs b/ReservationSaga/ExpireReservation.cs new file mode 100644 index 0000000..6ce79e9 --- /dev/null +++ b/ReservationSaga/ExpireReservation.cs @@ -0,0 +1,28 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ExpireReservation : ICommand +{ + public string Username { get; set; } +} + +public class ExpireReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ExpireReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public Task Handle(ExpireReservation message, IMessageHandlerContext context) + { + if (_usernameReservation.IsReserved(message.Username)) + { + Console.WriteLine($"Async: Expire Reservation for {message.Username}"); + _usernameReservation.Expire(message.Username); + } + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/ReservationSaga/FakeDatabase.cs b/ReservationSaga/FakeDatabase.cs new file mode 100644 index 0000000..1bfbebd --- /dev/null +++ b/ReservationSaga/FakeDatabase.cs @@ -0,0 +1,48 @@ +namespace Reservation; + +public class FakeDatabase +{ + public List RegisteredUsernames { get; set; } = new(); + public List ReservedUsernames { get; set; } = new(); + public List UserAccounts { get; set; } = new(); +} + +public record Account(string Username); + +public interface IUserRepository +{ + public void Add(Account account); + public void Remove(Account account); + void Save(); +} + +public class UserRepository : IUserRepository +{ + private readonly FakeDatabase _db; + private readonly List _unsaved = new(); + + public UserRepository(FakeDatabase db) + { + _db = db; + } + + public void Add(Account account) + { + _unsaved.Add(account); + } + + public void Remove(Account account) + { + var item = _db.UserAccounts.SingleOrDefault(x => x.Username == account.Username); + if (item != null) + { + _db.UserAccounts.Remove(item); + } + } + + public void Save() + { + _db.UserAccounts.AddRange(_unsaved); + _unsaved.Clear(); + } +} \ No newline at end of file diff --git a/ReservationSaga/Program.cs b/ReservationSaga/Program.cs new file mode 100644 index 0000000..94ac295 --- /dev/null +++ b/ReservationSaga/Program.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NServiceBus; +using Reservation; +using ReservationSaga; + +static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseNServiceBus(context => + { + var endpointConfiguration = new EndpointConfiguration("Demo"); + var transport = endpointConfiguration.UseTransport(); + var persistence = endpointConfiguration.UsePersistence(); + + var routing = transport.Routing(); + + routing.RouteToEndpoint( + assembly: typeof(UserRegistration).Assembly, + destination: "Demo"); + + return endpointConfiguration; + }) + .ConfigureServices(services => + { + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + }); + +var host = CreateHostBuilder(args).Build(); +await host.StartAsync(); + +var session = host.Services.GetRequiredService(); +var usernameReservation = host.Services.GetRequiredService(); + +var username = string.Empty; +while (username != "q") +{ + Console.Write("Username: "); + username = Console.ReadLine(); + if (usernameReservation.IsAvailable(username)) + { + await session.Publish(new UserRegistrationStarted { Username = username }); + } + else + { + Console.WriteLine("Username already exists."); + } + Console.ReadLine(); +} \ No newline at end of file diff --git a/ReservationSaga/ReservationSaga.csproj b/ReservationSaga/ReservationSaga.csproj new file mode 100644 index 0000000..48682eb --- /dev/null +++ b/ReservationSaga/ReservationSaga.csproj @@ -0,0 +1,15 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + diff --git a/ReservationSaga/ReserveUsername.cs b/ReservationSaga/ReserveUsername.cs new file mode 100644 index 0000000..77cf924 --- /dev/null +++ b/ReservationSaga/ReserveUsername.cs @@ -0,0 +1,37 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ReserveUsername : ICommand +{ + public string Username { get; set; } +} + +public class ReserveUsernameHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ReserveUsernameHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public async Task Handle(ReserveUsername message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Reserving Username for {message.Username}"); + + if (_usernameReservation.Reserve(message.Username)) + { + var expireOptions = new SendOptions(); + expireOptions.DelayDeliveryWith(TimeSpan.FromSeconds(10)); + await context.Send(new ExpireReservation { Username = message.Username }, expireOptions); + + await context.Publish(new UsernameReserved { Username = message.Username }); + } + } +} + +public class UsernameReserved : IEvent +{ + public string Username { get; set; } +} \ 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/ReservationPattern.sln b/ReservationPattern.sln new file mode 100644 index 0000000..9aa461c --- /dev/null +++ b/ReservationPattern.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReservationSaga", "ReservationSaga\ReservationSaga.csproj", "{9C666269-78A0-4369-AF47-7D3600F5540F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReserveSync", "ReserveSync\ReserveSync.csproj", "{B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.Build.0 = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ReservationSaga/ConfirmUsernameReservation.cs b/ReservationSaga/ConfirmUsernameReservation.cs new file mode 100644 index 0000000..e28aabc --- /dev/null +++ b/ReservationSaga/ConfirmUsernameReservation.cs @@ -0,0 +1,33 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ConfirmUsernameReservation : ICommand +{ + public string Username { get; set; } +} + +public class UsernameRegistered : IEvent +{ + public string Username { get; set; } +} + +public class ConfirmUsernameReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ConfirmUsernameReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public async Task Handle(ConfirmUsernameReservation message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Confirmed Reservation for {message.Username}"); + + if (_usernameReservation.Complete(message.Username)) + { + await context.Publish(new UsernameRegistered { Username = message.Username }); + } + } +} \ No newline at end of file diff --git a/ReservationSaga/CreateUserAccount.cs b/ReservationSaga/CreateUserAccount.cs new file mode 100644 index 0000000..a9e6822 --- /dev/null +++ b/ReservationSaga/CreateUserAccount.cs @@ -0,0 +1,23 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class CreateUserAccount : ICommand +{ + public string Username { get; set; } +} + +public class CreateUserAccountHandler : IHandleMessages +{ + public async Task Handle(CreateUserAccount message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Create User Account for {message.Username}"); + + await context.Publish(new UserAccountCreated { Username = message.Username }); + } +} + +public class UserAccountCreated : IEvent +{ + public string Username { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/ExpireReservation.cs b/ReservationSaga/ExpireReservation.cs new file mode 100644 index 0000000..6ce79e9 --- /dev/null +++ b/ReservationSaga/ExpireReservation.cs @@ -0,0 +1,28 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ExpireReservation : ICommand +{ + public string Username { get; set; } +} + +public class ExpireReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ExpireReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public Task Handle(ExpireReservation message, IMessageHandlerContext context) + { + if (_usernameReservation.IsReserved(message.Username)) + { + Console.WriteLine($"Async: Expire Reservation for {message.Username}"); + _usernameReservation.Expire(message.Username); + } + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/ReservationSaga/FakeDatabase.cs b/ReservationSaga/FakeDatabase.cs new file mode 100644 index 0000000..1bfbebd --- /dev/null +++ b/ReservationSaga/FakeDatabase.cs @@ -0,0 +1,48 @@ +namespace Reservation; + +public class FakeDatabase +{ + public List RegisteredUsernames { get; set; } = new(); + public List ReservedUsernames { get; set; } = new(); + public List UserAccounts { get; set; } = new(); +} + +public record Account(string Username); + +public interface IUserRepository +{ + public void Add(Account account); + public void Remove(Account account); + void Save(); +} + +public class UserRepository : IUserRepository +{ + private readonly FakeDatabase _db; + private readonly List _unsaved = new(); + + public UserRepository(FakeDatabase db) + { + _db = db; + } + + public void Add(Account account) + { + _unsaved.Add(account); + } + + public void Remove(Account account) + { + var item = _db.UserAccounts.SingleOrDefault(x => x.Username == account.Username); + if (item != null) + { + _db.UserAccounts.Remove(item); + } + } + + public void Save() + { + _db.UserAccounts.AddRange(_unsaved); + _unsaved.Clear(); + } +} \ No newline at end of file diff --git a/ReservationSaga/Program.cs b/ReservationSaga/Program.cs new file mode 100644 index 0000000..94ac295 --- /dev/null +++ b/ReservationSaga/Program.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NServiceBus; +using Reservation; +using ReservationSaga; + +static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseNServiceBus(context => + { + var endpointConfiguration = new EndpointConfiguration("Demo"); + var transport = endpointConfiguration.UseTransport(); + var persistence = endpointConfiguration.UsePersistence(); + + var routing = transport.Routing(); + + routing.RouteToEndpoint( + assembly: typeof(UserRegistration).Assembly, + destination: "Demo"); + + return endpointConfiguration; + }) + .ConfigureServices(services => + { + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + }); + +var host = CreateHostBuilder(args).Build(); +await host.StartAsync(); + +var session = host.Services.GetRequiredService(); +var usernameReservation = host.Services.GetRequiredService(); + +var username = string.Empty; +while (username != "q") +{ + Console.Write("Username: "); + username = Console.ReadLine(); + if (usernameReservation.IsAvailable(username)) + { + await session.Publish(new UserRegistrationStarted { Username = username }); + } + else + { + Console.WriteLine("Username already exists."); + } + Console.ReadLine(); +} \ No newline at end of file diff --git a/ReservationSaga/ReservationSaga.csproj b/ReservationSaga/ReservationSaga.csproj new file mode 100644 index 0000000..48682eb --- /dev/null +++ b/ReservationSaga/ReservationSaga.csproj @@ -0,0 +1,15 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + diff --git a/ReservationSaga/ReserveUsername.cs b/ReservationSaga/ReserveUsername.cs new file mode 100644 index 0000000..77cf924 --- /dev/null +++ b/ReservationSaga/ReserveUsername.cs @@ -0,0 +1,37 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ReserveUsername : ICommand +{ + public string Username { get; set; } +} + +public class ReserveUsernameHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ReserveUsernameHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public async Task Handle(ReserveUsername message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Reserving Username for {message.Username}"); + + if (_usernameReservation.Reserve(message.Username)) + { + var expireOptions = new SendOptions(); + expireOptions.DelayDeliveryWith(TimeSpan.FromSeconds(10)); + await context.Send(new ExpireReservation { Username = message.Username }, expireOptions); + + await context.Publish(new UsernameReserved { Username = message.Username }); + } + } +} + +public class UsernameReserved : IEvent +{ + public string Username { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/UserRegistrationSaga.cs b/ReservationSaga/UserRegistrationSaga.cs new file mode 100644 index 0000000..31c8cd5 --- /dev/null +++ b/ReservationSaga/UserRegistrationSaga.cs @@ -0,0 +1,54 @@ +using NServiceBus; +namespace ReservationSaga; + +public class UserRegistration : + Saga, + IAmStartedByMessages, + IHandleMessages, + IHandleMessages, + IHandleMessages +{ + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) + { + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + } + + public async Task Handle(UserRegistrationStarted message, IMessageHandlerContext context) + { + await context.Send(new ReserveUsername { Username = message.Username }); + } + + public async Task Handle(UsernameReserved message, IMessageHandlerContext context) + { + await context.Send(new CreateUserAccount { Username = message.Username }); + } + + public async Task Handle(UserAccountCreated message, IMessageHandlerContext context) + { + if (message.Username == "test" && Data.Attempts == 0) + { + Data.Attempts++; + Console.WriteLine("Async: Account Creation Failed."); + return; + } + + await context.Send(new ConfirmUsernameReservation { Username = message.Username }); + } + + public Task Handle(UsernameRegistered message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Registration Complete for {message.Username}"); + + MarkAsComplete(); + return Task.CompletedTask; + } +} + +public class EmailReservationSagaData : ContainSagaData +{ + public string Username { get; set; } + public int Attempts { get; set; } +} \ 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/ReservationPattern.sln b/ReservationPattern.sln new file mode 100644 index 0000000..9aa461c --- /dev/null +++ b/ReservationPattern.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReservationSaga", "ReservationSaga\ReservationSaga.csproj", "{9C666269-78A0-4369-AF47-7D3600F5540F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReserveSync", "ReserveSync\ReserveSync.csproj", "{B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.Build.0 = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ReservationSaga/ConfirmUsernameReservation.cs b/ReservationSaga/ConfirmUsernameReservation.cs new file mode 100644 index 0000000..e28aabc --- /dev/null +++ b/ReservationSaga/ConfirmUsernameReservation.cs @@ -0,0 +1,33 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ConfirmUsernameReservation : ICommand +{ + public string Username { get; set; } +} + +public class UsernameRegistered : IEvent +{ + public string Username { get; set; } +} + +public class ConfirmUsernameReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ConfirmUsernameReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public async Task Handle(ConfirmUsernameReservation message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Confirmed Reservation for {message.Username}"); + + if (_usernameReservation.Complete(message.Username)) + { + await context.Publish(new UsernameRegistered { Username = message.Username }); + } + } +} \ No newline at end of file diff --git a/ReservationSaga/CreateUserAccount.cs b/ReservationSaga/CreateUserAccount.cs new file mode 100644 index 0000000..a9e6822 --- /dev/null +++ b/ReservationSaga/CreateUserAccount.cs @@ -0,0 +1,23 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class CreateUserAccount : ICommand +{ + public string Username { get; set; } +} + +public class CreateUserAccountHandler : IHandleMessages +{ + public async Task Handle(CreateUserAccount message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Create User Account for {message.Username}"); + + await context.Publish(new UserAccountCreated { Username = message.Username }); + } +} + +public class UserAccountCreated : IEvent +{ + public string Username { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/ExpireReservation.cs b/ReservationSaga/ExpireReservation.cs new file mode 100644 index 0000000..6ce79e9 --- /dev/null +++ b/ReservationSaga/ExpireReservation.cs @@ -0,0 +1,28 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ExpireReservation : ICommand +{ + public string Username { get; set; } +} + +public class ExpireReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ExpireReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public Task Handle(ExpireReservation message, IMessageHandlerContext context) + { + if (_usernameReservation.IsReserved(message.Username)) + { + Console.WriteLine($"Async: Expire Reservation for {message.Username}"); + _usernameReservation.Expire(message.Username); + } + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/ReservationSaga/FakeDatabase.cs b/ReservationSaga/FakeDatabase.cs new file mode 100644 index 0000000..1bfbebd --- /dev/null +++ b/ReservationSaga/FakeDatabase.cs @@ -0,0 +1,48 @@ +namespace Reservation; + +public class FakeDatabase +{ + public List RegisteredUsernames { get; set; } = new(); + public List ReservedUsernames { get; set; } = new(); + public List UserAccounts { get; set; } = new(); +} + +public record Account(string Username); + +public interface IUserRepository +{ + public void Add(Account account); + public void Remove(Account account); + void Save(); +} + +public class UserRepository : IUserRepository +{ + private readonly FakeDatabase _db; + private readonly List _unsaved = new(); + + public UserRepository(FakeDatabase db) + { + _db = db; + } + + public void Add(Account account) + { + _unsaved.Add(account); + } + + public void Remove(Account account) + { + var item = _db.UserAccounts.SingleOrDefault(x => x.Username == account.Username); + if (item != null) + { + _db.UserAccounts.Remove(item); + } + } + + public void Save() + { + _db.UserAccounts.AddRange(_unsaved); + _unsaved.Clear(); + } +} \ No newline at end of file diff --git a/ReservationSaga/Program.cs b/ReservationSaga/Program.cs new file mode 100644 index 0000000..94ac295 --- /dev/null +++ b/ReservationSaga/Program.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NServiceBus; +using Reservation; +using ReservationSaga; + +static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseNServiceBus(context => + { + var endpointConfiguration = new EndpointConfiguration("Demo"); + var transport = endpointConfiguration.UseTransport(); + var persistence = endpointConfiguration.UsePersistence(); + + var routing = transport.Routing(); + + routing.RouteToEndpoint( + assembly: typeof(UserRegistration).Assembly, + destination: "Demo"); + + return endpointConfiguration; + }) + .ConfigureServices(services => + { + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + }); + +var host = CreateHostBuilder(args).Build(); +await host.StartAsync(); + +var session = host.Services.GetRequiredService(); +var usernameReservation = host.Services.GetRequiredService(); + +var username = string.Empty; +while (username != "q") +{ + Console.Write("Username: "); + username = Console.ReadLine(); + if (usernameReservation.IsAvailable(username)) + { + await session.Publish(new UserRegistrationStarted { Username = username }); + } + else + { + Console.WriteLine("Username already exists."); + } + Console.ReadLine(); +} \ No newline at end of file diff --git a/ReservationSaga/ReservationSaga.csproj b/ReservationSaga/ReservationSaga.csproj new file mode 100644 index 0000000..48682eb --- /dev/null +++ b/ReservationSaga/ReservationSaga.csproj @@ -0,0 +1,15 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + diff --git a/ReservationSaga/ReserveUsername.cs b/ReservationSaga/ReserveUsername.cs new file mode 100644 index 0000000..77cf924 --- /dev/null +++ b/ReservationSaga/ReserveUsername.cs @@ -0,0 +1,37 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ReserveUsername : ICommand +{ + public string Username { get; set; } +} + +public class ReserveUsernameHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ReserveUsernameHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public async Task Handle(ReserveUsername message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Reserving Username for {message.Username}"); + + if (_usernameReservation.Reserve(message.Username)) + { + var expireOptions = new SendOptions(); + expireOptions.DelayDeliveryWith(TimeSpan.FromSeconds(10)); + await context.Send(new ExpireReservation { Username = message.Username }, expireOptions); + + await context.Publish(new UsernameReserved { Username = message.Username }); + } + } +} + +public class UsernameReserved : IEvent +{ + public string Username { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/UserRegistrationSaga.cs b/ReservationSaga/UserRegistrationSaga.cs new file mode 100644 index 0000000..31c8cd5 --- /dev/null +++ b/ReservationSaga/UserRegistrationSaga.cs @@ -0,0 +1,54 @@ +using NServiceBus; +namespace ReservationSaga; + +public class UserRegistration : + Saga, + IAmStartedByMessages, + IHandleMessages, + IHandleMessages, + IHandleMessages +{ + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) + { + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + } + + public async Task Handle(UserRegistrationStarted message, IMessageHandlerContext context) + { + await context.Send(new ReserveUsername { Username = message.Username }); + } + + public async Task Handle(UsernameReserved message, IMessageHandlerContext context) + { + await context.Send(new CreateUserAccount { Username = message.Username }); + } + + public async Task Handle(UserAccountCreated message, IMessageHandlerContext context) + { + if (message.Username == "test" && Data.Attempts == 0) + { + Data.Attempts++; + Console.WriteLine("Async: Account Creation Failed."); + return; + } + + await context.Send(new ConfirmUsernameReservation { Username = message.Username }); + } + + public Task Handle(UsernameRegistered message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Registration Complete for {message.Username}"); + + MarkAsComplete(); + return Task.CompletedTask; + } +} + +public class EmailReservationSagaData : ContainSagaData +{ + public string Username { get; set; } + public int Attempts { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/UserRegistrationStarted.cs b/ReservationSaga/UserRegistrationStarted.cs new file mode 100644 index 0000000..91d9803 --- /dev/null +++ b/ReservationSaga/UserRegistrationStarted.cs @@ -0,0 +1,8 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class UserRegistrationStarted : IEvent +{ + public string Username { get; set; } +} \ 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/ReservationPattern.sln b/ReservationPattern.sln new file mode 100644 index 0000000..9aa461c --- /dev/null +++ b/ReservationPattern.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReservationSaga", "ReservationSaga\ReservationSaga.csproj", "{9C666269-78A0-4369-AF47-7D3600F5540F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReserveSync", "ReserveSync\ReserveSync.csproj", "{B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.Build.0 = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ReservationSaga/ConfirmUsernameReservation.cs b/ReservationSaga/ConfirmUsernameReservation.cs new file mode 100644 index 0000000..e28aabc --- /dev/null +++ b/ReservationSaga/ConfirmUsernameReservation.cs @@ -0,0 +1,33 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ConfirmUsernameReservation : ICommand +{ + public string Username { get; set; } +} + +public class UsernameRegistered : IEvent +{ + public string Username { get; set; } +} + +public class ConfirmUsernameReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ConfirmUsernameReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public async Task Handle(ConfirmUsernameReservation message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Confirmed Reservation for {message.Username}"); + + if (_usernameReservation.Complete(message.Username)) + { + await context.Publish(new UsernameRegistered { Username = message.Username }); + } + } +} \ No newline at end of file diff --git a/ReservationSaga/CreateUserAccount.cs b/ReservationSaga/CreateUserAccount.cs new file mode 100644 index 0000000..a9e6822 --- /dev/null +++ b/ReservationSaga/CreateUserAccount.cs @@ -0,0 +1,23 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class CreateUserAccount : ICommand +{ + public string Username { get; set; } +} + +public class CreateUserAccountHandler : IHandleMessages +{ + public async Task Handle(CreateUserAccount message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Create User Account for {message.Username}"); + + await context.Publish(new UserAccountCreated { Username = message.Username }); + } +} + +public class UserAccountCreated : IEvent +{ + public string Username { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/ExpireReservation.cs b/ReservationSaga/ExpireReservation.cs new file mode 100644 index 0000000..6ce79e9 --- /dev/null +++ b/ReservationSaga/ExpireReservation.cs @@ -0,0 +1,28 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ExpireReservation : ICommand +{ + public string Username { get; set; } +} + +public class ExpireReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ExpireReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public Task Handle(ExpireReservation message, IMessageHandlerContext context) + { + if (_usernameReservation.IsReserved(message.Username)) + { + Console.WriteLine($"Async: Expire Reservation for {message.Username}"); + _usernameReservation.Expire(message.Username); + } + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/ReservationSaga/FakeDatabase.cs b/ReservationSaga/FakeDatabase.cs new file mode 100644 index 0000000..1bfbebd --- /dev/null +++ b/ReservationSaga/FakeDatabase.cs @@ -0,0 +1,48 @@ +namespace Reservation; + +public class FakeDatabase +{ + public List RegisteredUsernames { get; set; } = new(); + public List ReservedUsernames { get; set; } = new(); + public List UserAccounts { get; set; } = new(); +} + +public record Account(string Username); + +public interface IUserRepository +{ + public void Add(Account account); + public void Remove(Account account); + void Save(); +} + +public class UserRepository : IUserRepository +{ + private readonly FakeDatabase _db; + private readonly List _unsaved = new(); + + public UserRepository(FakeDatabase db) + { + _db = db; + } + + public void Add(Account account) + { + _unsaved.Add(account); + } + + public void Remove(Account account) + { + var item = _db.UserAccounts.SingleOrDefault(x => x.Username == account.Username); + if (item != null) + { + _db.UserAccounts.Remove(item); + } + } + + public void Save() + { + _db.UserAccounts.AddRange(_unsaved); + _unsaved.Clear(); + } +} \ No newline at end of file diff --git a/ReservationSaga/Program.cs b/ReservationSaga/Program.cs new file mode 100644 index 0000000..94ac295 --- /dev/null +++ b/ReservationSaga/Program.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NServiceBus; +using Reservation; +using ReservationSaga; + +static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseNServiceBus(context => + { + var endpointConfiguration = new EndpointConfiguration("Demo"); + var transport = endpointConfiguration.UseTransport(); + var persistence = endpointConfiguration.UsePersistence(); + + var routing = transport.Routing(); + + routing.RouteToEndpoint( + assembly: typeof(UserRegistration).Assembly, + destination: "Demo"); + + return endpointConfiguration; + }) + .ConfigureServices(services => + { + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + }); + +var host = CreateHostBuilder(args).Build(); +await host.StartAsync(); + +var session = host.Services.GetRequiredService(); +var usernameReservation = host.Services.GetRequiredService(); + +var username = string.Empty; +while (username != "q") +{ + Console.Write("Username: "); + username = Console.ReadLine(); + if (usernameReservation.IsAvailable(username)) + { + await session.Publish(new UserRegistrationStarted { Username = username }); + } + else + { + Console.WriteLine("Username already exists."); + } + Console.ReadLine(); +} \ No newline at end of file diff --git a/ReservationSaga/ReservationSaga.csproj b/ReservationSaga/ReservationSaga.csproj new file mode 100644 index 0000000..48682eb --- /dev/null +++ b/ReservationSaga/ReservationSaga.csproj @@ -0,0 +1,15 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + diff --git a/ReservationSaga/ReserveUsername.cs b/ReservationSaga/ReserveUsername.cs new file mode 100644 index 0000000..77cf924 --- /dev/null +++ b/ReservationSaga/ReserveUsername.cs @@ -0,0 +1,37 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ReserveUsername : ICommand +{ + public string Username { get; set; } +} + +public class ReserveUsernameHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ReserveUsernameHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public async Task Handle(ReserveUsername message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Reserving Username for {message.Username}"); + + if (_usernameReservation.Reserve(message.Username)) + { + var expireOptions = new SendOptions(); + expireOptions.DelayDeliveryWith(TimeSpan.FromSeconds(10)); + await context.Send(new ExpireReservation { Username = message.Username }, expireOptions); + + await context.Publish(new UsernameReserved { Username = message.Username }); + } + } +} + +public class UsernameReserved : IEvent +{ + public string Username { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/UserRegistrationSaga.cs b/ReservationSaga/UserRegistrationSaga.cs new file mode 100644 index 0000000..31c8cd5 --- /dev/null +++ b/ReservationSaga/UserRegistrationSaga.cs @@ -0,0 +1,54 @@ +using NServiceBus; +namespace ReservationSaga; + +public class UserRegistration : + Saga, + IAmStartedByMessages, + IHandleMessages, + IHandleMessages, + IHandleMessages +{ + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) + { + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + } + + public async Task Handle(UserRegistrationStarted message, IMessageHandlerContext context) + { + await context.Send(new ReserveUsername { Username = message.Username }); + } + + public async Task Handle(UsernameReserved message, IMessageHandlerContext context) + { + await context.Send(new CreateUserAccount { Username = message.Username }); + } + + public async Task Handle(UserAccountCreated message, IMessageHandlerContext context) + { + if (message.Username == "test" && Data.Attempts == 0) + { + Data.Attempts++; + Console.WriteLine("Async: Account Creation Failed."); + return; + } + + await context.Send(new ConfirmUsernameReservation { Username = message.Username }); + } + + public Task Handle(UsernameRegistered message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Registration Complete for {message.Username}"); + + MarkAsComplete(); + return Task.CompletedTask; + } +} + +public class EmailReservationSagaData : ContainSagaData +{ + public string Username { get; set; } + public int Attempts { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/UserRegistrationStarted.cs b/ReservationSaga/UserRegistrationStarted.cs new file mode 100644 index 0000000..91d9803 --- /dev/null +++ b/ReservationSaga/UserRegistrationStarted.cs @@ -0,0 +1,8 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class UserRegistrationStarted : IEvent +{ + public string Username { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/UsernameReservation.cs b/ReservationSaga/UsernameReservation.cs new file mode 100644 index 0000000..e48a45a --- /dev/null +++ b/ReservationSaga/UsernameReservation.cs @@ -0,0 +1,60 @@ +using Reservation; + +namespace ReservationSaga; + +public class UsernameReservation +{ + private readonly FakeDatabase _db; + + public UsernameReservation(FakeDatabase db) + { + _db = db; + } + + public bool IsAvailable(string username) + { + return _db.RegisteredUsernames.Contains(username) == false && _db.ReservedUsernames.Contains(username) == false; + } + + public bool Reserve(string username) + { + if (_db.RegisteredUsernames.Any(x => x == username)) + { + return false; + } + if (_db.ReservedUsernames.Any(x => x == username)) + { + return false; + } + + _db.ReservedUsernames.Add(username); + return true; + } + + public void Expire(string username) + { + _db.ReservedUsernames.Remove(username); + } + + public bool Complete(string username) + { + if (_db.ReservedUsernames.Any(x => x == username) == false) + { + return false; + } + if (_db.RegisteredUsernames.Any(x => x == username)) + { + return false; + } + + _db.ReservedUsernames.Remove(username); + _db.RegisteredUsernames.Add(username); + + return true; + } + + public bool IsReserved(string username) + { + return _db.ReservedUsernames.Contains(username); + } +} \ 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/ReservationPattern.sln b/ReservationPattern.sln new file mode 100644 index 0000000..9aa461c --- /dev/null +++ b/ReservationPattern.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReservationSaga", "ReservationSaga\ReservationSaga.csproj", "{9C666269-78A0-4369-AF47-7D3600F5540F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReserveSync", "ReserveSync\ReserveSync.csproj", "{B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.Build.0 = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ReservationSaga/ConfirmUsernameReservation.cs b/ReservationSaga/ConfirmUsernameReservation.cs new file mode 100644 index 0000000..e28aabc --- /dev/null +++ b/ReservationSaga/ConfirmUsernameReservation.cs @@ -0,0 +1,33 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ConfirmUsernameReservation : ICommand +{ + public string Username { get; set; } +} + +public class UsernameRegistered : IEvent +{ + public string Username { get; set; } +} + +public class ConfirmUsernameReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ConfirmUsernameReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public async Task Handle(ConfirmUsernameReservation message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Confirmed Reservation for {message.Username}"); + + if (_usernameReservation.Complete(message.Username)) + { + await context.Publish(new UsernameRegistered { Username = message.Username }); + } + } +} \ No newline at end of file diff --git a/ReservationSaga/CreateUserAccount.cs b/ReservationSaga/CreateUserAccount.cs new file mode 100644 index 0000000..a9e6822 --- /dev/null +++ b/ReservationSaga/CreateUserAccount.cs @@ -0,0 +1,23 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class CreateUserAccount : ICommand +{ + public string Username { get; set; } +} + +public class CreateUserAccountHandler : IHandleMessages +{ + public async Task Handle(CreateUserAccount message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Create User Account for {message.Username}"); + + await context.Publish(new UserAccountCreated { Username = message.Username }); + } +} + +public class UserAccountCreated : IEvent +{ + public string Username { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/ExpireReservation.cs b/ReservationSaga/ExpireReservation.cs new file mode 100644 index 0000000..6ce79e9 --- /dev/null +++ b/ReservationSaga/ExpireReservation.cs @@ -0,0 +1,28 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ExpireReservation : ICommand +{ + public string Username { get; set; } +} + +public class ExpireReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ExpireReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public Task Handle(ExpireReservation message, IMessageHandlerContext context) + { + if (_usernameReservation.IsReserved(message.Username)) + { + Console.WriteLine($"Async: Expire Reservation for {message.Username}"); + _usernameReservation.Expire(message.Username); + } + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/ReservationSaga/FakeDatabase.cs b/ReservationSaga/FakeDatabase.cs new file mode 100644 index 0000000..1bfbebd --- /dev/null +++ b/ReservationSaga/FakeDatabase.cs @@ -0,0 +1,48 @@ +namespace Reservation; + +public class FakeDatabase +{ + public List RegisteredUsernames { get; set; } = new(); + public List ReservedUsernames { get; set; } = new(); + public List UserAccounts { get; set; } = new(); +} + +public record Account(string Username); + +public interface IUserRepository +{ + public void Add(Account account); + public void Remove(Account account); + void Save(); +} + +public class UserRepository : IUserRepository +{ + private readonly FakeDatabase _db; + private readonly List _unsaved = new(); + + public UserRepository(FakeDatabase db) + { + _db = db; + } + + public void Add(Account account) + { + _unsaved.Add(account); + } + + public void Remove(Account account) + { + var item = _db.UserAccounts.SingleOrDefault(x => x.Username == account.Username); + if (item != null) + { + _db.UserAccounts.Remove(item); + } + } + + public void Save() + { + _db.UserAccounts.AddRange(_unsaved); + _unsaved.Clear(); + } +} \ No newline at end of file diff --git a/ReservationSaga/Program.cs b/ReservationSaga/Program.cs new file mode 100644 index 0000000..94ac295 --- /dev/null +++ b/ReservationSaga/Program.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NServiceBus; +using Reservation; +using ReservationSaga; + +static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseNServiceBus(context => + { + var endpointConfiguration = new EndpointConfiguration("Demo"); + var transport = endpointConfiguration.UseTransport(); + var persistence = endpointConfiguration.UsePersistence(); + + var routing = transport.Routing(); + + routing.RouteToEndpoint( + assembly: typeof(UserRegistration).Assembly, + destination: "Demo"); + + return endpointConfiguration; + }) + .ConfigureServices(services => + { + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + }); + +var host = CreateHostBuilder(args).Build(); +await host.StartAsync(); + +var session = host.Services.GetRequiredService(); +var usernameReservation = host.Services.GetRequiredService(); + +var username = string.Empty; +while (username != "q") +{ + Console.Write("Username: "); + username = Console.ReadLine(); + if (usernameReservation.IsAvailable(username)) + { + await session.Publish(new UserRegistrationStarted { Username = username }); + } + else + { + Console.WriteLine("Username already exists."); + } + Console.ReadLine(); +} \ No newline at end of file diff --git a/ReservationSaga/ReservationSaga.csproj b/ReservationSaga/ReservationSaga.csproj new file mode 100644 index 0000000..48682eb --- /dev/null +++ b/ReservationSaga/ReservationSaga.csproj @@ -0,0 +1,15 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + diff --git a/ReservationSaga/ReserveUsername.cs b/ReservationSaga/ReserveUsername.cs new file mode 100644 index 0000000..77cf924 --- /dev/null +++ b/ReservationSaga/ReserveUsername.cs @@ -0,0 +1,37 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ReserveUsername : ICommand +{ + public string Username { get; set; } +} + +public class ReserveUsernameHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ReserveUsernameHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public async Task Handle(ReserveUsername message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Reserving Username for {message.Username}"); + + if (_usernameReservation.Reserve(message.Username)) + { + var expireOptions = new SendOptions(); + expireOptions.DelayDeliveryWith(TimeSpan.FromSeconds(10)); + await context.Send(new ExpireReservation { Username = message.Username }, expireOptions); + + await context.Publish(new UsernameReserved { Username = message.Username }); + } + } +} + +public class UsernameReserved : IEvent +{ + public string Username { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/UserRegistrationSaga.cs b/ReservationSaga/UserRegistrationSaga.cs new file mode 100644 index 0000000..31c8cd5 --- /dev/null +++ b/ReservationSaga/UserRegistrationSaga.cs @@ -0,0 +1,54 @@ +using NServiceBus; +namespace ReservationSaga; + +public class UserRegistration : + Saga, + IAmStartedByMessages, + IHandleMessages, + IHandleMessages, + IHandleMessages +{ + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) + { + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + } + + public async Task Handle(UserRegistrationStarted message, IMessageHandlerContext context) + { + await context.Send(new ReserveUsername { Username = message.Username }); + } + + public async Task Handle(UsernameReserved message, IMessageHandlerContext context) + { + await context.Send(new CreateUserAccount { Username = message.Username }); + } + + public async Task Handle(UserAccountCreated message, IMessageHandlerContext context) + { + if (message.Username == "test" && Data.Attempts == 0) + { + Data.Attempts++; + Console.WriteLine("Async: Account Creation Failed."); + return; + } + + await context.Send(new ConfirmUsernameReservation { Username = message.Username }); + } + + public Task Handle(UsernameRegistered message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Registration Complete for {message.Username}"); + + MarkAsComplete(); + return Task.CompletedTask; + } +} + +public class EmailReservationSagaData : ContainSagaData +{ + public string Username { get; set; } + public int Attempts { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/UserRegistrationStarted.cs b/ReservationSaga/UserRegistrationStarted.cs new file mode 100644 index 0000000..91d9803 --- /dev/null +++ b/ReservationSaga/UserRegistrationStarted.cs @@ -0,0 +1,8 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class UserRegistrationStarted : IEvent +{ + public string Username { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/UsernameReservation.cs b/ReservationSaga/UsernameReservation.cs new file mode 100644 index 0000000..e48a45a --- /dev/null +++ b/ReservationSaga/UsernameReservation.cs @@ -0,0 +1,60 @@ +using Reservation; + +namespace ReservationSaga; + +public class UsernameReservation +{ + private readonly FakeDatabase _db; + + public UsernameReservation(FakeDatabase db) + { + _db = db; + } + + public bool IsAvailable(string username) + { + return _db.RegisteredUsernames.Contains(username) == false && _db.ReservedUsernames.Contains(username) == false; + } + + public bool Reserve(string username) + { + if (_db.RegisteredUsernames.Any(x => x == username)) + { + return false; + } + if (_db.ReservedUsernames.Any(x => x == username)) + { + return false; + } + + _db.ReservedUsernames.Add(username); + return true; + } + + public void Expire(string username) + { + _db.ReservedUsernames.Remove(username); + } + + public bool Complete(string username) + { + if (_db.ReservedUsernames.Any(x => x == username) == false) + { + return false; + } + if (_db.RegisteredUsernames.Any(x => x == username)) + { + return false; + } + + _db.ReservedUsernames.Remove(username); + _db.RegisteredUsernames.Add(username); + + return true; + } + + public bool IsReserved(string username) + { + return _db.ReservedUsernames.Contains(username); + } +} \ No newline at end of file diff --git a/ReserveSync/Program.cs b/ReserveSync/Program.cs new file mode 100644 index 0000000..328d071 --- /dev/null +++ b/ReserveSync/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using Reservation; +using ReservationSync; + +var services = new ServiceCollection(); +services.AddSingleton(); +services.AddTransient(); +services.AddTransient(); +services.AddTransient(); +var provider = services.BuildServiceProvider(); + +var username = string.Empty; +while (username != "q") +{ + Console.Write("Username: "); + username = Console.ReadLine(); + var userRegistration = provider.GetRequiredService(); + var result = userRegistration.Register(username); + Console.WriteLine(result ? "Registration Complete" : "Registration Failed"); +} \ 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/ReservationPattern.sln b/ReservationPattern.sln new file mode 100644 index 0000000..9aa461c --- /dev/null +++ b/ReservationPattern.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReservationSaga", "ReservationSaga\ReservationSaga.csproj", "{9C666269-78A0-4369-AF47-7D3600F5540F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReserveSync", "ReserveSync\ReserveSync.csproj", "{B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.Build.0 = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ReservationSaga/ConfirmUsernameReservation.cs b/ReservationSaga/ConfirmUsernameReservation.cs new file mode 100644 index 0000000..e28aabc --- /dev/null +++ b/ReservationSaga/ConfirmUsernameReservation.cs @@ -0,0 +1,33 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ConfirmUsernameReservation : ICommand +{ + public string Username { get; set; } +} + +public class UsernameRegistered : IEvent +{ + public string Username { get; set; } +} + +public class ConfirmUsernameReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ConfirmUsernameReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public async Task Handle(ConfirmUsernameReservation message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Confirmed Reservation for {message.Username}"); + + if (_usernameReservation.Complete(message.Username)) + { + await context.Publish(new UsernameRegistered { Username = message.Username }); + } + } +} \ No newline at end of file diff --git a/ReservationSaga/CreateUserAccount.cs b/ReservationSaga/CreateUserAccount.cs new file mode 100644 index 0000000..a9e6822 --- /dev/null +++ b/ReservationSaga/CreateUserAccount.cs @@ -0,0 +1,23 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class CreateUserAccount : ICommand +{ + public string Username { get; set; } +} + +public class CreateUserAccountHandler : IHandleMessages +{ + public async Task Handle(CreateUserAccount message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Create User Account for {message.Username}"); + + await context.Publish(new UserAccountCreated { Username = message.Username }); + } +} + +public class UserAccountCreated : IEvent +{ + public string Username { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/ExpireReservation.cs b/ReservationSaga/ExpireReservation.cs new file mode 100644 index 0000000..6ce79e9 --- /dev/null +++ b/ReservationSaga/ExpireReservation.cs @@ -0,0 +1,28 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ExpireReservation : ICommand +{ + public string Username { get; set; } +} + +public class ExpireReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ExpireReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public Task Handle(ExpireReservation message, IMessageHandlerContext context) + { + if (_usernameReservation.IsReserved(message.Username)) + { + Console.WriteLine($"Async: Expire Reservation for {message.Username}"); + _usernameReservation.Expire(message.Username); + } + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/ReservationSaga/FakeDatabase.cs b/ReservationSaga/FakeDatabase.cs new file mode 100644 index 0000000..1bfbebd --- /dev/null +++ b/ReservationSaga/FakeDatabase.cs @@ -0,0 +1,48 @@ +namespace Reservation; + +public class FakeDatabase +{ + public List RegisteredUsernames { get; set; } = new(); + public List ReservedUsernames { get; set; } = new(); + public List UserAccounts { get; set; } = new(); +} + +public record Account(string Username); + +public interface IUserRepository +{ + public void Add(Account account); + public void Remove(Account account); + void Save(); +} + +public class UserRepository : IUserRepository +{ + private readonly FakeDatabase _db; + private readonly List _unsaved = new(); + + public UserRepository(FakeDatabase db) + { + _db = db; + } + + public void Add(Account account) + { + _unsaved.Add(account); + } + + public void Remove(Account account) + { + var item = _db.UserAccounts.SingleOrDefault(x => x.Username == account.Username); + if (item != null) + { + _db.UserAccounts.Remove(item); + } + } + + public void Save() + { + _db.UserAccounts.AddRange(_unsaved); + _unsaved.Clear(); + } +} \ No newline at end of file diff --git a/ReservationSaga/Program.cs b/ReservationSaga/Program.cs new file mode 100644 index 0000000..94ac295 --- /dev/null +++ b/ReservationSaga/Program.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NServiceBus; +using Reservation; +using ReservationSaga; + +static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseNServiceBus(context => + { + var endpointConfiguration = new EndpointConfiguration("Demo"); + var transport = endpointConfiguration.UseTransport(); + var persistence = endpointConfiguration.UsePersistence(); + + var routing = transport.Routing(); + + routing.RouteToEndpoint( + assembly: typeof(UserRegistration).Assembly, + destination: "Demo"); + + return endpointConfiguration; + }) + .ConfigureServices(services => + { + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + }); + +var host = CreateHostBuilder(args).Build(); +await host.StartAsync(); + +var session = host.Services.GetRequiredService(); +var usernameReservation = host.Services.GetRequiredService(); + +var username = string.Empty; +while (username != "q") +{ + Console.Write("Username: "); + username = Console.ReadLine(); + if (usernameReservation.IsAvailable(username)) + { + await session.Publish(new UserRegistrationStarted { Username = username }); + } + else + { + Console.WriteLine("Username already exists."); + } + Console.ReadLine(); +} \ No newline at end of file diff --git a/ReservationSaga/ReservationSaga.csproj b/ReservationSaga/ReservationSaga.csproj new file mode 100644 index 0000000..48682eb --- /dev/null +++ b/ReservationSaga/ReservationSaga.csproj @@ -0,0 +1,15 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + diff --git a/ReservationSaga/ReserveUsername.cs b/ReservationSaga/ReserveUsername.cs new file mode 100644 index 0000000..77cf924 --- /dev/null +++ b/ReservationSaga/ReserveUsername.cs @@ -0,0 +1,37 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ReserveUsername : ICommand +{ + public string Username { get; set; } +} + +public class ReserveUsernameHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ReserveUsernameHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public async Task Handle(ReserveUsername message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Reserving Username for {message.Username}"); + + if (_usernameReservation.Reserve(message.Username)) + { + var expireOptions = new SendOptions(); + expireOptions.DelayDeliveryWith(TimeSpan.FromSeconds(10)); + await context.Send(new ExpireReservation { Username = message.Username }, expireOptions); + + await context.Publish(new UsernameReserved { Username = message.Username }); + } + } +} + +public class UsernameReserved : IEvent +{ + public string Username { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/UserRegistrationSaga.cs b/ReservationSaga/UserRegistrationSaga.cs new file mode 100644 index 0000000..31c8cd5 --- /dev/null +++ b/ReservationSaga/UserRegistrationSaga.cs @@ -0,0 +1,54 @@ +using NServiceBus; +namespace ReservationSaga; + +public class UserRegistration : + Saga, + IAmStartedByMessages, + IHandleMessages, + IHandleMessages, + IHandleMessages +{ + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) + { + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + } + + public async Task Handle(UserRegistrationStarted message, IMessageHandlerContext context) + { + await context.Send(new ReserveUsername { Username = message.Username }); + } + + public async Task Handle(UsernameReserved message, IMessageHandlerContext context) + { + await context.Send(new CreateUserAccount { Username = message.Username }); + } + + public async Task Handle(UserAccountCreated message, IMessageHandlerContext context) + { + if (message.Username == "test" && Data.Attempts == 0) + { + Data.Attempts++; + Console.WriteLine("Async: Account Creation Failed."); + return; + } + + await context.Send(new ConfirmUsernameReservation { Username = message.Username }); + } + + public Task Handle(UsernameRegistered message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Registration Complete for {message.Username}"); + + MarkAsComplete(); + return Task.CompletedTask; + } +} + +public class EmailReservationSagaData : ContainSagaData +{ + public string Username { get; set; } + public int Attempts { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/UserRegistrationStarted.cs b/ReservationSaga/UserRegistrationStarted.cs new file mode 100644 index 0000000..91d9803 --- /dev/null +++ b/ReservationSaga/UserRegistrationStarted.cs @@ -0,0 +1,8 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class UserRegistrationStarted : IEvent +{ + public string Username { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/UsernameReservation.cs b/ReservationSaga/UsernameReservation.cs new file mode 100644 index 0000000..e48a45a --- /dev/null +++ b/ReservationSaga/UsernameReservation.cs @@ -0,0 +1,60 @@ +using Reservation; + +namespace ReservationSaga; + +public class UsernameReservation +{ + private readonly FakeDatabase _db; + + public UsernameReservation(FakeDatabase db) + { + _db = db; + } + + public bool IsAvailable(string username) + { + return _db.RegisteredUsernames.Contains(username) == false && _db.ReservedUsernames.Contains(username) == false; + } + + public bool Reserve(string username) + { + if (_db.RegisteredUsernames.Any(x => x == username)) + { + return false; + } + if (_db.ReservedUsernames.Any(x => x == username)) + { + return false; + } + + _db.ReservedUsernames.Add(username); + return true; + } + + public void Expire(string username) + { + _db.ReservedUsernames.Remove(username); + } + + public bool Complete(string username) + { + if (_db.ReservedUsernames.Any(x => x == username) == false) + { + return false; + } + if (_db.RegisteredUsernames.Any(x => x == username)) + { + return false; + } + + _db.ReservedUsernames.Remove(username); + _db.RegisteredUsernames.Add(username); + + return true; + } + + public bool IsReserved(string username) + { + return _db.ReservedUsernames.Contains(username); + } +} \ No newline at end of file diff --git a/ReserveSync/Program.cs b/ReserveSync/Program.cs new file mode 100644 index 0000000..328d071 --- /dev/null +++ b/ReserveSync/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using Reservation; +using ReservationSync; + +var services = new ServiceCollection(); +services.AddSingleton(); +services.AddTransient(); +services.AddTransient(); +services.AddTransient(); +var provider = services.BuildServiceProvider(); + +var username = string.Empty; +while (username != "q") +{ + Console.Write("Username: "); + username = Console.ReadLine(); + var userRegistration = provider.GetRequiredService(); + var result = userRegistration.Register(username); + Console.WriteLine(result ? "Registration Complete" : "Registration Failed"); +} \ No newline at end of file diff --git a/ReserveSync/ReserveSync.csproj b/ReserveSync/ReserveSync.csproj new file mode 100644 index 0000000..d251651 --- /dev/null +++ b/ReserveSync/ReserveSync.csproj @@ -0,0 +1,14 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + 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/ReservationPattern.sln b/ReservationPattern.sln new file mode 100644 index 0000000..9aa461c --- /dev/null +++ b/ReservationPattern.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReservationSaga", "ReservationSaga\ReservationSaga.csproj", "{9C666269-78A0-4369-AF47-7D3600F5540F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReserveSync", "ReserveSync\ReserveSync.csproj", "{B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.Build.0 = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ReservationSaga/ConfirmUsernameReservation.cs b/ReservationSaga/ConfirmUsernameReservation.cs new file mode 100644 index 0000000..e28aabc --- /dev/null +++ b/ReservationSaga/ConfirmUsernameReservation.cs @@ -0,0 +1,33 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ConfirmUsernameReservation : ICommand +{ + public string Username { get; set; } +} + +public class UsernameRegistered : IEvent +{ + public string Username { get; set; } +} + +public class ConfirmUsernameReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ConfirmUsernameReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public async Task Handle(ConfirmUsernameReservation message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Confirmed Reservation for {message.Username}"); + + if (_usernameReservation.Complete(message.Username)) + { + await context.Publish(new UsernameRegistered { Username = message.Username }); + } + } +} \ No newline at end of file diff --git a/ReservationSaga/CreateUserAccount.cs b/ReservationSaga/CreateUserAccount.cs new file mode 100644 index 0000000..a9e6822 --- /dev/null +++ b/ReservationSaga/CreateUserAccount.cs @@ -0,0 +1,23 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class CreateUserAccount : ICommand +{ + public string Username { get; set; } +} + +public class CreateUserAccountHandler : IHandleMessages +{ + public async Task Handle(CreateUserAccount message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Create User Account for {message.Username}"); + + await context.Publish(new UserAccountCreated { Username = message.Username }); + } +} + +public class UserAccountCreated : IEvent +{ + public string Username { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/ExpireReservation.cs b/ReservationSaga/ExpireReservation.cs new file mode 100644 index 0000000..6ce79e9 --- /dev/null +++ b/ReservationSaga/ExpireReservation.cs @@ -0,0 +1,28 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ExpireReservation : ICommand +{ + public string Username { get; set; } +} + +public class ExpireReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ExpireReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public Task Handle(ExpireReservation message, IMessageHandlerContext context) + { + if (_usernameReservation.IsReserved(message.Username)) + { + Console.WriteLine($"Async: Expire Reservation for {message.Username}"); + _usernameReservation.Expire(message.Username); + } + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/ReservationSaga/FakeDatabase.cs b/ReservationSaga/FakeDatabase.cs new file mode 100644 index 0000000..1bfbebd --- /dev/null +++ b/ReservationSaga/FakeDatabase.cs @@ -0,0 +1,48 @@ +namespace Reservation; + +public class FakeDatabase +{ + public List RegisteredUsernames { get; set; } = new(); + public List ReservedUsernames { get; set; } = new(); + public List UserAccounts { get; set; } = new(); +} + +public record Account(string Username); + +public interface IUserRepository +{ + public void Add(Account account); + public void Remove(Account account); + void Save(); +} + +public class UserRepository : IUserRepository +{ + private readonly FakeDatabase _db; + private readonly List _unsaved = new(); + + public UserRepository(FakeDatabase db) + { + _db = db; + } + + public void Add(Account account) + { + _unsaved.Add(account); + } + + public void Remove(Account account) + { + var item = _db.UserAccounts.SingleOrDefault(x => x.Username == account.Username); + if (item != null) + { + _db.UserAccounts.Remove(item); + } + } + + public void Save() + { + _db.UserAccounts.AddRange(_unsaved); + _unsaved.Clear(); + } +} \ No newline at end of file diff --git a/ReservationSaga/Program.cs b/ReservationSaga/Program.cs new file mode 100644 index 0000000..94ac295 --- /dev/null +++ b/ReservationSaga/Program.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NServiceBus; +using Reservation; +using ReservationSaga; + +static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseNServiceBus(context => + { + var endpointConfiguration = new EndpointConfiguration("Demo"); + var transport = endpointConfiguration.UseTransport(); + var persistence = endpointConfiguration.UsePersistence(); + + var routing = transport.Routing(); + + routing.RouteToEndpoint( + assembly: typeof(UserRegistration).Assembly, + destination: "Demo"); + + return endpointConfiguration; + }) + .ConfigureServices(services => + { + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + }); + +var host = CreateHostBuilder(args).Build(); +await host.StartAsync(); + +var session = host.Services.GetRequiredService(); +var usernameReservation = host.Services.GetRequiredService(); + +var username = string.Empty; +while (username != "q") +{ + Console.Write("Username: "); + username = Console.ReadLine(); + if (usernameReservation.IsAvailable(username)) + { + await session.Publish(new UserRegistrationStarted { Username = username }); + } + else + { + Console.WriteLine("Username already exists."); + } + Console.ReadLine(); +} \ No newline at end of file diff --git a/ReservationSaga/ReservationSaga.csproj b/ReservationSaga/ReservationSaga.csproj new file mode 100644 index 0000000..48682eb --- /dev/null +++ b/ReservationSaga/ReservationSaga.csproj @@ -0,0 +1,15 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + diff --git a/ReservationSaga/ReserveUsername.cs b/ReservationSaga/ReserveUsername.cs new file mode 100644 index 0000000..77cf924 --- /dev/null +++ b/ReservationSaga/ReserveUsername.cs @@ -0,0 +1,37 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ReserveUsername : ICommand +{ + public string Username { get; set; } +} + +public class ReserveUsernameHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ReserveUsernameHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public async Task Handle(ReserveUsername message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Reserving Username for {message.Username}"); + + if (_usernameReservation.Reserve(message.Username)) + { + var expireOptions = new SendOptions(); + expireOptions.DelayDeliveryWith(TimeSpan.FromSeconds(10)); + await context.Send(new ExpireReservation { Username = message.Username }, expireOptions); + + await context.Publish(new UsernameReserved { Username = message.Username }); + } + } +} + +public class UsernameReserved : IEvent +{ + public string Username { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/UserRegistrationSaga.cs b/ReservationSaga/UserRegistrationSaga.cs new file mode 100644 index 0000000..31c8cd5 --- /dev/null +++ b/ReservationSaga/UserRegistrationSaga.cs @@ -0,0 +1,54 @@ +using NServiceBus; +namespace ReservationSaga; + +public class UserRegistration : + Saga, + IAmStartedByMessages, + IHandleMessages, + IHandleMessages, + IHandleMessages +{ + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) + { + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + } + + public async Task Handle(UserRegistrationStarted message, IMessageHandlerContext context) + { + await context.Send(new ReserveUsername { Username = message.Username }); + } + + public async Task Handle(UsernameReserved message, IMessageHandlerContext context) + { + await context.Send(new CreateUserAccount { Username = message.Username }); + } + + public async Task Handle(UserAccountCreated message, IMessageHandlerContext context) + { + if (message.Username == "test" && Data.Attempts == 0) + { + Data.Attempts++; + Console.WriteLine("Async: Account Creation Failed."); + return; + } + + await context.Send(new ConfirmUsernameReservation { Username = message.Username }); + } + + public Task Handle(UsernameRegistered message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Registration Complete for {message.Username}"); + + MarkAsComplete(); + return Task.CompletedTask; + } +} + +public class EmailReservationSagaData : ContainSagaData +{ + public string Username { get; set; } + public int Attempts { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/UserRegistrationStarted.cs b/ReservationSaga/UserRegistrationStarted.cs new file mode 100644 index 0000000..91d9803 --- /dev/null +++ b/ReservationSaga/UserRegistrationStarted.cs @@ -0,0 +1,8 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class UserRegistrationStarted : IEvent +{ + public string Username { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/UsernameReservation.cs b/ReservationSaga/UsernameReservation.cs new file mode 100644 index 0000000..e48a45a --- /dev/null +++ b/ReservationSaga/UsernameReservation.cs @@ -0,0 +1,60 @@ +using Reservation; + +namespace ReservationSaga; + +public class UsernameReservation +{ + private readonly FakeDatabase _db; + + public UsernameReservation(FakeDatabase db) + { + _db = db; + } + + public bool IsAvailable(string username) + { + return _db.RegisteredUsernames.Contains(username) == false && _db.ReservedUsernames.Contains(username) == false; + } + + public bool Reserve(string username) + { + if (_db.RegisteredUsernames.Any(x => x == username)) + { + return false; + } + if (_db.ReservedUsernames.Any(x => x == username)) + { + return false; + } + + _db.ReservedUsernames.Add(username); + return true; + } + + public void Expire(string username) + { + _db.ReservedUsernames.Remove(username); + } + + public bool Complete(string username) + { + if (_db.ReservedUsernames.Any(x => x == username) == false) + { + return false; + } + if (_db.RegisteredUsernames.Any(x => x == username)) + { + return false; + } + + _db.ReservedUsernames.Remove(username); + _db.RegisteredUsernames.Add(username); + + return true; + } + + public bool IsReserved(string username) + { + return _db.ReservedUsernames.Contains(username); + } +} \ No newline at end of file diff --git a/ReserveSync/Program.cs b/ReserveSync/Program.cs new file mode 100644 index 0000000..328d071 --- /dev/null +++ b/ReserveSync/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using Reservation; +using ReservationSync; + +var services = new ServiceCollection(); +services.AddSingleton(); +services.AddTransient(); +services.AddTransient(); +services.AddTransient(); +var provider = services.BuildServiceProvider(); + +var username = string.Empty; +while (username != "q") +{ + Console.Write("Username: "); + username = Console.ReadLine(); + var userRegistration = provider.GetRequiredService(); + var result = userRegistration.Register(username); + Console.WriteLine(result ? "Registration Complete" : "Registration Failed"); +} \ No newline at end of file diff --git a/ReserveSync/ReserveSync.csproj b/ReserveSync/ReserveSync.csproj new file mode 100644 index 0000000..d251651 --- /dev/null +++ b/ReserveSync/ReserveSync.csproj @@ -0,0 +1,14 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + diff --git a/ReserveSync/UserRegistration.cs b/ReserveSync/UserRegistration.cs new file mode 100644 index 0000000..3fec76b --- /dev/null +++ b/ReserveSync/UserRegistration.cs @@ -0,0 +1,43 @@ +using Reservation; + +namespace ReservationSync; + +public class UserRegistration +{ + private static int _testCount = 0; + private readonly UsernameReservationSync _reservation; + private readonly IUserRepository _repository; + + public UserRegistration(UsernameReservationSync reservation, IUserRepository repository) + { + _reservation = reservation; + _repository = repository; + } + + public bool Register(string username) + { + if (_reservation.Reserve(username) == false) + { + return false; + } + + // For testing to show the expiry + if (username == "test" && _testCount == 0) + { + _testCount++; + return false; + } + + var account = new Account(username); + _repository.Add(account); + _repository.Save(); + + if (_reservation.Complete(username) == false) + { + _repository.Remove(account); + return false; + } + + return 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/ReservationPattern.sln b/ReservationPattern.sln new file mode 100644 index 0000000..9aa461c --- /dev/null +++ b/ReservationPattern.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReservationSaga", "ReservationSaga\ReservationSaga.csproj", "{9C666269-78A0-4369-AF47-7D3600F5540F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReserveSync", "ReserveSync\ReserveSync.csproj", "{B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C666269-78A0-4369-AF47-7D3600F5540F}.Release|Any CPU.Build.0 = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0DFD31E-E53A-45C6-AB2A-541F0E1F3EE0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ReservationSaga/ConfirmUsernameReservation.cs b/ReservationSaga/ConfirmUsernameReservation.cs new file mode 100644 index 0000000..e28aabc --- /dev/null +++ b/ReservationSaga/ConfirmUsernameReservation.cs @@ -0,0 +1,33 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ConfirmUsernameReservation : ICommand +{ + public string Username { get; set; } +} + +public class UsernameRegistered : IEvent +{ + public string Username { get; set; } +} + +public class ConfirmUsernameReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ConfirmUsernameReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public async Task Handle(ConfirmUsernameReservation message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Confirmed Reservation for {message.Username}"); + + if (_usernameReservation.Complete(message.Username)) + { + await context.Publish(new UsernameRegistered { Username = message.Username }); + } + } +} \ No newline at end of file diff --git a/ReservationSaga/CreateUserAccount.cs b/ReservationSaga/CreateUserAccount.cs new file mode 100644 index 0000000..a9e6822 --- /dev/null +++ b/ReservationSaga/CreateUserAccount.cs @@ -0,0 +1,23 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class CreateUserAccount : ICommand +{ + public string Username { get; set; } +} + +public class CreateUserAccountHandler : IHandleMessages +{ + public async Task Handle(CreateUserAccount message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Create User Account for {message.Username}"); + + await context.Publish(new UserAccountCreated { Username = message.Username }); + } +} + +public class UserAccountCreated : IEvent +{ + public string Username { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/ExpireReservation.cs b/ReservationSaga/ExpireReservation.cs new file mode 100644 index 0000000..6ce79e9 --- /dev/null +++ b/ReservationSaga/ExpireReservation.cs @@ -0,0 +1,28 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ExpireReservation : ICommand +{ + public string Username { get; set; } +} + +public class ExpireReservationHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ExpireReservationHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public Task Handle(ExpireReservation message, IMessageHandlerContext context) + { + if (_usernameReservation.IsReserved(message.Username)) + { + Console.WriteLine($"Async: Expire Reservation for {message.Username}"); + _usernameReservation.Expire(message.Username); + } + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/ReservationSaga/FakeDatabase.cs b/ReservationSaga/FakeDatabase.cs new file mode 100644 index 0000000..1bfbebd --- /dev/null +++ b/ReservationSaga/FakeDatabase.cs @@ -0,0 +1,48 @@ +namespace Reservation; + +public class FakeDatabase +{ + public List RegisteredUsernames { get; set; } = new(); + public List ReservedUsernames { get; set; } = new(); + public List UserAccounts { get; set; } = new(); +} + +public record Account(string Username); + +public interface IUserRepository +{ + public void Add(Account account); + public void Remove(Account account); + void Save(); +} + +public class UserRepository : IUserRepository +{ + private readonly FakeDatabase _db; + private readonly List _unsaved = new(); + + public UserRepository(FakeDatabase db) + { + _db = db; + } + + public void Add(Account account) + { + _unsaved.Add(account); + } + + public void Remove(Account account) + { + var item = _db.UserAccounts.SingleOrDefault(x => x.Username == account.Username); + if (item != null) + { + _db.UserAccounts.Remove(item); + } + } + + public void Save() + { + _db.UserAccounts.AddRange(_unsaved); + _unsaved.Clear(); + } +} \ No newline at end of file diff --git a/ReservationSaga/Program.cs b/ReservationSaga/Program.cs new file mode 100644 index 0000000..94ac295 --- /dev/null +++ b/ReservationSaga/Program.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NServiceBus; +using Reservation; +using ReservationSaga; + +static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseNServiceBus(context => + { + var endpointConfiguration = new EndpointConfiguration("Demo"); + var transport = endpointConfiguration.UseTransport(); + var persistence = endpointConfiguration.UsePersistence(); + + var routing = transport.Routing(); + + routing.RouteToEndpoint( + assembly: typeof(UserRegistration).Assembly, + destination: "Demo"); + + return endpointConfiguration; + }) + .ConfigureServices(services => + { + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + }); + +var host = CreateHostBuilder(args).Build(); +await host.StartAsync(); + +var session = host.Services.GetRequiredService(); +var usernameReservation = host.Services.GetRequiredService(); + +var username = string.Empty; +while (username != "q") +{ + Console.Write("Username: "); + username = Console.ReadLine(); + if (usernameReservation.IsAvailable(username)) + { + await session.Publish(new UserRegistrationStarted { Username = username }); + } + else + { + Console.WriteLine("Username already exists."); + } + Console.ReadLine(); +} \ No newline at end of file diff --git a/ReservationSaga/ReservationSaga.csproj b/ReservationSaga/ReservationSaga.csproj new file mode 100644 index 0000000..48682eb --- /dev/null +++ b/ReservationSaga/ReservationSaga.csproj @@ -0,0 +1,15 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + diff --git a/ReservationSaga/ReserveUsername.cs b/ReservationSaga/ReserveUsername.cs new file mode 100644 index 0000000..77cf924 --- /dev/null +++ b/ReservationSaga/ReserveUsername.cs @@ -0,0 +1,37 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class ReserveUsername : ICommand +{ + public string Username { get; set; } +} + +public class ReserveUsernameHandler : IHandleMessages +{ + private readonly UsernameReservation _usernameReservation; + + public ReserveUsernameHandler(UsernameReservation usernameReservation) + { + _usernameReservation = usernameReservation; + } + + public async Task Handle(ReserveUsername message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Reserving Username for {message.Username}"); + + if (_usernameReservation.Reserve(message.Username)) + { + var expireOptions = new SendOptions(); + expireOptions.DelayDeliveryWith(TimeSpan.FromSeconds(10)); + await context.Send(new ExpireReservation { Username = message.Username }, expireOptions); + + await context.Publish(new UsernameReserved { Username = message.Username }); + } + } +} + +public class UsernameReserved : IEvent +{ + public string Username { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/UserRegistrationSaga.cs b/ReservationSaga/UserRegistrationSaga.cs new file mode 100644 index 0000000..31c8cd5 --- /dev/null +++ b/ReservationSaga/UserRegistrationSaga.cs @@ -0,0 +1,54 @@ +using NServiceBus; +namespace ReservationSaga; + +public class UserRegistration : + Saga, + IAmStartedByMessages, + IHandleMessages, + IHandleMessages, + IHandleMessages +{ + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) + { + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + mapper.ConfigureMapping(message => message.Username).ToSaga(sagaData => sagaData.Username); + } + + public async Task Handle(UserRegistrationStarted message, IMessageHandlerContext context) + { + await context.Send(new ReserveUsername { Username = message.Username }); + } + + public async Task Handle(UsernameReserved message, IMessageHandlerContext context) + { + await context.Send(new CreateUserAccount { Username = message.Username }); + } + + public async Task Handle(UserAccountCreated message, IMessageHandlerContext context) + { + if (message.Username == "test" && Data.Attempts == 0) + { + Data.Attempts++; + Console.WriteLine("Async: Account Creation Failed."); + return; + } + + await context.Send(new ConfirmUsernameReservation { Username = message.Username }); + } + + public Task Handle(UsernameRegistered message, IMessageHandlerContext context) + { + Console.WriteLine($"Async: Registration Complete for {message.Username}"); + + MarkAsComplete(); + return Task.CompletedTask; + } +} + +public class EmailReservationSagaData : ContainSagaData +{ + public string Username { get; set; } + public int Attempts { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/UserRegistrationStarted.cs b/ReservationSaga/UserRegistrationStarted.cs new file mode 100644 index 0000000..91d9803 --- /dev/null +++ b/ReservationSaga/UserRegistrationStarted.cs @@ -0,0 +1,8 @@ +using NServiceBus; + +namespace ReservationSaga; + +public class UserRegistrationStarted : IEvent +{ + public string Username { get; set; } +} \ No newline at end of file diff --git a/ReservationSaga/UsernameReservation.cs b/ReservationSaga/UsernameReservation.cs new file mode 100644 index 0000000..e48a45a --- /dev/null +++ b/ReservationSaga/UsernameReservation.cs @@ -0,0 +1,60 @@ +using Reservation; + +namespace ReservationSaga; + +public class UsernameReservation +{ + private readonly FakeDatabase _db; + + public UsernameReservation(FakeDatabase db) + { + _db = db; + } + + public bool IsAvailable(string username) + { + return _db.RegisteredUsernames.Contains(username) == false && _db.ReservedUsernames.Contains(username) == false; + } + + public bool Reserve(string username) + { + if (_db.RegisteredUsernames.Any(x => x == username)) + { + return false; + } + if (_db.ReservedUsernames.Any(x => x == username)) + { + return false; + } + + _db.ReservedUsernames.Add(username); + return true; + } + + public void Expire(string username) + { + _db.ReservedUsernames.Remove(username); + } + + public bool Complete(string username) + { + if (_db.ReservedUsernames.Any(x => x == username) == false) + { + return false; + } + if (_db.RegisteredUsernames.Any(x => x == username)) + { + return false; + } + + _db.ReservedUsernames.Remove(username); + _db.RegisteredUsernames.Add(username); + + return true; + } + + public bool IsReserved(string username) + { + return _db.ReservedUsernames.Contains(username); + } +} \ No newline at end of file diff --git a/ReserveSync/Program.cs b/ReserveSync/Program.cs new file mode 100644 index 0000000..328d071 --- /dev/null +++ b/ReserveSync/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using Reservation; +using ReservationSync; + +var services = new ServiceCollection(); +services.AddSingleton(); +services.AddTransient(); +services.AddTransient(); +services.AddTransient(); +var provider = services.BuildServiceProvider(); + +var username = string.Empty; +while (username != "q") +{ + Console.Write("Username: "); + username = Console.ReadLine(); + var userRegistration = provider.GetRequiredService(); + var result = userRegistration.Register(username); + Console.WriteLine(result ? "Registration Complete" : "Registration Failed"); +} \ No newline at end of file diff --git a/ReserveSync/ReserveSync.csproj b/ReserveSync/ReserveSync.csproj new file mode 100644 index 0000000..d251651 --- /dev/null +++ b/ReserveSync/ReserveSync.csproj @@ -0,0 +1,14 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + diff --git a/ReserveSync/UserRegistration.cs b/ReserveSync/UserRegistration.cs new file mode 100644 index 0000000..3fec76b --- /dev/null +++ b/ReserveSync/UserRegistration.cs @@ -0,0 +1,43 @@ +using Reservation; + +namespace ReservationSync; + +public class UserRegistration +{ + private static int _testCount = 0; + private readonly UsernameReservationSync _reservation; + private readonly IUserRepository _repository; + + public UserRegistration(UsernameReservationSync reservation, IUserRepository repository) + { + _reservation = reservation; + _repository = repository; + } + + public bool Register(string username) + { + if (_reservation.Reserve(username) == false) + { + return false; + } + + // For testing to show the expiry + if (username == "test" && _testCount == 0) + { + _testCount++; + return false; + } + + var account = new Account(username); + _repository.Add(account); + _repository.Save(); + + if (_reservation.Complete(username) == false) + { + _repository.Remove(account); + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/ReserveSync/UsernameReservationSync.cs b/ReserveSync/UsernameReservationSync.cs new file mode 100644 index 0000000..ef8e243 --- /dev/null +++ b/ReserveSync/UsernameReservationSync.cs @@ -0,0 +1,58 @@ +using Reservation; + +namespace ReservationSync; + +public class UsernameReservationSync +{ + private TimeSpan Timeout => TimeSpan.FromSeconds(5); + private readonly FakeDatabase _db; + + public UsernameReservationSync(FakeDatabase db) + { + _db = db; + } + + public bool Reserve(string username) + { + if (_db.RegisteredUsernames.Any(x => x == username)) + { + return false; + } + if (_db.ReservedUsernames.Any(x => x == username)) + { + return false; + } + + _db.ReservedUsernames.Add(username); + + Task.Run(async () => + { + await Task.Delay(Timeout); + Expire(username); + }); + + return true; + } + + private void Expire(string username) + { + _db.ReservedUsernames.Remove(username); + } + + public bool Complete(string username) + { + if (_db.ReservedUsernames.Any(x => x == username) == false) + { + return false; + } + if (_db.RegisteredUsernames.Any(x => x == username)) + { + return false; + } + + _db.ReservedUsernames.Remove(username); + _db.RegisteredUsernames.Add(username); + + return true; + } +} \ No newline at end of file