diff --git a/src/.gitignore b/src/.gitignore
new file mode 100644
index 0000000..d73637c
--- /dev/null
+++ b/src/.gitignore
@@ -0,0 +1 @@
+/binaries/**/*
\ No newline at end of file
diff --git a/src/Billing/.editorconfig b/src/Billing/.editorconfig
new file mode 100644
index 0000000..7333907
--- /dev/null
+++ b/src/Billing/.editorconfig
@@ -0,0 +1,7 @@
+[*.cs]
+
+# Justification: Test project
+dotnet_diagnostic.CA2007.severity = none
+
+# Justification: Tests don't support cancellation and don't need to forward IMessageHandlerContext.CancellationToken
+dotnet_diagnostic.NSB0002.severity = suggestion
\ No newline at end of file
diff --git a/src/Billing/Billing.csproj b/src/Billing/Billing.csproj
new file mode 100644
index 0000000..d890dc6
--- /dev/null
+++ b/src/Billing/Billing.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net8.0
+ Exe
+ enable
+ enable
+ ..\binaries\Billing\
+ failures.ico
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Billing/OrderPlacedHandler.cs b/src/Billing/OrderPlacedHandler.cs
new file mode 100644
index 0000000..0ec8f80
--- /dev/null
+++ b/src/Billing/OrderPlacedHandler.cs
@@ -0,0 +1,20 @@
+namespace Billing;
+
+using System.Threading.Tasks;
+using MassTransit;
+using Messages;
+
+public class OrderPlacedHandler(SimulationEffects simulationEffects) : IConsumer
+{
+ public async Task Consume(ConsumeContext context)
+ {
+ await simulationEffects.SimulatedMessageProcessing(context.CancellationToken);
+
+ var orderBilled = new OrderBilled
+ {
+ OrderId = context.Message.OrderId
+ };
+
+ await context.Publish(orderBilled);
+ }
+}
\ No newline at end of file
diff --git a/src/Billing/Program.cs b/src/Billing/Program.cs
new file mode 100644
index 0000000..1ed13d8
--- /dev/null
+++ b/src/Billing/Program.cs
@@ -0,0 +1,86 @@
+#pragma warning disable IDE0010
+namespace Billing;
+
+using Microsoft.Extensions.Hosting;
+using MassTransit;
+using Microsoft.Extensions.DependencyInjection;
+using System.Reflection;
+
+class Program
+{
+ public static IHostBuilder CreateHostBuilder(string[] args)
+ {
+ var host = Host.CreateDefaultBuilder(args)
+ .ConfigureServices((hostContext, services) =>
+ {
+ services.AddMassTransit(x =>
+ {
+ x.UsingRabbitMq((context, cfg) =>
+ {
+ cfg.Host("localhost", "/", h =>
+ {
+ h.Username("guest");
+ h.Password("guest");
+ });
+
+ cfg.ConfigureEndpoints(context);
+ });
+
+ x.AddConfigureEndpointsCallback((name, cfg) =>
+ {
+ if (cfg is IRabbitMqReceiveEndpointConfigurator rmq)
+ {
+ rmq.SetQuorumQueue();
+ }
+ });
+
+ x.AddConsumers(Assembly.GetExecutingAssembly());
+ });
+
+ services.AddSingleton();
+ });
+
+ return host;
+ }
+
+ static async Task Main(string[] args)
+ {
+ Console.Title = "Failure rate (Billing)";
+ Console.SetWindowSize(65, 15);
+
+ var host = CreateHostBuilder(args).Build();
+ await host.StartAsync();
+
+ var state = host.Services.GetRequiredService();
+ await RunUserInterfaceLoop(state);
+ }
+
+ static Task RunUserInterfaceLoop(SimulationEffects state)
+ {
+ while (true)
+ {
+ Console.Clear();
+ Console.WriteLine("Billing Endpoint");
+ Console.WriteLine("Press F to increase the simulated failure rate");
+ Console.WriteLine("Press S to decrease the simulated failure rate");
+ Console.WriteLine("Press ESC to quit");
+ Console.WriteLine();
+
+ state.WriteState(Console.Out);
+
+ var input = Console.ReadKey(true);
+
+ switch (input.Key)
+ {
+ case ConsoleKey.F:
+ state.IncreaseFailureRate();
+ break;
+ case ConsoleKey.S:
+ state.DecreaseFailureRate();
+ break;
+ case ConsoleKey.Escape:
+ return Task.CompletedTask;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Billing/SimulationEffects.cs b/src/Billing/SimulationEffects.cs
new file mode 100644
index 0000000..6324d32
--- /dev/null
+++ b/src/Billing/SimulationEffects.cs
@@ -0,0 +1,23 @@
+namespace Billing;
+
+public class SimulationEffects
+{
+ public void IncreaseFailureRate() => failureRate = Math.Min(1, failureRate + FailureRateIncrement);
+
+ public void DecreaseFailureRate() => failureRate = Math.Max(0, failureRate - FailureRateIncrement);
+
+ public void WriteState(TextWriter output) => output.WriteLine("Failure rate: {0:P0}", failureRate);
+
+ public async Task SimulatedMessageProcessing(CancellationToken cancellationToken = default)
+ {
+ await Task.Delay(200, cancellationToken);
+
+ if (Random.Shared.NextDouble() < failureRate)
+ {
+ throw new Exception("BOOM! A failure occurred");
+ }
+ }
+
+ double failureRate;
+ const double FailureRateIncrement = 0.1;
+}
\ No newline at end of file
diff --git a/src/Billing/failures.ico b/src/Billing/failures.ico
new file mode 100644
index 0000000..d302303
Binary files /dev/null and b/src/Billing/failures.ico differ
diff --git a/src/ClientUI/.editorconfig b/src/ClientUI/.editorconfig
new file mode 100644
index 0000000..7333907
--- /dev/null
+++ b/src/ClientUI/.editorconfig
@@ -0,0 +1,7 @@
+[*.cs]
+
+# Justification: Test project
+dotnet_diagnostic.CA2007.severity = none
+
+# Justification: Tests don't support cancellation and don't need to forward IMessageHandlerContext.CancellationToken
+dotnet_diagnostic.NSB0002.severity = suggestion
\ No newline at end of file
diff --git a/src/ClientUI/ClientUI.csproj b/src/ClientUI/ClientUI.csproj
new file mode 100644
index 0000000..923e7dc
--- /dev/null
+++ b/src/ClientUI/ClientUI.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net8.0
+ Exe
+ enable
+ enable
+ ..\binaries\ClientUI\
+ traffic.ico
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/ClientUI/Program.cs b/src/ClientUI/Program.cs
new file mode 100644
index 0000000..e2e6602
--- /dev/null
+++ b/src/ClientUI/Program.cs
@@ -0,0 +1,84 @@
+#pragma warning disable IDE0010
+namespace ClientUI;
+
+using Microsoft.Extensions.Hosting;
+using MassTransit;
+using Microsoft.Extensions.DependencyInjection;
+using System.Reflection;
+
+class Program
+{
+ public static IHostBuilder CreateHostBuilder(string[] args)
+ {
+ var host = Host.CreateDefaultBuilder(args)
+ .ConfigureServices((_, services) =>
+ {
+ services.AddMassTransit(x =>
+ {
+ x.UsingRabbitMq((context, cfg) =>
+ {
+ cfg.Host("localhost", "/", h =>
+ {
+ h.Username("guest");
+ h.Password("guest");
+ });
+
+ cfg.ConfigureEndpoints(context);
+ });
+
+ x.AddConfigureEndpointsCallback((name, cfg) =>
+ {
+ if (cfg is IRabbitMqReceiveEndpointConfigurator rmq)
+ {
+ rmq.SetQuorumQueue();
+ }
+ });
+
+ x.AddConsumers(Assembly.GetExecutingAssembly());
+ });
+
+ services.AddSingleton();
+ services.AddHostedService(p => p.GetRequiredService());
+ });
+
+ return host;
+ }
+
+ static async Task Main(string[] args)
+ {
+ Console.Title = "Load (ClientUI)";
+ Console.SetWindowSize(65, 15);
+
+ var host = CreateHostBuilder(args).Build();
+ await host.StartAsync();
+
+ var customers = host.Services.GetRequiredService();
+
+ await RunUserInterfaceLoop(customers);
+ }
+
+ static Task RunUserInterfaceLoop(SimulatedCustomers simulatedCustomers)
+ {
+ while (true)
+ {
+ Console.Clear();
+ Console.WriteLine("Simulating customers placing orders on a website");
+ Console.WriteLine("Press T to toggle High/Low traffic mode");
+ Console.WriteLine("Press ESC to quit");
+ Console.WriteLine();
+
+ simulatedCustomers.WriteState(Console.Out);
+
+ var input = Console.ReadKey(true);
+
+ switch (input.Key)
+ {
+ case ConsoleKey.T:
+ simulatedCustomers.ToggleTrafficMode();
+ break;
+ case ConsoleKey.Escape:
+ return Task.CompletedTask;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ClientUI/SimulatedCustomers.cs b/src/ClientUI/SimulatedCustomers.cs
new file mode 100644
index 0000000..d64ec7b
--- /dev/null
+++ b/src/ClientUI/SimulatedCustomers.cs
@@ -0,0 +1,74 @@
+namespace ClientUI;
+
+using MassTransit;
+using Messages;
+using Microsoft.Extensions.Hosting;
+
+class SimulatedCustomers(IBus _bus) : BackgroundService
+{
+ public void WriteState(TextWriter output)
+ {
+ var trafficMode = highTrafficMode ? "High" : "Low";
+ output.WriteLine($"{trafficMode} traffic mode - sending {rate} orders / second");
+ }
+
+ public void ToggleTrafficMode()
+ {
+ highTrafficMode = !highTrafficMode;
+ rate = highTrafficMode ? HightTrafficRate : LowTrafficRate;
+ }
+
+ Task PlaceSingleOrder(CancellationToken cancellationToken)
+ {
+ var placeOrderCommand = new PlaceOrder
+ {
+ OrderId = Guid.NewGuid().ToString()
+ };
+
+ return _bus.Publish(placeOrderCommand, cancellationToken);
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken cancellationToken)
+ {
+ nextReset = DateTime.UtcNow.AddSeconds(1);
+ currentIntervalCount = 0;
+
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ var now = DateTime.UtcNow;
+ if (now > nextReset)
+ {
+ currentIntervalCount = 0;
+ nextReset = now.AddSeconds(1);
+ }
+
+ await PlaceSingleOrder(cancellationToken);
+ currentIntervalCount++;
+
+ try
+ {
+ if (currentIntervalCount >= rate)
+ {
+ var delay = nextReset - DateTime.UtcNow;
+ if (delay > TimeSpan.Zero)
+ {
+ await Task.Delay(delay, cancellationToken);
+ }
+ }
+ }
+ catch (TaskCanceledException)
+ {
+ break;
+ }
+ }
+ }
+
+ bool highTrafficMode;
+
+ DateTime nextReset;
+ int currentIntervalCount;
+ int rate = LowTrafficRate;
+
+ const int HightTrafficRate = 8;
+ const int LowTrafficRate = 1;
+}
\ No newline at end of file
diff --git a/src/ClientUI/traffic.ico b/src/ClientUI/traffic.ico
new file mode 100644
index 0000000..d5dc024
Binary files /dev/null and b/src/ClientUI/traffic.ico differ
diff --git a/src/MassTransitShowcaseDemo.sln b/src/MassTransitShowcaseDemo.sln
new file mode 100644
index 0000000..9dd0615
--- /dev/null
+++ b/src/MassTransitShowcaseDemo.sln
@@ -0,0 +1,53 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.9.34902.65
+MinimumVisualStudioVersion = 15.0.26730.12
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7D29C905-CE3A-4D93-8271-7BA09CEE1631}"
+ ProjectSection(SolutionItems) = preProject
+ Directory.Build.props = Directory.Build.props
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClientUI", "ClientUI\ClientUI.csproj", "{918001C1-B9F6-4E81-894B-128271E8D910}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Messages", "Messages\Messages.csproj", "{CFF586B0-0FA1-4F3C-B860-44BE86B0F341}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sales", "Sales\Sales.csproj", "{6AD27F13-8B6B-4851-BBB8-A93D7FE463D9}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Billing", "Billing\Billing.csproj", "{709E5DF7-B76F-4FA6-BCB3-EF0C43C51FC6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shipping", "Shipping\Shipping.csproj", "{457FCA71-C1D9-43FF-838A-825A47E9E112}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {918001C1-B9F6-4E81-894B-128271E8D910}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {918001C1-B9F6-4E81-894B-128271E8D910}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {918001C1-B9F6-4E81-894B-128271E8D910}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {918001C1-B9F6-4E81-894B-128271E8D910}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CFF586B0-0FA1-4F3C-B860-44BE86B0F341}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CFF586B0-0FA1-4F3C-B860-44BE86B0F341}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CFF586B0-0FA1-4F3C-B860-44BE86B0F341}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CFF586B0-0FA1-4F3C-B860-44BE86B0F341}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6AD27F13-8B6B-4851-BBB8-A93D7FE463D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6AD27F13-8B6B-4851-BBB8-A93D7FE463D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6AD27F13-8B6B-4851-BBB8-A93D7FE463D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6AD27F13-8B6B-4851-BBB8-A93D7FE463D9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {709E5DF7-B76F-4FA6-BCB3-EF0C43C51FC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {709E5DF7-B76F-4FA6-BCB3-EF0C43C51FC6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {709E5DF7-B76F-4FA6-BCB3-EF0C43C51FC6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {709E5DF7-B76F-4FA6-BCB3-EF0C43C51FC6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {457FCA71-C1D9-43FF-838A-825A47E9E112}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {457FCA71-C1D9-43FF-838A-825A47E9E112}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {457FCA71-C1D9-43FF-838A-825A47E9E112}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {457FCA71-C1D9-43FF-838A-825A47E9E112}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {081FC59E-04F4-4FB2-88A6-64A7C18BAA10}
+ EndGlobalSection
+EndGlobal
diff --git a/src/Messages/.editorconfig b/src/Messages/.editorconfig
new file mode 100644
index 0000000..7333907
--- /dev/null
+++ b/src/Messages/.editorconfig
@@ -0,0 +1,7 @@
+[*.cs]
+
+# Justification: Test project
+dotnet_diagnostic.CA2007.severity = none
+
+# Justification: Tests don't support cancellation and don't need to forward IMessageHandlerContext.CancellationToken
+dotnet_diagnostic.NSB0002.severity = suggestion
\ No newline at end of file
diff --git a/src/Messages/Messages.csproj b/src/Messages/Messages.csproj
new file mode 100644
index 0000000..fa71b7a
--- /dev/null
+++ b/src/Messages/Messages.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/src/Messages/OrderBilled.cs b/src/Messages/OrderBilled.cs
new file mode 100644
index 0000000..87c9759
--- /dev/null
+++ b/src/Messages/OrderBilled.cs
@@ -0,0 +1,6 @@
+namespace Messages;
+
+public class OrderBilled
+{
+ public string? OrderId { get; set; }
+}
\ No newline at end of file
diff --git a/src/Messages/OrderPlaced.cs b/src/Messages/OrderPlaced.cs
new file mode 100644
index 0000000..413151c
--- /dev/null
+++ b/src/Messages/OrderPlaced.cs
@@ -0,0 +1,6 @@
+namespace Messages;
+
+public class OrderPlaced
+{
+ public string? OrderId { get; set; }
+}
\ No newline at end of file
diff --git a/src/Messages/PlaceOrder.cs b/src/Messages/PlaceOrder.cs
new file mode 100644
index 0000000..b5988cd
--- /dev/null
+++ b/src/Messages/PlaceOrder.cs
@@ -0,0 +1,6 @@
+namespace Messages;
+
+public class PlaceOrder
+{
+ public string? OrderId { get; set; }
+}
\ No newline at end of file
diff --git a/src/Sales/.editorconfig b/src/Sales/.editorconfig
new file mode 100644
index 0000000..7333907
--- /dev/null
+++ b/src/Sales/.editorconfig
@@ -0,0 +1,7 @@
+[*.cs]
+
+# Justification: Test project
+dotnet_diagnostic.CA2007.severity = none
+
+# Justification: Tests don't support cancellation and don't need to forward IMessageHandlerContext.CancellationToken
+dotnet_diagnostic.NSB0002.severity = suggestion
\ No newline at end of file
diff --git a/src/Sales/PlaceOrderHandler.cs b/src/Sales/PlaceOrderHandler.cs
new file mode 100644
index 0000000..b262b41
--- /dev/null
+++ b/src/Sales/PlaceOrderHandler.cs
@@ -0,0 +1,21 @@
+namespace Sales;
+
+using Messages;
+using MassTransit;
+using System.Threading.Tasks;
+
+public class PlaceOrderHandler(SimulationEffects simulationEffects) : IConsumer
+{
+ public async Task Consume(ConsumeContext context)
+ {
+ // Simulate the time taken to process a message
+ await simulationEffects.SimulateMessageProcessing(context.CancellationToken);
+
+ var orderPlaced = new OrderPlaced
+ {
+ OrderId = context.Message.OrderId
+ };
+
+ await context.Publish(orderPlaced);
+ }
+}
\ No newline at end of file
diff --git a/src/Sales/Program.cs b/src/Sales/Program.cs
new file mode 100644
index 0000000..5d70d82
--- /dev/null
+++ b/src/Sales/Program.cs
@@ -0,0 +1,89 @@
+#pragma warning disable IDE0010
+namespace Sales;
+
+using Microsoft.Extensions.Hosting;
+using MassTransit;
+using Microsoft.Extensions.DependencyInjection;
+using System.Security.Cryptography;
+using System.Text;
+using System.Reflection;
+
+class Program
+{
+ public static IHostBuilder CreateHostBuilder(string[] args)
+ {
+ var host = Host.CreateDefaultBuilder(args)
+ .ConfigureServices((hostContext, services) =>
+ {
+ services.AddMassTransit(x =>
+ {
+ x.UsingRabbitMq((context, cfg) =>
+ {
+ cfg.Host("localhost", "/", h =>
+ {
+ h.Username("guest");
+ h.Password("guest");
+ });
+
+ cfg.ConfigureEndpoints(context);
+ });
+
+ x.AddConsumers(Assembly.GetExecutingAssembly());
+
+ x.AddConfigureEndpointsCallback((name, cfg) =>
+ {
+ if (cfg is IRabbitMqReceiveEndpointConfigurator rmq)
+ {
+ rmq.SetQuorumQueue();
+ }
+ });
+ });
+
+ services.AddSingleton();
+ });
+
+ return host;
+ }
+
+ static async Task Main(string[] args)
+ {
+ Console.SetWindowSize(65, 15);
+ Console.Title = "Processing (Sales)";
+
+ var host = CreateHostBuilder(args).Build();
+ await host.StartAsync();
+
+ var state = host.Services.GetRequiredService();
+ await RunUserInterfaceLoop(state);
+ }
+
+ static Task RunUserInterfaceLoop(SimulationEffects state)
+ {
+ while (true)
+ {
+ Console.Clear();
+ Console.WriteLine($"Sales Endpoint");
+ Console.WriteLine("Press F to process messages faster");
+ Console.WriteLine("Press S to process messages slower");
+
+ Console.WriteLine("Press ESC to quit");
+ Console.WriteLine();
+
+ state.WriteState(Console.Out);
+
+ var input = Console.ReadKey(true);
+
+ switch (input.Key)
+ {
+ case ConsoleKey.F:
+ state.ProcessMessagesFaster();
+ break;
+ case ConsoleKey.S:
+ state.ProcessMessagesSlower();
+ break;
+ case ConsoleKey.Escape:
+ return Task.CompletedTask;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Sales/Sales.csproj b/src/Sales/Sales.csproj
new file mode 100644
index 0000000..e9da9a2
--- /dev/null
+++ b/src/Sales/Sales.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net8.0
+ Exe
+ enable
+ enable
+ ..\binaries\Sales\
+ processing-time-alternate.ico
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Sales/SimulationEffects.cs b/src/Sales/SimulationEffects.cs
new file mode 100644
index 0000000..4dfea0c
--- /dev/null
+++ b/src/Sales/SimulationEffects.cs
@@ -0,0 +1,30 @@
+namespace Sales;
+
+public class SimulationEffects
+{
+ public void WriteState(TextWriter output)
+ {
+ output.WriteLine("Base time to handle each order: {0} seconds", baseProcessingTime.TotalSeconds);
+ }
+
+ public Task SimulateMessageProcessing(CancellationToken cancellationToken = default)
+ {
+ return Task.Delay(baseProcessingTime, cancellationToken);
+ }
+
+ public void ProcessMessagesFaster()
+ {
+ if (baseProcessingTime > TimeSpan.Zero)
+ {
+ baseProcessingTime -= increment;
+ }
+ }
+
+ public void ProcessMessagesSlower()
+ {
+ baseProcessingTime += increment;
+ }
+
+ TimeSpan baseProcessingTime = TimeSpan.FromMilliseconds(1300);
+ TimeSpan increment = TimeSpan.FromMilliseconds(100);
+}
\ No newline at end of file
diff --git a/src/Sales/processing-time-alternate.ico b/src/Sales/processing-time-alternate.ico
new file mode 100644
index 0000000..776266f
Binary files /dev/null and b/src/Sales/processing-time-alternate.ico differ
diff --git a/src/Shipping/.editorconfig b/src/Shipping/.editorconfig
new file mode 100644
index 0000000..7333907
--- /dev/null
+++ b/src/Shipping/.editorconfig
@@ -0,0 +1,7 @@
+[*.cs]
+
+# Justification: Test project
+dotnet_diagnostic.CA2007.severity = none
+
+# Justification: Tests don't support cancellation and don't need to forward IMessageHandlerContext.CancellationToken
+dotnet_diagnostic.NSB0002.severity = suggestion
\ No newline at end of file
diff --git a/src/Shipping/OrderBilledHandler.cs b/src/Shipping/OrderBilledHandler.cs
new file mode 100644
index 0000000..09ba7d9
--- /dev/null
+++ b/src/Shipping/OrderBilledHandler.cs
@@ -0,0 +1,13 @@
+namespace Shipping;
+
+using System.Threading.Tasks;
+using MassTransit;
+using Messages;
+
+public class OrderBilledHandler(SimulationEffects simulationEffects) : IConsumer
+{
+ public Task Consume(ConsumeContext context)
+ {
+ return simulationEffects.SimulateOrderBilledMessageProcessing(context.CancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/src/Shipping/OrderPlacedHandler.cs b/src/Shipping/OrderPlacedHandler.cs
new file mode 100644
index 0000000..53e06b7
--- /dev/null
+++ b/src/Shipping/OrderPlacedHandler.cs
@@ -0,0 +1,13 @@
+namespace Shipping;
+
+using System.Threading.Tasks;
+using MassTransit;
+using Messages;
+
+public class OrderPlacedHandler(SimulationEffects simulationEffects) : IConsumer
+{
+ public Task Consume(ConsumeContext context)
+ {
+ return simulationEffects.SimulateOrderPlacedMessageProcessing(context.CancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/src/Shipping/Program.cs b/src/Shipping/Program.cs
new file mode 100644
index 0000000..14ccc4b
--- /dev/null
+++ b/src/Shipping/Program.cs
@@ -0,0 +1,93 @@
+#pragma warning disable IDE0010
+namespace Shipping;
+
+using Microsoft.Extensions.Hosting;
+using MassTransit;
+using Microsoft.Extensions.DependencyInjection;
+using System.Security.Cryptography;
+using System.Text;
+using System.Reflection;
+
+class Program
+{
+ public static IHostBuilder CreateHostBuilder(string[] args)
+ {
+ var host = Host.CreateDefaultBuilder(args)
+ .ConfigureServices((_, services) =>
+ {
+ services.AddMassTransit(x =>
+ {
+ x.UsingRabbitMq((context, cfg) =>
+ {
+ cfg.Host("localhost", "/", h =>
+ {
+ h.Username("guest");
+ h.Password("guest");
+ });
+
+ cfg.ConfigureEndpoints(context);
+ });
+
+ x.AddConsumers(Assembly.GetExecutingAssembly());
+
+ x.AddConfigureEndpointsCallback((name, cfg) =>
+ {
+ if (cfg is IRabbitMqReceiveEndpointConfigurator rmq)
+ {
+ rmq.SetQuorumQueue();
+ }
+ });
+ });
+
+ services.AddSingleton();
+ });
+
+ return host;
+ }
+
+ static async Task Main(string[] args)
+ {
+ Console.SetWindowSize(65, 15);
+
+ Console.Title = "Processing (Shipping)";
+
+ var host = CreateHostBuilder(args).Build();
+ await host.StartAsync();
+
+ var state = host.Services.GetRequiredService();
+ await RunUserInterfaceLoop(state);
+ }
+
+ static Task RunUserInterfaceLoop(SimulationEffects state)
+ {
+ while (true)
+ {
+ Console.Clear();
+ Console.WriteLine("Shipping Endpoint");
+ Console.WriteLine("Press D to toggle resource degradation simulation");
+ Console.WriteLine("Press F to process OrderBilled events faster");
+ Console.WriteLine("Press S to process OrderBilled events slower");
+ Console.WriteLine("Press ESC to quit");
+ Console.WriteLine();
+
+ state.WriteState(Console.Out);
+
+ var input = Console.ReadKey(true);
+
+ switch (input.Key)
+ {
+ case ConsoleKey.D:
+ state.ToggleDegradationSimulation();
+ break;
+ case ConsoleKey.F:
+ state.ProcessMessagesFaster();
+ break;
+ case ConsoleKey.S:
+ state.ProcessMessagesSlower();
+ break;
+ case ConsoleKey.Escape:
+ return Task.CompletedTask;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Shipping/Shipping.csproj b/src/Shipping/Shipping.csproj
new file mode 100644
index 0000000..31e0112
--- /dev/null
+++ b/src/Shipping/Shipping.csproj
@@ -0,0 +1,21 @@
+
+
+
+ net8.0
+ Exe
+ enable
+ enable
+ ..\binaries\Shipping\
+ processing-time.ico
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Shipping/SimulationEffects.cs b/src/Shipping/SimulationEffects.cs
new file mode 100644
index 0000000..35d7dcf
--- /dev/null
+++ b/src/Shipping/SimulationEffects.cs
@@ -0,0 +1,58 @@
+namespace Shipping;
+
+public class SimulationEffects
+{
+ public void WriteState(TextWriter output)
+ {
+ output.WriteLine("Base time to handle each OrderBilled event: {0} seconds", baseProcessingTime.TotalSeconds);
+
+ output.Write("Simulated degrading resource: ");
+ output.WriteLine(degradingResourceSimulationStarted.HasValue ? "ON" : "OFF");
+ }
+
+ public Task SimulateOrderBilledMessageProcessing(CancellationToken cancellationToken = default)
+ {
+ return Task.Delay(baseProcessingTime, cancellationToken);
+ }
+
+ public void ProcessMessagesFaster()
+ {
+ if (baseProcessingTime > TimeSpan.Zero)
+ {
+ baseProcessingTime -= increment;
+ }
+ }
+
+ public void ProcessMessagesSlower()
+ {
+ baseProcessingTime += increment;
+ }
+
+ public Task SimulateOrderPlacedMessageProcessing(CancellationToken cancellationToken = default)
+ {
+ var delay = TimeSpan.FromMilliseconds(200) + Degradation();
+ return Task.Delay(delay, cancellationToken);
+ }
+
+ public void ToggleDegradationSimulation()
+ {
+ degradingResourceSimulationStarted = degradingResourceSimulationStarted.HasValue ? default(DateTime?) : DateTime.UtcNow;
+ }
+
+ TimeSpan Degradation()
+ {
+ var timeSinceDegradationStarted = DateTime.UtcNow - (degradingResourceSimulationStarted ?? DateTime.MaxValue);
+ if (timeSinceDegradationStarted < TimeSpan.Zero)
+ {
+ return TimeSpan.Zero;
+ }
+
+ return new TimeSpan(timeSinceDegradationStarted.Ticks / degradationRate);
+ }
+
+ TimeSpan baseProcessingTime = TimeSpan.FromMilliseconds(700);
+ TimeSpan increment = TimeSpan.FromMilliseconds(100);
+
+ DateTime? degradingResourceSimulationStarted;
+ const int degradationRate = 5;
+}
\ No newline at end of file
diff --git a/src/Shipping/processing-time.ico b/src/Shipping/processing-time.ico
new file mode 100644
index 0000000..afd6aba
Binary files /dev/null and b/src/Shipping/processing-time.ico differ