├── src
├── Website
│ ├── wwwroot
│ │ ├── js
│ │ │ └── site.js
│ │ ├── favicon.ico
│ │ └── css
│ │ │ └── site.css
│ ├── Views
│ │ ├── _ViewStart.cshtml
│ │ ├── _ViewImports.cshtml
│ │ ├── Reservations
│ │ │ ├── CheckedOut.cshtml
│ │ │ ├── Added.cshtml
│ │ │ ├── Review.cshtml
│ │ │ └── Index.cshtml
│ │ ├── Shared
│ │ │ ├── _ValidationScriptsPartial.cshtml
│ │ │ └── _Layout.cshtml
│ │ └── Home
│ │ │ └── Index.cshtml
│ ├── appsettings.json
│ ├── appsettings.Development.json
│ ├── Models
│ │ └── ErrorViewModel.cs
│ ├── Controllers
│ │ ├── HomeController.cs
│ │ └── ReservationsController.cs
│ ├── Properties
│ │ └── launchSettings.json
│ ├── libman.json
│ ├── Program.cs
│ ├── Startup.cs
│ └── Website.csproj
├── Directory.Build.props
├── Finance.Service
│ ├── Properties
│ │ └── launchSettings.json
│ ├── Messages
│ │ ├── InitiatePaymentProcessing.cs
│ │ ├── PaymentAuthorized.cs
│ │ └── PaymentSucceeded.cs
│ ├── Handlers
│ │ └── StoreReservedTicketHandler.cs
│ ├── Finance.Service.csproj
│ ├── Program.cs
│ └── Policies
│ │ └── PaymentPolicy.cs
├── Shipping.Service
│ ├── Properties
│ │ └── launchSettings.json
│ ├── Messages
│ │ └── OrderShipped.cs
│ ├── Handlers
│ │ └── StoreReservationForVenueDeliveryHandler.cs
│ ├── Shipping.Service.csproj
│ ├── Program.cs
│ └── Policies
│ │ └── ShippingPolicy.cs
├── Finance.PaymentGateway
│ ├── Properties
│ │ └── launchSettings.json
│ ├── Finance.PaymentGateway.csproj
│ ├── Handlers
│ │ ├── ReleaseCardAuthorizationHandler.cs
│ │ ├── ChargeCardHandler.cs
│ │ └── AuthorizeCardHandler.cs
│ └── Program.cs
├── Reservations.Service
│ ├── Properties
│ │ └── launchSettings.json
│ ├── Messages
│ │ ├── MarkTicketAsReserved.cs
│ │ ├── OrderCreated.cs
│ │ └── ReservationCheckedout.cs
│ ├── Reservations.Service.csproj
│ ├── Program.cs
│ ├── Handlers
│ │ ├── MarkTicketAsReservedHandler.cs
│ │ ├── CheckoutReservationHandler.cs
│ │ └── ReservationCheckedoutHandler.cs
│ └── Policies
│ │ └── ReservationPolicy.cs
├── Shipping.Data
│ ├── DeliveryOptions.cs
│ └── Shipping.Data.csproj
├── Finance.Messages.Events
│ ├── Finance.Messages.Events.csproj
│ ├── IPaymentAuthorized.cs
│ └── IPaymentSucceeded.cs
├── Shipping.Messages.Events
│ ├── Shipping.Messages.Events.csproj
│ └── IOrderShipped.cs
├── Finance.Messages.Commands
│ ├── Finance.Messages.Commands.csproj
│ ├── StoreReservedTicket.cs
│ └── InitializeReservationPaymentPolicy.cs
├── Ticketing.Data
│ ├── Models
│ │ └── Ticket.cs
│ ├── Ticketing.Data.csproj
│ └── TicketingContext.cs
├── Finance.Data
│ ├── Models
│ │ ├── TicketPrice.cs
│ │ ├── PaymentMethod.cs
│ │ └── ReservedTicket.cs
│ ├── Finance.Data.csproj
│ └── FinanceContext.cs
├── Reservations.Messages.Events
│ ├── Reservations.Messages.Events.csproj
│ ├── IOrderCreated.cs
│ └── IReservationCheckedout.cs
├── Finance.PaymentGateway.Messages
│ ├── Finance.PaymentGateway.Messages.csproj
│ ├── CardChargedResponse.cs
│ ├── ChargeCard.cs
│ ├── AuthorizeCard.cs
│ ├── CardAuthorizedResponse.cs
│ └── ReleaseCardAuthorization.cs
├── Reservations.Messages.Commands
│ ├── Reservations.Messages.Commands.csproj
│ ├── CheckoutReservation.cs
│ └── ReserveTicket.cs
├── Reservations.Data
│ ├── Models
│ │ ├── AvailableTickets.cs
│ │ ├── Reservation.cs
│ │ └── Order.cs
│ ├── Reservations.Data.csproj
│ └── ReservationsContext.cs
├── Ticketing.ViewModelComposition.Events
│ ├── Ticketing.ViewModelComposition.Events.csproj
│ └── AvailableTicketsLoaded.cs
├── Reservations.ViewModelComposition.Events
│ ├── Reservations.ViewModelComposition.Events.csproj
│ └── ReservedTicketsLoaded.cs
├── Shipping.Messages.Commands
│ ├── Shipping.Messages.Commands.csproj
│ ├── InitializeReservationShippingPolicy.cs
│ └── StoreReservationForVenueDelivery.cs
├── .run
│ └── Platform demo.run.xml
├── welcome-to-the-state-machine-demos.sln.startup.json
├── Finance.ViewModelComposition
│ ├── ReservationsFinalizePostHandler.cs
│ ├── Finance.ViewModelComposition.csproj
│ ├── ReservationsCheckedoutGetHandler.cs
│ ├── AvailableTicketsLoadedSubscriber.cs
│ ├── ReservationsCheckoutPostHandler.cs
│ ├── ReservationsReservePostHandler.cs
│ ├── ReservedTicketsLoadedSubscriber.cs
│ └── ReviewReservedTicketsLoadedSubscriber.cs
├── CreateRequiredDatabases
│ ├── CreateRequiredDatabases.csproj
│ └── Program.cs
├── NServiceBus.Shared
│ ├── NServiceBus.Shared.csproj
│ └── CommonEndpointSettings.cs
├── Shipping.ViewModelComposition
│ ├── ReservationsFinalizePostHandler.cs
│ ├── Shipping.ViewModelComposition.csproj
│ ├── ReservationsCheckedoutGetHandler.cs
│ ├── ReservedTicketsLoadedSubscriber.cs
│ ├── ReviewReservedTicketsLoadedSubscriber.cs
│ └── ReservationsCheckoutPostHandler.cs
├── Ticketing.ViewModelComposition
│ ├── Ticketing.ViewModelComposition.csproj
│ ├── ReservedTicketsLoadedSubscriber.cs
│ └── AvailableTicketsGetHandler.cs
├── Reservations.ViewModelComposition
│ ├── Reservations.ViewModelComposition.csproj
│ ├── Middlewares
│ │ └── ReservationMiddleware.cs
│ ├── ReservationsCheckedoutGetHandler.cs
│ ├── ReservationsCheckoutPostHandler.cs
│ ├── ReservationsReservePostHandler.cs
│ ├── AvailableTicketsLoadedSubscriber.cs
│ └── TicketsReservationGetHandler.cs
└── welcome-to-the-state-machine-demos.sln
├── .devcontainer
├── finance-database.env
├── ticketing-database.env
├── reservations-database.env
├── finance-service-database.env
├── shipping-service-database.env
├── reservations-service-database.env
├── devcontainer.json
└── docker-compose.yml
├── .github
├── dependabot.yml
└── workflows
│ ├── ci.yml
│ └── dependabot-auto-merge.yml
├── .gitignore
├── .vscode
├── tasks.json
└── launch.json
├── LICENSE
├── .gitattributes
├── README.md
└── .editorconfig
/src/Website/wwwroot/js/site.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/Website/Views/_ViewStart.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | Layout = "_Layout";
3 | }
4 |
--------------------------------------------------------------------------------
/.devcontainer/finance-database.env:
--------------------------------------------------------------------------------
1 | POSTGRES_USER=db_user
2 | POSTGRES_PASSWORD=P@ssw0rd
3 | POSTGRES_DB=finance_database
4 | PGPORT=6432
--------------------------------------------------------------------------------
/.devcontainer/ticketing-database.env:
--------------------------------------------------------------------------------
1 | POSTGRES_USER=db_user
2 | POSTGRES_PASSWORD=P@ssw0rd
3 | POSTGRES_DB=ticketing_database
4 | PGPORT=5432
--------------------------------------------------------------------------------
/src/Website/Views/_ViewImports.cshtml:
--------------------------------------------------------------------------------
1 | @using Website
2 | @using Website.Models
3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
4 |
--------------------------------------------------------------------------------
/.devcontainer/reservations-database.env:
--------------------------------------------------------------------------------
1 | POSTGRES_USER=db_user
2 | POSTGRES_PASSWORD=P@ssw0rd
3 | POSTGRES_DB=reservations_database
4 | PGPORT=8432
--------------------------------------------------------------------------------
/src/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | latest
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/Website/wwwroot/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mauroservienti/welcome-to-the-state-machine-demos/HEAD/src/Website/wwwroot/favicon.ico
--------------------------------------------------------------------------------
/.devcontainer/finance-service-database.env:
--------------------------------------------------------------------------------
1 | POSTGRES_USER=db_user
2 | POSTGRES_PASSWORD=P@ssw0rd
3 | POSTGRES_DB=finance_service_database
4 | PGPORT=7432
--------------------------------------------------------------------------------
/.devcontainer/shipping-service-database.env:
--------------------------------------------------------------------------------
1 | POSTGRES_USER=db_user
2 | POSTGRES_PASSWORD=P@ssw0rd
3 | POSTGRES_DB=shipping_service_database
4 | PGPORT=10432
--------------------------------------------------------------------------------
/src/Website/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Warning"
5 | }
6 | },
7 | "AllowedHosts": "*"
8 | }
9 |
--------------------------------------------------------------------------------
/.devcontainer/reservations-service-database.env:
--------------------------------------------------------------------------------
1 | POSTGRES_USER=db_user
2 | POSTGRES_PASSWORD=P@ssw0rd
3 | POSTGRES_DB=reservations_service_database
4 | PGPORT=9432
--------------------------------------------------------------------------------
/src/Finance.Service/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "Finance.Service": {
4 | "commandName": "Project"
5 | }
6 | }
7 | }
--------------------------------------------------------------------------------
/src/Shipping.Service/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "Shipping.Service": {
4 | "commandName": "Project"
5 | }
6 | }
7 | }
--------------------------------------------------------------------------------
/src/Finance.PaymentGateway/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "Finance.PaymentGateway": {
4 | "commandName": "Project"
5 | }
6 | }
7 | }
--------------------------------------------------------------------------------
/src/Reservations.Service/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "Reservations.Service": {
4 | "commandName": "Project"
5 | }
6 | }
7 | }
--------------------------------------------------------------------------------
/src/Shipping.Data/DeliveryOptions.cs:
--------------------------------------------------------------------------------
1 | namespace Shipping.Data
2 | {
3 | public enum DeliveryOptions
4 | {
5 | ShipAtHome,
6 | CollectAtTheVenue
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/Shipping.Data/Shipping.Data.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/Finance.Messages.Events/Finance.Messages.Events.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/Shipping.Messages.Events/Shipping.Messages.Events.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/Finance.Messages.Commands/Finance.Messages.Commands.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/Ticketing.Data/Models/Ticket.cs:
--------------------------------------------------------------------------------
1 | namespace Ticketing.Data.Models
2 | {
3 | public class Ticket
4 | {
5 | public int Id { get; set; }
6 | public string Description { get; set; }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/Website/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Debug",
5 | "System": "Information",
6 | "Microsoft": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Finance.Data/Models/TicketPrice.cs:
--------------------------------------------------------------------------------
1 | namespace Finance.Data.Models
2 | {
3 | public class TicketPrice
4 | {
5 | public int Id { get; set; }
6 | public decimal Price { get; set; }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/Reservations.Messages.Events/Reservations.Messages.Events.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/Finance.Data/Models/PaymentMethod.cs:
--------------------------------------------------------------------------------
1 | namespace Finance.Data.Models
2 | {
3 | public class PaymentMethod
4 | {
5 | public int Id { get; set; }
6 | public string Description { get; set; }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/Finance.PaymentGateway.Messages/Finance.PaymentGateway.Messages.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/Reservations.Messages.Commands/Reservations.Messages.Commands.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/Finance.Messages.Events/IPaymentAuthorized.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Finance.Messages.Events
4 | {
5 | public interface IPaymentAuthorized
6 | {
7 | Guid ReservationId { get; }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Reservations.Data/Models/AvailableTickets.cs:
--------------------------------------------------------------------------------
1 | namespace Reservations.Data.Models
2 | {
3 | public class AvailableTickets
4 | {
5 | public int Id { get; set; }
6 | public int TotalTickets { get; set; }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/Ticketing.ViewModelComposition.Events/Ticketing.ViewModelComposition.Events.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/Finance.Service/Messages/InitiatePaymentProcessing.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Finance.Service.Messages
4 | {
5 | class InitiatePaymentProcessing
6 | {
7 | public Guid ReservationId { get; set; }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Reservations.ViewModelComposition.Events/Reservations.ViewModelComposition.Events.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/Website/Models/ErrorViewModel.cs:
--------------------------------------------------------------------------------
1 | namespace Website.Models
2 | {
3 | public class ErrorViewModel
4 | {
5 | public string RequestId { get; set; }
6 |
7 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
8 | }
9 | }
--------------------------------------------------------------------------------
/src/Finance.PaymentGateway.Messages/CardChargedResponse.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Finance.PaymentGateway.Messages
4 | {
5 | public class CardChargedResponse
6 | {
7 | public Guid ReservationId { get; set; }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Reservations.Messages.Commands/CheckoutReservation.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Reservations.Messages.Commands
4 | {
5 | public class CheckoutReservation
6 | {
7 | public Guid ReservationId { get; set; }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Website/Views/Reservations/CheckedOut.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | ViewData["Title"] = "Thank you!";
3 | }
4 |
5 |
6 | Thank you for your order. At this point I'm too lazy to create the "Your orders page".
7 | It'll be delivered, be faithful ;-)
8 |
--------------------------------------------------------------------------------
/src/Finance.Messages.Events/IPaymentSucceeded.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Finance.Messages.Events
4 | {
5 | public interface IPaymentSucceeded
6 | {
7 | Guid ReservationId { get; }
8 | Guid OrderId { get; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Reservations.Messages.Events/IOrderCreated.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Reservations.Messages.Events
4 | {
5 | public interface IOrderCreated
6 | {
7 | Guid ReservationId { get; }
8 | Guid OrderId { get; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Reservations.Messages.Events/IReservationCheckedout.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Reservations.Messages.Events
4 | {
5 | public interface IReservationCheckedout
6 | {
7 | Guid ReservationId { get; }
8 | int[] Tickets { get; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Finance.Messages.Commands/StoreReservedTicket.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Finance.Messages.Commands
4 | {
5 | public class StoreReservedTicket
6 | {
7 | public int TicketId { get; set; }
8 | public Guid ReservationId { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Finance.Service/Messages/PaymentAuthorized.cs:
--------------------------------------------------------------------------------
1 | using Finance.Messages.Events;
2 | using System;
3 |
4 | namespace Finance.Service.Messages
5 | {
6 | class PaymentAuthorized : IPaymentAuthorized
7 | {
8 | public Guid ReservationId { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Reservations.Messages.Commands/ReserveTicket.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Reservations.Messages.Commands
4 | {
5 | public class ReserveTicket
6 | {
7 | public int TicketId { get; set; }
8 | public Guid ReservationId { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Finance.PaymentGateway.Messages/ChargeCard.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Finance.PaymentGateway.Messages
4 | {
5 | public class ChargeCard
6 | {
7 | public Guid ReservationId { get; set; }
8 | public Guid AuthorizationId { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Reservations.Service/Messages/MarkTicketAsReserved.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Reservations.Service.Messages
4 | {
5 | class MarkTicketAsReserved
6 | {
7 | public int TicketId { get; set; }
8 | public Guid ReservationId { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Shipping.Messages.Events/IOrderShipped.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Shipping.Messages.Events
4 | {
5 | public interface IOrderShipped
6 | {
7 | Guid ReservationId { get; }
8 | Guid OrderId { get; }
9 | Guid ShipmentId { get; }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/Finance.PaymentGateway.Messages/AuthorizeCard.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Finance.PaymentGateway.Messages
4 | {
5 | public class AuthorizeCard
6 | {
7 | public Guid ReservationId { get; set; }
8 | public int PaymentMethodId { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Finance.Data/Models/ReservedTicket.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Finance.Data.Models
4 | {
5 | public class ReservedTicket
6 | {
7 | public int Id { get; set; }
8 | public Guid ReservationId { get; set; }
9 | public int TicketId { get; set; }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/Finance.PaymentGateway.Messages/CardAuthorizedResponse.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Finance.PaymentGateway.Messages
4 | {
5 | public class CardAuthorizedResponse
6 | {
7 | public Guid ReservationId { get; set; }
8 | public Guid AuthorizationId { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Finance.PaymentGateway.Messages/ReleaseCardAuthorization.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Finance.PaymentGateway.Messages
4 | {
5 | public class ReleaseCardAuthorization
6 | {
7 | public Guid ReservationId { get; set; }
8 | public Guid AuthorizationId { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Ticketing.ViewModelComposition.Events/AvailableTicketsLoaded.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace Ticketing.ViewModelComposition.Events
4 | {
5 | public class AvailableTicketsLoaded
6 | {
7 | public IDictionary AvailableTicketsViewModel { get; set; }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Finance.Messages.Commands/InitializeReservationPaymentPolicy.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Finance.Messages.Commands
4 | {
5 | public class InitializeReservationPaymentPolicy
6 | {
7 | public Guid ReservationId { get; set; }
8 | public int PaymentMethodId { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Finance.Service/Messages/PaymentSucceeded.cs:
--------------------------------------------------------------------------------
1 | using Finance.Messages.Events;
2 | using System;
3 |
4 | namespace Finance.Service.Messages
5 | {
6 | class PaymentSucceeded : IPaymentSucceeded
7 | {
8 | public Guid ReservationId { get; set; }
9 | public Guid OrderId { get; set; }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/Website/Controllers/HomeController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 |
3 | namespace Website.Controllers
4 | {
5 | public class HomeController : Controller
6 | {
7 | [HttpGet("/")]
8 | public IActionResult Index()
9 | {
10 | return View();
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Finance.Data/Finance.Data.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/Reservations.Service/Messages/OrderCreated.cs:
--------------------------------------------------------------------------------
1 | using Reservations.Messages.Events;
2 | using System;
3 |
4 | namespace Reservations.Service.Messages
5 | {
6 | class OrderCreated : IOrderCreated
7 | {
8 | public Guid ReservationId { get; set; }
9 | public Guid OrderId { get; set; }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/Website/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "Website": {
4 | "commandName": "Project",
5 | "launchBrowser": true,
6 | "environmentVariables": {
7 | "ASPNETCORE_ENVIRONMENT": "Development"
8 | },
9 | "applicationUrl": "http://localhost:5010"
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/src/Ticketing.Data/Ticketing.Data.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/Reservations.Data/Reservations.Data.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/Shipping.Messages.Commands/Shipping.Messages.Commands.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/Reservations.Service/Messages/ReservationCheckedout.cs:
--------------------------------------------------------------------------------
1 | using Reservations.Messages.Events;
2 | using System;
3 |
4 | namespace Reservations.Service.Messages
5 | {
6 | class ReservationCheckedout : IReservationCheckedout
7 | {
8 | public Guid ReservationId { get; set; }
9 | public int[] Tickets { get; set; }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: nuget
4 | directory: "/src"
5 | schedule:
6 | interval: daily
7 | time: '04:00'
8 | open-pull-requests-limit: 10
9 | - package-ecosystem: github-actions
10 | directory: "/"
11 | schedule:
12 | interval: daily
13 | time: '04:00'
14 | open-pull-requests-limit: 10
15 |
--------------------------------------------------------------------------------
/src/Shipping.Messages.Commands/InitializeReservationShippingPolicy.cs:
--------------------------------------------------------------------------------
1 | using Shipping.Data;
2 | using System;
3 |
4 | namespace Shipping.Messages.Commands
5 | {
6 | public class InitializeReservationShippingPolicy
7 | {
8 | public Guid ReservationId { get; set; }
9 | public DeliveryOptions DeliveryOption { get; set; }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/Shipping.Messages.Commands/StoreReservationForVenueDelivery.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace Shipping.Messages.Commands
6 | {
7 | public class StoreReservationForVenueDelivery
8 | {
9 | public Guid OrderId { get; set; }
10 | public Guid ReservationId { get; set; }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/Reservations.ViewModelComposition.Events/ReservedTicketsLoaded.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace Reservations.ViewModelComposition.Events
4 | {
5 | public class ReservedTicketsLoaded
6 | {
7 | public IDictionary ReservedTicketsViewModel { get; set; }
8 | public dynamic Reservation { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Shipping.Service/Messages/OrderShipped.cs:
--------------------------------------------------------------------------------
1 | using Shipping.Messages.Events;
2 | using System;
3 |
4 | namespace Shipping.Service.Messages
5 | {
6 | class OrderShipped : IOrderShipped
7 | {
8 | public Guid ReservationId { get; set; }
9 |
10 | public Guid OrderId { get; set; }
11 |
12 | public Guid ShipmentId { get; set; }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "dockerComposeFile": "docker-compose.yml",
3 | "service": "demo",
4 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
5 | "forwardPorts": [ 15672 ],
6 | "customizations": {
7 | "vscode": {
8 | "extensions": [
9 | "ms-dotnettools.csharp",
10 | "ckolkman.vscode-postgres"
11 | ]
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/src/Website/libman.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0",
3 | "defaultProvider": "cdnjs",
4 | "libraries": [
5 | {
6 | "library": "jquery@3.4.1",
7 | "destination": "wwwroot/lib/jquery/dist",
8 | "files": [
9 | "jquery.js"
10 | ]
11 | },
12 | {
13 | "library": "twitter-bootstrap@4.3.1",
14 | "destination": "wwwroot/lib/bootstrap/dist/",
15 | "files": [
16 | "css/bootstrap.css",
17 | "js/bootstrap.bundle.js"
18 | ]
19 | }
20 | ]
21 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # NServiceBus learning transport storage
2 | .learningtransport/
3 |
4 | # Front-end libs
5 | **/wwwroot/lib/
6 |
7 | # User-specific files
8 | *.suo
9 | *.user
10 |
11 | # Build dependencies and artifacts
12 | .dotnet/
13 | .build/
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Rr]elease/
18 | [Rr]eleases/
19 | x64/
20 | x86/
21 | [Bb]in/
22 | [Oo]bj/
23 |
24 | # Visual Studio cache/options directory
25 | .vs/
26 |
27 | # JetBrains cache/options directory
28 | .idea
29 |
30 | # macOS garbage
31 | .DS_Store
32 |
--------------------------------------------------------------------------------
/src/Website/Views/Reservations/Added.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | ViewData["Title"] = "Thank you!";
3 | }
4 |
5 |
6 | Thank you for your reservation. Your tickets will be reserved for 5 minutes, be quick or be dead... (cit.)
7 | This is a lie. The reservation expiration is not implemented, so basically you can toast the system by simply reserving tickets...
8 | this is not fun, it's a demo ;-)
9 | Proceed to the
your reservations page to complete the purchase process.
10 |
--------------------------------------------------------------------------------
/src/Reservations.Data/Models/Reservation.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace Reservations.Data.Models
5 | {
6 | public class Reservation
7 | {
8 | public Guid Id { get; set; }
9 | public List ReservedTickets { get; set; } = new List();
10 | }
11 |
12 | public class ReservedTicket
13 | {
14 | public int Id { get; set; }
15 | public Guid ReservationId { get; set; }
16 | public int TicketId { get; set; }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/.run/Platform demo.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/Reservations.Data/Models/Order.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace Reservations.Data.Models
5 | {
6 | public class Order
7 | {
8 | public Guid Id { get; set; }
9 | public Guid ReservationId { get; set; }
10 | public List OrderedTickets { get; set; } = new List();
11 | }
12 |
13 | public class OrderedTicket
14 | {
15 | public int Id { get; set; }
16 | public Guid OrderId { get; set; }
17 | public int TicketId { get; set; }
18 | public int Quantity { get; set; }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/welcome-to-the-state-machine-demos.sln.startup.json:
--------------------------------------------------------------------------------
1 | {
2 | "Version": 3,
3 | "ListAllProjects": false,
4 | "MultiProjectConfigurations": {
5 | "Demo": {
6 | "Projects": {
7 | "website": {},
8 | "Reservations.Service": {},
9 | "Finance.Service": {},
10 | "Shipping.Service": {},
11 | "Finance.PaymentGateway": {}
12 | }
13 | },
14 | "Demo (Platform)": {
15 | "Projects": {
16 | "website": {},
17 | "Reservations.Service": {},
18 | "Finance.Service": {},
19 | "Shipping.Service": {},
20 | "Finance.PaymentGateway": {},
21 | "PlatformLauncher": {}
22 | }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Finance.ViewModelComposition/ReservationsFinalizePostHandler.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using ServiceComposer.AspNetCore;
3 | using System.Threading.Tasks;
4 | using Microsoft.AspNetCore.Mvc;
5 |
6 | namespace Finance.ViewModelComposition
7 | {
8 | class ReservationsFinalizePostHandler : ICompositionRequestsHandler
9 | {
10 | [HttpPost("/reservations/finalize")]
11 | public Task Handle(HttpRequest request)
12 | {
13 | var response = request.HttpContext.Response;
14 | response.Cookies.Append("reservation-payment-method-id", request.Form["PaymentMethod"]);
15 |
16 | return Task.CompletedTask;
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/CreateRequiredDatabases/CreateRequiredDatabases.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0
6 | enable
7 | enable
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/NServiceBus.Shared/NServiceBus.Shared.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/Shipping.ViewModelComposition/ReservationsFinalizePostHandler.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using Microsoft.AspNetCore.Routing;
3 | using ServiceComposer.AspNetCore;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Mvc;
6 |
7 | namespace Shipping.ViewModelComposition
8 | {
9 | class ReservationsFinalizePostHandler : ICompositionRequestsHandler
10 | {
11 | [HttpPost("/reservations/finalize")]
12 | public Task Handle(HttpRequest request)
13 | {
14 | var response = request.HttpContext.Response;
15 | response.Cookies.Append("reservation-delivery-option-id", request.Form["DeliveryOption"]);
16 |
17 | return Task.CompletedTask;
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Finance.PaymentGateway/Finance.PaymentGateway.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0
6 | latest
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/Ticketing.ViewModelComposition/Ticketing.ViewModelComposition.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches:
5 | - master
6 | pull_request:
7 | workflow_dispatch:
8 | env:
9 | DOTNET_NOLOGO: true
10 | jobs:
11 | build:
12 | name: Build and test on ${{ matrix.name }}
13 | runs-on: ${{ matrix.os }}
14 | strategy:
15 | matrix:
16 | include:
17 | - os: ubuntu-latest
18 | name: Linux
19 | fail-fast: false
20 | steps:
21 | - name: Checkout
22 | uses: actions/checkout@v6.0.1
23 | with:
24 | fetch-depth: 0
25 | - name: Setup .NET SDK
26 | uses: actions/setup-dotnet@v5.0.1
27 | with:
28 | dotnet-version: 8.0.x
29 | - name: Build solution
30 | run: dotnet build "src/welcome-to-the-state-machine-demos.sln" --configuration Release
31 |
--------------------------------------------------------------------------------
/src/Shipping.ViewModelComposition/Shipping.ViewModelComposition.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/Website/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Hosting;
2 | using Microsoft.Extensions.Hosting;
3 | using NServiceBus;
4 |
5 | namespace Website
6 | {
7 | public class Program
8 | {
9 | public static void Main(string[] args)
10 | {
11 | CreateWebHostBuilder(args).Build().Run();
12 | }
13 |
14 | public static IHostBuilder CreateWebHostBuilder(string[] args) =>
15 | Host.CreateDefaultBuilder(args)
16 | .UseNServiceBus(ctx =>
17 | {
18 | var config = new EndpointConfiguration("Webapp");
19 | config.ApplyCommonConfiguration();
20 |
21 | return config;
22 | })
23 | .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); });
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/.github/workflows/dependabot-auto-merge.yml:
--------------------------------------------------------------------------------
1 | name: Dependabot
2 | on: pull_request
3 |
4 | permissions:
5 | pull-requests: write
6 | contents: write
7 |
8 | jobs:
9 | dependabot:
10 | name: Enable auto-merge
11 | runs-on: ubuntu-latest
12 | if: ${{ github.actor == 'dependabot[bot]' }}
13 | steps:
14 | - name: Dependabot metadata
15 | id: metadata
16 | uses: dependabot/fetch-metadata@v2.4.0
17 | with:
18 | github-token: "${{ secrets.GITHUB_TOKEN }}"
19 | - name: Enable auto-merge for Dependabot PRs
20 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.update-type == 'version-update:semver-patch'}}
21 | run: gh pr merge --auto --merge "$PR_URL"
22 | env:
23 | PR_URL: ${{github.event.pull_request.html_url}}
24 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
25 |
--------------------------------------------------------------------------------
/src/Shipping.Service/Handlers/StoreReservationForVenueDeliveryHandler.cs:
--------------------------------------------------------------------------------
1 | using NServiceBus;
2 | using Shipping.Messages.Commands;
3 | using System.Drawing;
4 | using System.Threading.Tasks;
5 | using Console = Colorful.Console;
6 |
7 | namespace Shipping.Service.Handlers
8 | {
9 | class StoreReservationForVenueDeliveryHandler : IHandleMessages
10 | {
11 | public Task Handle(StoreReservationForVenueDelivery message, IMessageHandlerContext context)
12 | {
13 | /*
14 | * This empty handler has the only reason to avoid
15 | * that the StoreReservationForVenueDelivery fails
16 | * because no handlers can be found.
17 | */
18 | Console.WriteLine($"Venue delivery batch for {message.OrderId} accepted.", Color.Green);
19 |
20 | return Task.CompletedTask;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Finance.PaymentGateway/Handlers/ReleaseCardAuthorizationHandler.cs:
--------------------------------------------------------------------------------
1 | using Finance.PaymentGateway.Messages;
2 | using NServiceBus;
3 | using System.Drawing;
4 | using System.Threading.Tasks;
5 | using Console = Colorful.Console;
6 |
7 | namespace Finance.PaymentGateway.Handlers
8 | {
9 | class ReleaseCardAuthorizationHandler : IHandleMessages
10 | {
11 | public Task Handle(ReleaseCardAuthorization message, IMessageHandlerContext context)
12 | {
13 | /*
14 | * contact the credit card provider and release
15 | * the authorized transaction identified by the
16 | * incoming message AuthorizationId.
17 | */
18 |
19 | Console.WriteLine($"Releasing card authorization '{message.AuthorizationId}' for reservation '{message.ReservationId}'", Color.Green);
20 | return Task.CompletedTask;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Finance.ViewModelComposition/Finance.ViewModelComposition.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/Finance.PaymentGateway/Program.cs:
--------------------------------------------------------------------------------
1 | using NServiceBus;
2 | using System;
3 | using System.Threading.Tasks;
4 |
5 | namespace Finance.PaymentGateway
6 | {
7 | class Program
8 | {
9 | static async Task Main(string[] args)
10 | {
11 | var serviceName = typeof(Program).Namespace;
12 | Console.Title = serviceName;
13 |
14 | const string connectionString = @"Host=localhost;Port=7432;Username=db_user;Password=P@ssw0rd;Database=finance_service_database";
15 | var config = new EndpointConfiguration(serviceName);
16 | config.ApplyCommonConfigurationWithPersistence(connectionString, tablePrefix:"FinPayGate");
17 |
18 | var endpointInstance = await Endpoint.Start(config);
19 |
20 | Console.WriteLine($"{serviceName} started. Press any key to stop.");
21 | Console.ReadLine();
22 |
23 | await endpointInstance.Stop();
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Reservations.ViewModelComposition/Reservations.ViewModelComposition.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/Finance.Service/Handlers/StoreReservedTicketHandler.cs:
--------------------------------------------------------------------------------
1 | using Finance.Data;
2 | using Finance.Data.Models;
3 | using Finance.Messages.Commands;
4 | using NServiceBus;
5 | using System.Drawing;
6 | using System.Threading.Tasks;
7 | using Console = Colorful.Console;
8 |
9 | namespace Finance.Service.Handlers
10 | {
11 | class StoreReservedTicketHandler : IHandleMessages
12 | {
13 | public async Task Handle(StoreReservedTicket message, IMessageHandlerContext context)
14 | {
15 | Console.WriteLine($"Adding ticket '{message.TicketId}' to reservation '{message.ReservationId}'.", Color.Green);
16 |
17 | await using var db = new FinanceContext();
18 | db.ReservedTickets.Add(new ReservedTicket() { ReservationId = message.ReservationId, TicketId = message.TicketId });
19 | await db.SaveChangesAsync(context.CancellationToken);
20 |
21 |
22 | Console.WriteLine($"Ticket added.", Color.Green);
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/src/Finance.ViewModelComposition/ReservationsCheckedoutGetHandler.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using ServiceComposer.AspNetCore;
3 | using System;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Mvc;
6 |
7 | namespace Finance.ViewModelComposition
8 | {
9 | class ReservationsCheckedoutGetHandler : ICompositionRequestsHandler
10 | {
11 | [HttpGet("/reservations/checkedout")]
12 | public Task Handle(HttpRequest request)
13 | {
14 | /*
15 | * delete the reservation payment method cookie
16 | */
17 | var response = request.HttpContext.Response;
18 | response.Cookies.Append(
19 | key: "reservation-payment-method-id",
20 | value: "",
21 | options: new CookieOptions()
22 | {
23 | Expires = DateTimeOffset.Now.AddHours(-1)
24 | });
25 |
26 | return Task.CompletedTask;
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Shipping.ViewModelComposition/ReservationsCheckedoutGetHandler.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using Microsoft.AspNetCore.Routing;
3 | using ServiceComposer.AspNetCore;
4 | using System;
5 | using System.Threading.Tasks;
6 | using Microsoft.AspNetCore.Mvc;
7 |
8 | namespace Shipping.ViewModelComposition
9 | {
10 | class ReservationsCheckedoutGetHandler : ICompositionRequestsHandler
11 | {
12 | [HttpGet("/reservations/checkedout")]
13 | public Task Handle(HttpRequest request)
14 | {
15 | /*
16 | * delete the reservation delivery option cookie
17 | */
18 | var response = request.HttpContext.Response;
19 | response.Cookies.Append(
20 | key: "reservation-delivery-option-id",
21 | value: "",
22 | options: new CookieOptions()
23 | {
24 | Expires = DateTimeOffset.Now.AddHours(-1)
25 | });
26 |
27 | return Task.CompletedTask;
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Reservations.ViewModelComposition/Middlewares/ReservationMiddleware.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using System;
3 | using System.Threading.Tasks;
4 |
5 | namespace Reservations.ViewModelComposition.Middlewares
6 | {
7 | public class ReservationMiddleware
8 | {
9 | private readonly RequestDelegate _next;
10 |
11 | public ReservationMiddleware(RequestDelegate next)
12 | {
13 | _next = next;
14 | }
15 |
16 | public async Task InvokeAsync(HttpContext context)
17 | {
18 | if (!context.Request.Cookies.ContainsKey("reservation-id"))
19 | {
20 | context.Response.Cookies.Append(
21 | key: "reservation-id",
22 | value: Guid.NewGuid().ToString(),
23 | options: new CookieOptions()
24 | {
25 | Expires = DateTimeOffset.Now.AddHours(1)
26 | });
27 | }
28 |
29 | await _next(context);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Reservations.ViewModelComposition/ReservationsCheckedoutGetHandler.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using ServiceComposer.AspNetCore;
3 | using System;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Mvc;
6 |
7 | namespace Reservations.ViewModelComposition
8 | {
9 | class ReservationsCheckedoutGetHandler : ICompositionRequestsHandler
10 | {
11 | [HttpGet("/reservations/checkedout")]
12 | public Task Handle(HttpRequest request)
13 | {
14 | /*
15 | * delete the reservation cookie so to let
16 | * infrastructure create a new reservation
17 | */
18 | var response = request.HttpContext.Response;
19 | response.Cookies.Append(
20 | key: "reservation-id",
21 | value: "",
22 | options: new CookieOptions()
23 | {
24 | Expires = DateTimeOffset.Now.AddHours(-1)
25 | });
26 |
27 | return Task.CompletedTask;
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Finance.PaymentGateway/Handlers/ChargeCardHandler.cs:
--------------------------------------------------------------------------------
1 | using Finance.PaymentGateway.Messages;
2 | using NServiceBus;
3 | using System.Drawing;
4 | using System.Threading.Tasks;
5 | using Console = Colorful.Console;
6 |
7 | namespace Finance.PaymentGateway.Handlers
8 | {
9 | class ChargeCardHandler : IHandleMessages
10 | {
11 | public async Task Handle(ChargeCard message, IMessageHandlerContext context)
12 | {
13 | Console.WriteLine($"Going to charge card for authorization '{message.AuthorizationId}' for reservation '{message.ReservationId}'", Color.Green);
14 | Console.WriteLine("Waiting 5\" before replying...", Color.Yellow);
15 |
16 | await Task.Delay(5000, context.CancellationToken);
17 |
18 | await context.Reply(new CardChargedResponse()
19 | {
20 | ReservationId = message.ReservationId
21 | });
22 |
23 | Console.WriteLine($"Payment for reservation '{message.ReservationId}' succeeded.", Color.Green);
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "Build solution",
6 | "command": "dotnet build ${workspaceFolder}/src/welcome-to-the-state-machine-demos.sln",
7 | "type": "shell",
8 | "group": "build",
9 | "presentation": {
10 | "reveal": "silent"
11 | },
12 | "problemMatcher": "$msCompile"
13 | },
14 | {
15 | "label": "Create databases",
16 | "command": "dotnet run --project ${workspaceFolder}/src/CreateRequiredDatabases/CreateRequiredDatabases.csproj",
17 | "type": "shell",
18 | "group": "none",
19 | "presentation": {
20 | "reveal": "silent"
21 | },
22 | "problemMatcher": "$msCompile"
23 | },
24 | {
25 | "label": "Build & create databases",
26 | "group": "build",
27 | "dependsOn": ["Build solution", "Create databases"],
28 | "dependsOrder": "sequence"
29 | }
30 | ]
31 | }
--------------------------------------------------------------------------------
/src/CreateRequiredDatabases/Program.cs:
--------------------------------------------------------------------------------
1 | using Finance.Data;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.EntityFrameworkCore.Infrastructure;
4 | using Microsoft.EntityFrameworkCore.Storage;
5 | using Reservations.Data;
6 | using Ticketing.Data;
7 |
8 | /*
9 | * Entity Framework .Database.EnsureCreated() works only if the database is empty
10 | * in this demo NServiceBus persistence might populate the database _before_ EF
11 | * checks for te first time. If that happens EF never creates the db. Thus, we create
12 | * it while building the solution. Another option (better, but far more complex
13 | * for this demo) would have been to use migrations.
14 | */
15 |
16 | void CreateRequiredTables(DbContext context)
17 | {
18 | var databaseCreator = (RelationalDatabaseCreator)context.Database.GetService();
19 |
20 | if (databaseCreator.HasTables())
21 | {
22 | return;
23 | }
24 |
25 | databaseCreator.CreateTables();
26 | }
27 |
28 | CreateRequiredTables(new TicketingContext());
29 | CreateRequiredTables(new FinanceContext());
30 | CreateRequiredTables(new ReservationsContext());
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Mauro Servienti
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/Website/Startup.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.Extensions.DependencyInjection;
3 | using Reservations.ViewModelComposition.Middlewares;
4 | using ServiceComposer.AspNetCore;
5 |
6 | namespace Website
7 | {
8 | public class Startup
9 | {
10 | public void ConfigureServices(IServiceCollection services)
11 | {
12 | services.AddRouting();
13 | services.AddControllersWithViews();
14 |
15 | services.AddViewModelComposition(options =>
16 | {
17 | options.EnableCompositionOverControllers(true);
18 | });
19 | }
20 |
21 | public void Configure(IApplicationBuilder app)
22 | {
23 | app.UseDeveloperExceptionPage();
24 |
25 | app.UseHttpsRedirection();
26 | app.UseStaticFiles();
27 | app.UseMiddleware();
28 | app.UseRouting();
29 | app.UseEndpoints(endpoints =>
30 | {
31 | endpoints.MapControllers();
32 | endpoints.MapCompositionHandlers();
33 | });
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Reservations.Service/Reservations.Service.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/Website/Views/Shared/_ValidationScriptsPartial.cshtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/src/Ticketing.ViewModelComposition/ReservedTicketsLoadedSubscriber.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Reservations.ViewModelComposition.Events;
3 | using ServiceComposer.AspNetCore;
4 | using System.Linq;
5 | using Microsoft.AspNetCore.Mvc;
6 |
7 | namespace Ticketing.ViewModelComposition
8 | {
9 | class ReservedTicketsLoadedSubscriber : ICompositionEventsSubscriber
10 | {
11 | [HttpGet("/reservations")]
12 | [HttpGet("/reservations/review")]
13 | public void Subscribe(ICompositionEventsPublisher publisher)
14 | {
15 | publisher.Subscribe(async (@event, request) =>
16 | {
17 | var ids = @event.ReservedTicketsViewModel.Keys.ToArray();
18 | await using var db = new Data.TicketingContext();
19 | var reservedTickets = await db.Tickets
20 | .Where(ticket => ids.Contains(ticket.Id))
21 | .ToListAsync();
22 |
23 | foreach (var ticket in reservedTickets)
24 | {
25 | @event.ReservedTicketsViewModel[(int)ticket.Id].TicketDescription = ticket.Description;
26 | }
27 | });
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Finance.ViewModelComposition/AvailableTicketsLoadedSubscriber.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using Microsoft.AspNetCore.Routing;
3 | using Microsoft.EntityFrameworkCore;
4 | using ServiceComposer.AspNetCore;
5 | using System.Linq;
6 | using Microsoft.AspNetCore.Mvc;
7 | using Ticketing.ViewModelComposition.Events;
8 |
9 | namespace Finance.ViewModelComposition
10 | {
11 | class AvailableTicketsLoadedSubscriber : ICompositionEventsSubscriber
12 | {
13 | [HttpGet("/")]
14 | public void Subscribe(ICompositionEventsPublisher publisher)
15 | {
16 | publisher.Subscribe(async (@event, request) =>
17 | {
18 | var ids = @event.AvailableTicketsViewModel.Keys.ToArray();
19 | await using var db = new Data.FinanceContext();
20 | var ticketPrices = await db.TicketPrices
21 | .Where(ticketPrice => ids.Contains(ticketPrice.Id))
22 | .ToListAsync();
23 |
24 | foreach (var ticketPrice in ticketPrices)
25 | {
26 | @event.AvailableTicketsViewModel[(int)ticketPrice.Id].TicketPrice = ticketPrice.Price;
27 | }
28 | });
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Shipping.Service/Shipping.Service.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0
6 | latest
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/Finance.PaymentGateway/Handlers/AuthorizeCardHandler.cs:
--------------------------------------------------------------------------------
1 | using Finance.PaymentGateway.Messages;
2 | using NServiceBus;
3 | using System;
4 | using System.Drawing;
5 | using System.Threading.Tasks;
6 | using Console = Colorful.Console;
7 |
8 | namespace Finance.PaymentGateway.Handlers
9 | {
10 | class AuthorizeCardHandler : IHandleMessages
11 | {
12 | public async Task Handle(AuthorizeCard message, IMessageHandlerContext context)
13 | {
14 | /*
15 | * Use PaymentMethodId to retrieve credit card
16 | * information from the Vault and process the
17 | * authorization request
18 | */
19 | Console.WriteLine($"Attempt to authorize card with Id '{message.PaymentMethodId}' for reservation '{message.ReservationId}'", Color.Green);
20 | Console.WriteLine("Waiting 5\" before replying...", Color.Yellow);
21 |
22 | await Task.Delay(5000, context.CancellationToken);
23 |
24 | await context.Reply(new CardAuthorizedResponse()
25 | {
26 | ReservationId = message.ReservationId,
27 | AuthorizationId = Guid.NewGuid()
28 | });
29 |
30 | Console.WriteLine($"Payment for reservation '{message.ReservationId}' authorized.", Color.Green);
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Website/Controllers/ReservationsController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 |
3 | namespace Website.Controllers
4 | {
5 | public class ReservationsController : Controller
6 | {
7 | [HttpGet("/reservations")]
8 | public IActionResult Index()
9 | {
10 | return View();
11 | }
12 |
13 | [HttpPost("/reservations/reserve/{id}")]
14 | public IActionResult Reserve(int id)
15 | {
16 | return RedirectToAction("Added");
17 | }
18 |
19 | [HttpGet("/reservations/added")]
20 | public IActionResult Added()
21 | {
22 | return View();
23 | }
24 |
25 | [HttpGet("/reservations/review")]
26 | public IActionResult Review()
27 | {
28 | return View();
29 | }
30 |
31 | [HttpPost("/reservations/finalize")]
32 | public IActionResult Finalize()
33 | {
34 | return RedirectToAction("Review");
35 | }
36 |
37 | [HttpPost("/reservations/checkout")]
38 | public IActionResult Checkout()
39 | {
40 | return RedirectToAction("CheckedOut");
41 | }
42 |
43 | [HttpGet("/reservations/checkedout")]
44 | public IActionResult CheckedOut()
45 | {
46 | return View();
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Website/wwwroot/css/site.css:
--------------------------------------------------------------------------------
1 | /* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
2 | for details on configuring this project to bundle and minify static web assets. */
3 |
4 | a.navbar-brand {
5 | white-space: normal;
6 | text-align: center;
7 | word-break: break-all;
8 | }
9 |
10 | /* Sticky footer styles
11 | -------------------------------------------------- */
12 | html {
13 | font-size: 14px;
14 | }
15 | @media (min-width: 768px) {
16 | html {
17 | font-size: 16px;
18 | }
19 | }
20 |
21 | .border-top {
22 | border-top: 1px solid #e5e5e5;
23 | }
24 | .border-bottom {
25 | border-bottom: 1px solid #e5e5e5;
26 | }
27 |
28 | .box-shadow {
29 | box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
30 | }
31 |
32 | button.accept-policy {
33 | font-size: 1rem;
34 | line-height: inherit;
35 | }
36 |
37 | /* Sticky footer styles
38 | -------------------------------------------------- */
39 | html {
40 | position: relative;
41 | min-height: 100%;
42 | }
43 |
44 | body {
45 | /* Margin bottom by footer height */
46 | margin-bottom: 60px;
47 | }
48 | .footer {
49 | position: absolute;
50 | bottom: 0;
51 | width: 100%;
52 | white-space: nowrap;
53 | /* Set the fixed height of the footer here */
54 | height: 60px;
55 | line-height: 60px; /* Vertically center the text there */
56 | }
57 |
--------------------------------------------------------------------------------
/src/Website/Views/Reservations/Review.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | ViewData["Title"] = "Your reservations : review";
3 | }
4 |
5 |
6 |
Review your reservation before checkout
7 |
8 |
9 | @if (Model.Reservation == null)
10 | {
11 | No reservations found.
12 | }
13 | else
14 | {
15 |
16 | @foreach (dynamic reservedTicket in Model.Reservation.ReservedTickets)
17 | {
18 |
19 | @reservedTicket.TicketDescription
20 | @if (reservedTicket.TicketPrice == 0)
21 | {
22 | @reservedTicket.Quantity, Free
23 | }
24 | else
25 | {
26 | @reservedTicket.Quantity x @reservedTicket.TicketPrice, @reservedTicket.TotalPrice
27 | }
28 |
29 | }
30 |
31 |
32 |
33 | Total due: @Model.Reservation.TotalPrice
34 |
35 |
36 |
37 | Selected Payment Method: @Model.PaymentMethod.Description
38 |
39 |
40 |
41 | Selected Delivery Option: @Model.DeliveryOption.Description
42 |
43 |
44 |
47 | }
--------------------------------------------------------------------------------
/src/Website/Views/Home/Index.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | ViewData["Title"] = "Home Page";
3 | }
4 |
5 |
6 |
Welcome to the (state) Machine
7 | Select available tickets to start the purchase process.
8 |
9 |
10 |
11 | @foreach (dynamic ticket in Model.AvailableTickets)
12 | {
13 |
14 |
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/src/Finance.Service/Finance.Service.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0
6 | latest
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/Shipping.Service/Program.cs:
--------------------------------------------------------------------------------
1 | using NServiceBus;
2 | using System;
3 | using Microsoft.Extensions.Hosting;
4 | using Microsoft.Extensions.Logging;
5 |
6 | namespace Shipping.Service
7 | {
8 | class Program
9 | {
10 | static void Main(string[] args)
11 | {
12 | var serviceName = typeof(Program).Namespace;
13 | Console.Title = serviceName;
14 |
15 | CreateHostBuilder(serviceName, args).Build().Run();
16 | }
17 |
18 | static IHostBuilder CreateHostBuilder(string serviceName, string[] args)
19 | {
20 | var builder = Host.CreateDefaultBuilder(args)
21 | .ConfigureLogging((ctx, logging) =>
22 | {
23 | logging.AddConfiguration(ctx.Configuration.GetSection("Logging"));
24 | logging.AddConsole();
25 | })
26 | .UseNServiceBus(ctx =>
27 | {
28 | const string connectionString = @"Host=localhost;Port=10432;Username=db_user;Password=P@ssw0rd;Database=shipping_service_database";
29 | var config = new EndpointConfiguration(serviceName);
30 | config.ApplyCommonConfigurationWithPersistence(connectionString, tablePrefix:"Shipping");
31 |
32 | return config;
33 | });
34 |
35 | return builder;
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/src/Ticketing.Data/TicketingContext.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Ticketing.Data.Models;
3 |
4 | namespace Ticketing.Data
5 | {
6 | public class TicketingContext : DbContext
7 | {
8 | public DbSet Tickets { get; set; }
9 |
10 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
11 | {
12 | optionsBuilder.UseNpgsql(@"Host=localhost;Port=5432;Username=db_user;Password=P@ssw0rd;Database=ticketing_database");
13 | }
14 |
15 | protected override void OnModelCreating(ModelBuilder modelBuilder)
16 | {
17 | modelBuilder.Entity().HasData(Seed.Tickets());
18 |
19 | base.OnModelCreating(modelBuilder);
20 | }
21 |
22 | private static class Seed
23 | {
24 | internal static Ticket[] Tickets()
25 | {
26 | return new[]
27 | {
28 | new Ticket()
29 | {
30 | Id = 1,
31 | Description = "Monsters of Rock, Modena Italy - 1991"
32 | },
33 | new Ticket()
34 | {
35 | Id = 2,
36 | Description = "Pink Floyd, Venice Italy - 1989",
37 | }
38 | };
39 | }
40 |
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Reservations.Service/Program.cs:
--------------------------------------------------------------------------------
1 | using NServiceBus;
2 | using System;
3 | using Microsoft.Extensions.Hosting;
4 | using Microsoft.Extensions.Logging;
5 |
6 | namespace Reservations.Service
7 | {
8 | class Program
9 | {
10 | static void Main(string[] args)
11 | {
12 | var serviceName = typeof(Program).Namespace;
13 | Console.Title = serviceName;
14 |
15 | CreateHostBuilder(serviceName, args).Build().Run();
16 | }
17 |
18 | static IHostBuilder CreateHostBuilder(string serviceName, string[] args)
19 | {
20 | var builder = Host.CreateDefaultBuilder(args)
21 | .ConfigureLogging((ctx, logging) =>
22 | {
23 | logging.AddConfiguration(ctx.Configuration.GetSection("Logging"));
24 | logging.AddConsole();
25 | })
26 | .UseNServiceBus(ctx =>
27 | {
28 | const string connectionString = @"Host=localhost;Port=9432;Username=db_user;Password=P@ssw0rd;Database=reservations_service_database";
29 | var config = new EndpointConfiguration(serviceName);
30 | config.ApplyCommonConfigurationWithPersistence(connectionString, tablePrefix: "reservations");
31 |
32 | return config;
33 | });
34 |
35 | return builder;
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/.devcontainer/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | demo:
5 | image: mcr.microsoft.com/devcontainers/dotnet:8.0
6 | volumes:
7 | - ../..:/workspaces:cached
8 | network_mode: service:rabbit
9 | command: sleep infinity
10 |
11 | rabbit:
12 | image: rabbitmq:3.10-management
13 | restart: unless-stopped
14 | hostname: rabbit
15 | environment:
16 | - RABBITMQ_DEFAULT_USER=guest
17 | - RABBITMQ_DEFAULT_PASS=guest
18 |
19 | ticketing-database:
20 | image: postgres
21 | env_file:
22 | - ticketing-database.env
23 | network_mode: service:rabbit
24 |
25 | finance-database:
26 | image: postgres
27 | env_file:
28 | - finance-database.env
29 | network_mode: service:rabbit
30 |
31 | finance-service-database:
32 | image: postgres
33 | env_file:
34 | - finance-service-database.env
35 | network_mode: service:rabbit
36 |
37 | reservations-database:
38 | image: postgres
39 | env_file:
40 | - reservations-database.env
41 | network_mode: service:rabbit
42 |
43 | reservations-service-database:
44 | image: postgres
45 | env_file:
46 | - reservations-service-database.env
47 | network_mode: service:rabbit
48 |
49 | shipping-service-database:
50 | image: postgres
51 | env_file:
52 | - shipping-service-database.env
53 | network_mode: service:rabbit
--------------------------------------------------------------------------------
/src/Shipping.ViewModelComposition/ReservedTicketsLoadedSubscriber.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using Microsoft.AspNetCore.Routing;
3 | using Reservations.ViewModelComposition.Events;
4 | using ServiceComposer.AspNetCore;
5 | using Shipping.Data;
6 | using System.Collections.Generic;
7 | using System.Dynamic;
8 | using System.Threading.Tasks;
9 | using Microsoft.AspNetCore.Mvc;
10 |
11 | namespace Shipping.ViewModelComposition
12 | {
13 | class ReservedTicketsLoadedSubscriber : ICompositionEventsSubscriber
14 | {
15 | [HttpGet("/reservations")]
16 | public void Subscribe(ICompositionEventsPublisher publisher)
17 | {
18 | publisher.Subscribe((@event, httpRequest) =>
19 | {
20 | dynamic shipAtHome = new ExpandoObject();
21 | shipAtHome.Id = DeliveryOptions.ShipAtHome;
22 | shipAtHome.Description = "Ship at Home.";
23 |
24 | dynamic collectAtTheVenue = new ExpandoObject();
25 | collectAtTheVenue.Id = DeliveryOptions.CollectAtTheVenue;
26 | collectAtTheVenue.Description = "Collect at the Venue.";
27 |
28 | var pageViewModel = httpRequest.GetComposedResponseModel();
29 | pageViewModel.DeliveryOptions = new List()
30 | {
31 | shipAtHome,
32 | collectAtTheVenue
33 | };
34 |
35 | return Task.CompletedTask;
36 | });
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Website/Website.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | InProcess
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | PreserveNewest
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/Reservations.Service/Handlers/MarkTicketAsReservedHandler.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using NServiceBus;
3 | using Reservations.Data;
4 | using Reservations.Data.Models;
5 | using Reservations.Service.Messages;
6 | using System.Linq;
7 | using System.Drawing;
8 | using System.Threading.Tasks;
9 | using Console = Colorful.Console;
10 |
11 | namespace Reservations.Service.Handlers
12 | {
13 | class MarkTicketAsReservedHandler : IHandleMessages
14 | {
15 | public async Task Handle(MarkTicketAsReserved message, IMessageHandlerContext context)
16 | {
17 | Console.WriteLine($"Going to mark ticket '{message.TicketId}' as reserved.", Color.Green);
18 |
19 | await using var db = new ReservationsContext();
20 | var reservation = await db.Reservations
21 | .Where(r=>r.Id==message.ReservationId)
22 | .Include(r=>r.ReservedTickets)
23 | .SingleOrDefaultAsync(context.CancellationToken);
24 |
25 | if (reservation == null)
26 | {
27 | reservation = new Reservation()
28 | {
29 | Id = message.ReservationId
30 | };
31 | db.Reservations.Add(reservation);
32 | }
33 |
34 | reservation.ReservedTickets.Add(new ReservedTicket()
35 | {
36 | ReservationId = message.ReservationId,
37 | TicketId = message.TicketId
38 | });
39 |
40 | await db.SaveChangesAsync(context.CancellationToken);
41 |
42 | Console.WriteLine($"Ticket '{message.TicketId}' reserved to reservation '{message.ReservationId}'.", Color.Green);
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Shipping.ViewModelComposition/ReviewReservedTicketsLoadedSubscriber.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using Microsoft.AspNetCore.Routing;
3 | using Reservations.ViewModelComposition.Events;
4 | using ServiceComposer.AspNetCore;
5 | using Shipping.Data;
6 | using System;
7 | using System.Dynamic;
8 | using System.Linq;
9 | using System.Threading.Tasks;
10 | using Microsoft.AspNetCore.Mvc;
11 |
12 | namespace Shipping.ViewModelComposition
13 | {
14 | class ReviewReservedTicketsLoadedSubscriber : ICompositionEventsSubscriber
15 | {
16 | [HttpGet("/reservations/review")]
17 | public void Subscribe(ICompositionEventsPublisher publisher)
18 | {
19 | publisher.Subscribe((@event, httpRequest) =>
20 | {
21 | /*
22 | * it's a demo, production code should check for cookie existence
23 | */
24 | var selectedDeliveryOption = (DeliveryOptions)Enum.Parse(typeof(Data.DeliveryOptions), httpRequest.Cookies["reservation-delivery-option-id"]);
25 | dynamic deliveryOption = new ExpandoObject();
26 | deliveryOption.Id = selectedDeliveryOption;
27 |
28 | deliveryOption.Description = selectedDeliveryOption switch
29 | {
30 | DeliveryOptions.ShipAtHome => "Ship at Home.",
31 | DeliveryOptions.CollectAtTheVenue => "Collect at the Venue.",
32 | _ => deliveryOption.Description
33 | };
34 |
35 | var viewModel = httpRequest.GetComposedResponseModel();
36 | viewModel.DeliveryOption = deliveryOption;
37 |
38 | return Task.CompletedTask;
39 | });
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Finance.Service/Program.cs:
--------------------------------------------------------------------------------
1 | using Finance.PaymentGateway.Messages;
2 | using NServiceBus;
3 | using System;
4 | using Microsoft.Extensions.Hosting;
5 | using Microsoft.Extensions.Logging;
6 |
7 | namespace Finance.Service
8 | {
9 | class Program
10 | {
11 | static void Main(string[] args)
12 | {
13 | var serviceName = typeof(Program).Namespace;
14 | Console.Title = serviceName;
15 |
16 | CreateHostBuilder(serviceName, args).Build().Run();
17 | }
18 |
19 | static IHostBuilder CreateHostBuilder(string serviceName, string[] args)
20 | {
21 | var builder = Host.CreateDefaultBuilder(args)
22 | .ConfigureLogging((ctx, logging) =>
23 | {
24 | logging.AddConfiguration(ctx.Configuration.GetSection("Logging"));
25 | logging.AddConsole();
26 | })
27 | .UseNServiceBus(ctx =>
28 | {
29 | const string connectionString = @"Host=localhost;Port=7432;Username=db_user;Password=P@ssw0rd;Database=finance_service_database";
30 | var config = new EndpointConfiguration(serviceName);
31 | config.ApplyCommonConfigurationWithPersistence(connectionString, tablePrefix:"Finance", configureRouting: routing =>
32 | {
33 | routing.RouteToEndpoint(typeof(AuthorizeCard), "Finance.PaymentGateway");
34 | routing.RouteToEndpoint(typeof(ReleaseCardAuthorization), "Finance.PaymentGateway");
35 | routing.RouteToEndpoint(typeof(ChargeCard), "Finance.PaymentGateway");
36 | });
37 |
38 | return config;
39 | });
40 |
41 | return builder;
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Ticketing.ViewModelComposition/AvailableTicketsGetHandler.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using Microsoft.EntityFrameworkCore;
3 | using ServiceComposer.AspNetCore;
4 | using System.Collections.Generic;
5 | using System.Dynamic;
6 | using System.Linq;
7 | using System.Threading.Tasks;
8 | using Ticketing.ViewModelComposition.Events;
9 | using Microsoft.AspNetCore.Mvc;
10 |
11 | namespace Ticketing.ViewModelComposition
12 | {
13 | class AvailableTicketsGetHandler : ICompositionRequestsHandler
14 | {
15 | [HttpGet("/")]
16 | public async Task Handle(HttpRequest request)
17 | {
18 | await using var db = new Data.TicketingContext();
19 | var allTickets = await db.Tickets.ToListAsync();
20 | var availableProductsViewModel = MapToDictionary(allTickets);
21 |
22 | var compositionContext = request.GetCompositionContext();
23 | await compositionContext.RaiseEvent(new AvailableTicketsLoaded()
24 | {
25 | AvailableTicketsViewModel = availableProductsViewModel
26 | });
27 |
28 | var vm = request.GetComposedResponseModel();
29 | vm.AvailableTickets = availableProductsViewModel.Values.ToList();
30 | }
31 |
32 | IDictionary MapToDictionary(IEnumerable allTickets)
33 | {
34 | var availableTicketsViewModel = new Dictionary();
35 |
36 | foreach (var ticket in allTickets)
37 | {
38 | dynamic vm = new ExpandoObject();
39 | vm.TicketId = ticket.Id;
40 | vm.TicketDescription = ticket.Description;
41 |
42 | availableTicketsViewModel[ticket.Id] = vm;
43 | }
44 |
45 | return availableTicketsViewModel;
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Website/Views/Reservations/Index.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | ViewData["Title"] = "Your reservations";
3 | }
4 |
5 |
6 |
My Reservations
7 |
8 |
9 | @if (Model.Reservation == null)
10 | {
11 | No reservations found.
12 | }
13 | else
14 | {
15 |
16 | @foreach (dynamic reservedTicket in Model.Reservation.ReservedTickets)
17 | {
18 |
19 | @reservedTicket.TicketDescription
20 | @if (reservedTicket.TicketPrice == 0)
21 | {
22 | @reservedTicket.Quantity, Free
23 | }
24 | else
25 | {
26 | @reservedTicket.Quantity x @reservedTicket.TicketPrice, @reservedTicket.TotalPrice
27 | }
28 |
29 | }
30 |
31 |
32 |
33 | Total due: @Model.Reservation.TotalPrice
34 |
35 |
36 |
64 | }
--------------------------------------------------------------------------------
/src/Reservations.ViewModelComposition/ReservationsCheckoutPostHandler.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using NServiceBus;
3 | using Reservations.Messages.Commands;
4 | using ServiceComposer.AspNetCore;
5 | using System;
6 | using System.Threading.Tasks;
7 | using Microsoft.AspNetCore.Mvc;
8 |
9 | namespace Reservations.ViewModelComposition
10 | {
11 | class ReservationsCheckoutPostHandler : ICompositionRequestsHandler
12 | {
13 | private readonly IMessageSession messageSession;
14 |
15 | public ReservationsCheckoutPostHandler(IMessageSession messageSession)
16 | {
17 | this.messageSession = messageSession;
18 | }
19 |
20 | [HttpPost("/reservations/checkout")]
21 | public Task Handle(HttpRequest request)
22 | {
23 | /*
24 | * In a production environment if multiple services are interested in the
25 | * same post request the handling logic is much more complex than what we
26 | * are doing in this demo. In this demo both Finance and Reservations need
27 | * to handle the POST to /reservations/checkout. The implementation assumes
28 | * that the host/infrastructure never fails, which is not the case in a
29 | * production environment. In order to make this part safe, which is not the
30 | * scope of this demo asynchronous messaging should be introduced earlier in
31 | * the processing pipeline.
32 | *
33 | * More information: https://milestone.topics.it/2019/05/02/safety-first.html
34 | */
35 |
36 | var message = new CheckoutReservation()
37 | {
38 | ReservationId = new Guid(request.Cookies["reservation-id"])
39 | };
40 |
41 | /*
42 | * WARN: destination is hardcoded to reduce demo complexity.
43 | * In a production environment routing should be configured
44 | * at startup by the host/infrastructure.
45 | */
46 | return messageSession.Send("Reservations.Service", message);
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Reservations.Service/Handlers/CheckoutReservationHandler.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using NServiceBus;
3 | using Reservations.Data;
4 | using Reservations.Messages.Commands;
5 | using Reservations.Service.Messages;
6 | using System.Linq;
7 | using System.Drawing;
8 | using System.Threading.Tasks;
9 | using Console = Colorful.Console;
10 |
11 | namespace Reservations.Service.Handlers
12 | {
13 | class CheckoutReservationHandler : IHandleMessages
14 | {
15 | public async Task Handle(CheckoutReservation message, IMessageHandlerContext context)
16 | {
17 | Console.WriteLine($"Going to check-out reservation '{message.ReservationId}'.", Color.Green);
18 |
19 | await using var db = new ReservationsContext();
20 | var reservation = await db.Reservations
21 | .Where(r => r.Id == message.ReservationId)
22 | .Include(r => r.ReservedTickets)
23 | .SingleOrDefaultAsync(context.CancellationToken);
24 |
25 | /*
26 | * In case reservations expires the demo ignores that.
27 | * A description of reservations expiration can be found
28 | * in the ReservationsPolicy class.
29 | * A reservation could be checked out after the Reservation
30 | * expiration timeout is expired. In such scenario there won't
31 | * be any reservation to checkout and the incoming message is
32 | * simply "lost", or leads to a failure.
33 | */
34 | await context.Publish(new ReservationCheckedout()
35 | {
36 | ReservationId = message.ReservationId,
37 | Tickets = reservation.ReservedTickets
38 | .Select(rt => rt.TicketId)
39 | .ToArray()
40 | });
41 |
42 | db.Reservations.Remove(reservation);
43 | await db.SaveChangesAsync(context.CancellationToken);
44 |
45 | Console.WriteLine($"ReservationCheckedout event published and reservation removed from db.", Color.Green);
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Finance.ViewModelComposition/ReservationsCheckoutPostHandler.cs:
--------------------------------------------------------------------------------
1 | using Finance.Messages.Commands;
2 | using Microsoft.AspNetCore.Http;
3 | using NServiceBus;
4 | using ServiceComposer.AspNetCore;
5 | using System;
6 | using System.Threading.Tasks;
7 | using Microsoft.AspNetCore.Mvc;
8 |
9 | namespace Finance.ViewModelComposition
10 | {
11 | class ReservationsCheckoutPostHandler : ICompositionRequestsHandler
12 | {
13 | private readonly IMessageSession messageSession;
14 |
15 | public ReservationsCheckoutPostHandler(IMessageSession messageSession)
16 | {
17 | this.messageSession = messageSession;
18 | }
19 |
20 | [HttpPost("/reservations/checkout")]
21 | public Task Handle(HttpRequest request)
22 | {
23 | /*
24 | * In a production environment if multiple services are interested in the
25 | * same post request the handling logic is much more complex than what we
26 | * are doing in this demo. In this demo both Finance and Reservations need
27 | * to handle the POST to /reservations/checkout. The implementation assumes
28 | * that the host/infrastructure never fails, which is not the case in a
29 | * production environment. In order to make this part safe, which is not the
30 | * scope of this demo asynchronous messaging should be introduced earlier in
31 | * the processing pipeline.
32 | *
33 | * More information: https://milestone.topics.it/2019/05/02/safety-first.html
34 | */
35 |
36 | var message = new InitializeReservationPaymentPolicy()
37 | {
38 | ReservationId = new Guid(request.Cookies["reservation-id"]),
39 | PaymentMethodId = int.Parse(request.Cookies["reservation-payment-method-id"])
40 | };
41 |
42 | /*
43 | * WARN: destination is hard-coded to reduce demo complexity.
44 | * In a production environment routing should be configured
45 | * at startup by the host/infrastructure.
46 | */
47 | return messageSession.Send("Finance.Service", message);
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Finance.Data/FinanceContext.cs:
--------------------------------------------------------------------------------
1 | using Finance.Data.Models;
2 | using Microsoft.EntityFrameworkCore;
3 |
4 | namespace Finance.Data
5 | {
6 | public class FinanceContext : DbContext
7 | {
8 | public DbSet TicketPrices { get; set; }
9 | public DbSet ReservedTickets { get; set; }
10 | public DbSet PaymentMethods { get; set; }
11 |
12 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
13 | {
14 | optionsBuilder.UseNpgsql(@"Host=localhost;Port=6432;Username=db_user;Password=P@ssw0rd;Database=finance_database");
15 | }
16 |
17 | protected override void OnModelCreating(ModelBuilder modelBuilder)
18 | {
19 | modelBuilder.Entity().HasData(Seed.TicketPrices());
20 | modelBuilder.Entity().HasData(Seed.PaymentMethods());
21 | modelBuilder.Entity();
22 |
23 | base.OnModelCreating(modelBuilder);
24 | }
25 |
26 | private static class Seed
27 | {
28 | internal static PaymentMethod[] PaymentMethods()
29 | {
30 | return new[]
31 | {
32 | new PaymentMethod()
33 | {
34 | Id = 1,
35 | Description = "Master Card (last 4 digits: 5555)"
36 | },
37 | new PaymentMethod()
38 | {
39 | Id = 2,
40 | Description = "Visa (last 4 digits: 1111)"
41 | }
42 | };
43 | }
44 |
45 | internal static TicketPrice[] TicketPrices()
46 | {
47 | return new[]
48 | {
49 | new TicketPrice()
50 | {
51 | Id = 1,
52 | Price = 96
53 | },
54 | new TicketPrice()
55 | {
56 | Id = 2,
57 | Price = 0
58 | }
59 | };
60 | }
61 |
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Finance.ViewModelComposition/ReservationsReservePostHandler.cs:
--------------------------------------------------------------------------------
1 | using Finance.Messages.Commands;
2 | using Microsoft.AspNetCore.Http;
3 | using Microsoft.AspNetCore.Routing;
4 | using NServiceBus;
5 | using ServiceComposer.AspNetCore;
6 | using System;
7 | using System.Threading.Tasks;
8 | using Microsoft.AspNetCore.Mvc;
9 |
10 | namespace Finance.ViewModelComposition
11 | {
12 | class ReservationsReservePostHandler : ICompositionRequestsHandler
13 | {
14 | private readonly IMessageSession messageSession;
15 |
16 | public ReservationsReservePostHandler(IMessageSession messageSession)
17 | {
18 | this.messageSession = messageSession;
19 | }
20 |
21 | [HttpPost("/reservations/reserve/{id}")]
22 | public Task Handle(HttpRequest request)
23 | {
24 | /*
25 | * In a production environment if multiple services are interested in the
26 | * same post request the handling logic is much more complex than what we
27 | * are doing in this demo. In this demo both Finance and Reservations need
28 | * to handle the POST to /reservations/reserve. The implementation assumes
29 | * that the host/infrastructure never fails, which is not the case in a
30 | * production environment. In order to make this part safe, which is not the
31 | * scope of this demo asynchronous messaging should be introduced earlier in
32 | * the processing pipeline.
33 | *
34 | * More information: https://milestone.topics.it/2019/05/02/safety-first.html
35 | */
36 |
37 | var message = new StoreReservedTicket()
38 | {
39 | TicketId = int.Parse((string)request.HttpContext.GetRouteValue("id")),
40 | ReservationId = new Guid(request.Cookies["reservation-id"])
41 | };
42 |
43 | /*
44 | * WARN: destination is hard-coded to reduce demo complexity.
45 | * In a production environment routing should be configured
46 | * at startup by the host/infrastructure.
47 | */
48 | return messageSession.Send("Finance.Service", message);
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Reservations.ViewModelComposition/ReservationsReservePostHandler.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using Microsoft.AspNetCore.Routing;
3 | using NServiceBus;
4 | using Reservations.Messages.Commands;
5 | using ServiceComposer.AspNetCore;
6 | using System;
7 | using System.Threading.Tasks;
8 | using Microsoft.AspNetCore.Mvc;
9 |
10 | namespace Reservations.ViewModelComposition
11 | {
12 | class ReservationsReservePostHandler : ICompositionRequestsHandler
13 | {
14 | private readonly IMessageSession messageSession;
15 |
16 | public ReservationsReservePostHandler(IMessageSession messageSession)
17 | {
18 | this.messageSession = messageSession;
19 | }
20 |
21 | [HttpPost("/reservations/reserve/{id}")]
22 | public Task Handle(HttpRequest request)
23 | {
24 | /*
25 | * In a production environment if multiple services are interested in the
26 | * same post request the handling logic is much more complex than what we
27 | * are doing in this demo. In this demo both Finance and Reservations need
28 | * to handle the POST to /reservations/reserve. The implementation assumes
29 | * that the host/infrastructure never fails, which is not the case in a
30 | * production environment. In order to make this part safe, which is not the
31 | * scope of this demo asynchronous messaging should be introduced earlier in
32 | * the processing pipeline.
33 | *
34 | * More information: https://milestone.topics.it/2019/05/02/safety-first.html
35 | */
36 | var message = new ReserveTicket()
37 | {
38 | TicketId = int.Parse((string)request.HttpContext.GetRouteValue("id")),
39 | ReservationId = new Guid(request.Cookies["reservation-id"])
40 | };
41 |
42 | /*
43 | * WARN: destination is hard-coded to reduce demo complexity.
44 | * In a production environment routing should be configured
45 | * at startup by the host/infrastructure.
46 | */
47 | return messageSession.Send("Reservations.Service", message);
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Shipping.ViewModelComposition/ReservationsCheckoutPostHandler.cs:
--------------------------------------------------------------------------------
1 | using Shipping.Messages.Commands;
2 | using Microsoft.AspNetCore.Http;
3 | using NServiceBus;
4 | using ServiceComposer.AspNetCore;
5 | using System;
6 | using System.Threading.Tasks;
7 | using Microsoft.AspNetCore.Mvc;
8 | using Shipping.Data;
9 |
10 | namespace Shipping.ViewModelComposition
11 | {
12 | class ReservationsCheckoutPostHandler : ICompositionRequestsHandler
13 | {
14 | private readonly IMessageSession messageSession;
15 |
16 | public ReservationsCheckoutPostHandler(IMessageSession messageSession)
17 | {
18 | this.messageSession = messageSession;
19 | }
20 |
21 | [HttpPost("/reservations/checkout")]
22 | public Task Handle(HttpRequest request)
23 | {
24 | /*
25 | * In a production environment if multiple services are interested in the
26 | * same post request the handling logic is much more complex than what we
27 | * are doing in this demo. In this demo both Finance and Reservations need
28 | * to handle the POST to /reservations/checkout. The implementation assumes
29 | * that the host/infrastructure never fails, which is not the case in a
30 | * production environment. In order to make this part safe, which is not the
31 | * scope of this demo asynchronous messaging should be introduced earlier in
32 | * the processing pipeline.
33 | *
34 | * More information: https://milestone.topics.it/2019/05/02/safety-first.html
35 | */
36 |
37 | var message = new InitializeReservationShippingPolicy()
38 | {
39 | ReservationId = new Guid(request.Cookies["reservation-id"]),
40 | DeliveryOption = (DeliveryOptions)Enum.Parse(typeof(DeliveryOptions), request.Cookies["reservation-delivery-option-id"])
41 | };
42 |
43 | /*
44 | * WARN: destination is hard-coded to reduce demo complexity.
45 | * In a production environment routing should be configured
46 | * at startup by the host/infrastructure.
47 | */
48 | return messageSession.Send("Shipping.Service", message);
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Reservations.Data/ReservationsContext.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Reservations.Data.Models;
3 |
4 | namespace Reservations.Data
5 | {
6 | public class ReservationsContext : DbContext
7 | {
8 | public DbSet Reservations { get; set; }
9 |
10 | public DbSet AvailableTickets { get; set; }
11 |
12 | public DbSet Orders { get; set; }
13 |
14 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
15 | {
16 | optionsBuilder.UseNpgsql(@"Host=localhost;Port=8432;Username=db_user;Password=P@ssw0rd;Database=reservations_database");
17 | }
18 |
19 | protected override void OnModelCreating(ModelBuilder modelBuilder)
20 | {
21 | modelBuilder.Entity().HasData(Seed.AvailableTickets());
22 |
23 | var reservedTicketEntity = modelBuilder.Entity();
24 | var reservationEntity = modelBuilder.Entity();
25 |
26 | reservedTicketEntity
27 | .HasOne()
28 | .WithMany(r => r.ReservedTickets)
29 | .IsRequired()
30 | .HasForeignKey(so => so.ReservationId)
31 | .OnDelete(DeleteBehavior.Cascade);
32 |
33 | var orderedTicketEntity = modelBuilder.Entity();
34 | var orderEntity = modelBuilder.Entity();
35 |
36 | orderedTicketEntity
37 | .HasOne()
38 | .WithMany(r => r.OrderedTickets)
39 | .IsRequired()
40 | .HasForeignKey(so => so.OrderId)
41 | .OnDelete(DeleteBehavior.Cascade);
42 |
43 | base.OnModelCreating(modelBuilder);
44 | }
45 |
46 | private static class Seed
47 | {
48 | internal static AvailableTickets[] AvailableTickets()
49 | {
50 | return new[]
51 | {
52 | new AvailableTickets()
53 | {
54 | Id = 1,
55 | TotalTickets= 100
56 | },
57 | new AvailableTickets()
58 | {
59 | Id = 2,
60 | TotalTickets= 200
61 | }
62 | };
63 | }
64 |
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/Reservations.ViewModelComposition/AvailableTicketsLoadedSubscriber.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using Microsoft.AspNetCore.Routing;
3 | using Microsoft.EntityFrameworkCore;
4 | using ServiceComposer.AspNetCore;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Linq;
8 | using Microsoft.AspNetCore.Mvc;
9 | using Ticketing.ViewModelComposition.Events;
10 |
11 | namespace Reservations.ViewModelComposition
12 | {
13 | class AvailableTicketsLoadedSubscriber : ICompositionEventsSubscriber
14 | {
15 | [HttpGet("/")]
16 | public void Subscribe(ICompositionEventsPublisher publisher)
17 | {
18 | publisher.Subscribe(async (@event, request) =>
19 | {
20 | var ids = @event.AvailableTicketsViewModel.Keys.ToArray();
21 | await using var db = new Data.ReservationsContext();
22 | var availableTickets = await db.AvailableTickets
23 | .Where(ticket => ids.Contains(ticket.Id))
24 | .ToListAsync();
25 |
26 | IDictionary reservedTickets = new Dictionary();
27 |
28 | if (request.Cookies.ContainsKey("reservation-id"))
29 | {
30 | var reservationId = new Guid(request.Cookies["reservation-id"]);
31 | var reservation = await db.Reservations
32 | .Where(r => r.Id == reservationId)
33 | .Include(r => r.ReservedTickets)
34 | .SingleOrDefaultAsync();
35 |
36 | if (reservation != null)
37 | {
38 | reservedTickets = reservation.ReservedTickets
39 | .GroupBy(t => t.TicketId)
40 | .ToDictionary(g => g.Key, g => g.Count());
41 | }
42 | }
43 |
44 | foreach (var availableTicket in availableTickets)
45 | {
46 | var availableTicketId = (int)availableTicket.Id;
47 | var reservedQuantity = reservedTickets.ContainsKey(availableTicketId)
48 | ? reservedTickets[availableTicketId]
49 | : 0;
50 |
51 | @event.AvailableTicketsViewModel[availableTicketId].TicketsLeft = availableTicket.TotalTickets - reservedQuantity;
52 | }
53 | });
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Finance.ViewModelComposition/ReservedTicketsLoadedSubscriber.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Reservations.ViewModelComposition.Events;
3 | using ServiceComposer.AspNetCore;
4 | using System;
5 | using System.Linq;
6 | using Microsoft.AspNetCore.Mvc;
7 |
8 | namespace Finance.ViewModelComposition
9 | {
10 | class ReservedTicketsLoadedSubscriber : ICompositionEventsSubscriber
11 | {
12 | [HttpGet("/reservations")]
13 | public void Subscribe(ICompositionEventsPublisher publisher)
14 | {
15 | publisher.Subscribe(async (@event, request) =>
16 | {
17 | var ids = @event.ReservedTicketsViewModel.Keys.ToArray();
18 | await using var db = new Data.FinanceContext();
19 | var ticketPrices = await db.TicketPrices
20 | .Where(ticketPrice => ids.Contains(ticketPrice.Id))
21 | .ToListAsync();
22 |
23 | Guid reservationId = @event.Reservation.Id;
24 | var reservedTickets =
25 | (
26 | await db.ReservedTickets
27 | .Where(r => r.ReservationId == reservationId)
28 | .ToListAsync()
29 | )
30 | .GroupBy(t => t.TicketId)
31 | .ToDictionary(g=>g.Key, g=>g.Count());
32 |
33 | var reservationTotalPrice = 0m;
34 |
35 | foreach (var ticketPrice in ticketPrices)
36 | {
37 | var ticketId = (int)ticketPrice.Id;
38 | var reservedTicketViewModel = @event.ReservedTicketsViewModel[ticketId];
39 |
40 | var currentReservedTicketQuantity = reservedTickets[ticketId];
41 | var currentReservedTicketTotalPrice = ticketPrice.Price * currentReservedTicketQuantity;
42 |
43 | reservationTotalPrice += currentReservedTicketTotalPrice;
44 |
45 | reservedTicketViewModel.TicketPrice = ticketPrice.Price;
46 | reservedTicketViewModel.TotalPrice = currentReservedTicketTotalPrice;
47 | }
48 |
49 | @event.Reservation.TotalPrice = reservationTotalPrice;
50 |
51 | var allPaymentMethods = await db.PaymentMethods.ToListAsync();
52 | var pageViewModel = request.GetComposedResponseModel();
53 | pageViewModel.PaymentMethods = allPaymentMethods;
54 | });
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/NServiceBus.Shared/CommonEndpointSettings.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Npgsql;
3 | using NpgsqlTypes;
4 |
5 | namespace NServiceBus
6 | {
7 | public static class CommonEndpointSettings
8 | {
9 | public static void ApplyCommonConfiguration(this EndpointConfiguration endpointConfiguration, Action> configureRouting = null)
10 | {
11 | endpointConfiguration.EnableInstallers();
12 |
13 | endpointConfiguration.UseSerialization();
14 |
15 | var routeSettings = endpointConfiguration.UseTransport(new RabbitMQTransport(
16 | RoutingTopology.Conventional(QueueType.Classic),
17 | "host=localhost"
18 | )
19 | );
20 | configureRouting?.Invoke(routeSettings);
21 |
22 | endpointConfiguration.AuditProcessedMessagesTo("audit");
23 | endpointConfiguration.SendFailedMessagesTo("error");
24 |
25 | var messageConventions = endpointConfiguration.Conventions();
26 | messageConventions.DefiningMessagesAs(t => t.Namespace != null && t.Namespace.EndsWith(".Messages"));
27 | messageConventions.DefiningEventsAs(t => t.Namespace != null && t.Namespace.EndsWith(".Messages.Events"));
28 | messageConventions.DefiningCommandsAs(t => t.Namespace != null && t.Namespace.EndsWith(".Messages.Commands"));
29 | }
30 |
31 | public static void ApplyCommonConfigurationWithPersistence(this EndpointConfiguration endpointConfiguration, string sqlPersistenceConnectionString, string tablePrefix = null, Action> configureRouting = null)
32 | {
33 | ApplyCommonConfiguration(endpointConfiguration, configureRouting);
34 |
35 | var persistence = endpointConfiguration.UsePersistence();
36 | var dialect = persistence.SqlDialect();
37 | if (!string.IsNullOrWhiteSpace(tablePrefix))
38 | {
39 | persistence.TablePrefix(tablePrefix);
40 | }
41 |
42 | dialect.JsonBParameterModifier(
43 | modifier: parameter =>
44 | {
45 | var npgsqlParameter = (NpgsqlParameter)parameter;
46 | npgsqlParameter.NpgsqlDbType = NpgsqlDbType.Jsonb;
47 | });
48 | persistence.ConnectionBuilder(
49 | connectionBuilder: () => new NpgsqlConnection(sqlPersistenceConnectionString));
50 |
51 | endpointConfiguration.EnableOutbox();
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/src/Finance.ViewModelComposition/ReviewReservedTicketsLoadedSubscriber.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Reservations.ViewModelComposition.Events;
3 | using ServiceComposer.AspNetCore;
4 | using System;
5 | using System.Linq;
6 | using Microsoft.AspNetCore.Mvc;
7 |
8 | namespace Finance.ViewModelComposition
9 | {
10 | class ReviewReservedTicketsLoadedSubscriber : ICompositionEventsSubscriber
11 | {
12 | [HttpGet("/reservations/review")]
13 | public void Subscribe(ICompositionEventsPublisher publisher)
14 | {
15 | publisher.Subscribe(async (@event,request) =>
16 | {
17 | var ids = @event.ReservedTicketsViewModel.Keys.ToArray();
18 | await using var db = new Data.FinanceContext();
19 | var ticketPrices = await db.TicketPrices
20 | .Where(ticketPrice => ids.Contains(ticketPrice.Id))
21 | .ToListAsync();
22 |
23 | Guid reservationId = @event.Reservation.Id;
24 | var reservedTickets =
25 | (
26 | await db.ReservedTickets
27 | .Where(r => r.ReservationId == reservationId)
28 | .ToListAsync()
29 | )
30 | .GroupBy(t => t.TicketId)
31 | .ToDictionary(g=>g.Key, g=>g.Count());
32 |
33 | var reservationTotalPrice = 0m;
34 |
35 | foreach (var ticketPrice in ticketPrices)
36 | {
37 | var ticketId = (int)ticketPrice.Id;
38 | var reservedTicketViewModel = @event.ReservedTicketsViewModel[ticketId];
39 |
40 | var currentReservedTicketQuantity = reservedTickets[ticketId];
41 | var currentReservedTicketTotalPrice = ticketPrice.Price * currentReservedTicketQuantity;
42 |
43 | reservationTotalPrice += currentReservedTicketTotalPrice;
44 |
45 | reservedTicketViewModel.TicketPrice = ticketPrice.Price;
46 | reservedTicketViewModel.TotalPrice = currentReservedTicketTotalPrice;
47 | }
48 |
49 | @event.Reservation.TotalPrice = reservationTotalPrice;
50 |
51 | /*
52 | * it's a demo, production code should check for cookie existence
53 | */
54 | var selectedPaymentMethodId = int.Parse(request.Cookies["reservation-payment-method-id"]);
55 | var paymentMethod = await db.PaymentMethods
56 | .Where(pm => pm.Id == selectedPaymentMethodId)
57 | .SingleAsync();
58 |
59 | var viewModel = request.GetComposedResponseModel();
60 | viewModel.PaymentMethod = paymentMethod;
61 | });
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Reservations.ViewModelComposition/TicketsReservationGetHandler.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using Microsoft.EntityFrameworkCore;
3 | using Reservations.ViewModelComposition.Events;
4 | using ServiceComposer.AspNetCore;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Dynamic;
8 | using System.Linq;
9 | using System.Threading.Tasks;
10 | using Microsoft.AspNetCore.Mvc;
11 |
12 | namespace Reservations.ViewModelComposition
13 | {
14 | class TicketsReservationGetHandler : ICompositionRequestsHandler
15 | {
16 | [HttpGet("/reservations")]
17 | [HttpGet("/reservations/review")]
18 | public async Task Handle(HttpRequest request)
19 | {
20 | var vm = request.GetComposedResponseModel();
21 |
22 | if (!request.Cookies.ContainsKey("reservation-id"))
23 | {
24 | vm.Reservation = null;
25 | return;
26 | }
27 |
28 | var reservationId = new Guid(request.Cookies["reservation-id"]);
29 | await using var db = new Data.ReservationsContext();
30 | var reservation = await db.Reservations
31 | .Where(r => r.Id == reservationId)
32 | .Include(r => r.ReservedTickets)
33 | .SingleOrDefaultAsync();
34 |
35 | if (reservation == null)
36 | {
37 | vm.Reservation = null;
38 | return;
39 | }
40 |
41 | vm.Reservation = new ExpandoObject();
42 | vm.Reservation.Id = reservationId;
43 |
44 | IEnumerable<(int TicketId, int Quantity)> reservedTickets = reservation.ReservedTickets
45 | .GroupBy(t => t.TicketId)
46 | .Select(g => (g.Key, g.Count()));
47 |
48 | var reservedTicketsViewModel = MapToDictionary(reservedTickets);
49 |
50 | var compositionContext = request.GetCompositionContext();
51 | await compositionContext.RaiseEvent(new ReservedTicketsLoaded()
52 | {
53 | Reservation = vm.Reservation,
54 | ReservedTicketsViewModel = reservedTicketsViewModel
55 | });
56 |
57 | vm.Reservation.ReservedTickets = reservedTicketsViewModel.Values.ToList();
58 | }
59 |
60 | static IDictionary MapToDictionary(IEnumerable<(int TicketId, int Quantity)> reservedTickets)
61 | {
62 | var reservedTicketsViewModel = new Dictionary();
63 |
64 | foreach ((int ticketId, int quantity) in reservedTickets)
65 | {
66 | dynamic vm = new ExpandoObject();
67 | vm.TicketId = ticketId;
68 | vm.Quantity = quantity;
69 |
70 | reservedTicketsViewModel[ticketId] = vm;
71 | }
72 |
73 | return reservedTicketsViewModel;
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/Reservations.Service/Handlers/ReservationCheckedoutHandler.cs:
--------------------------------------------------------------------------------
1 | using NServiceBus;
2 | using Reservations.Messages.Events;
3 | using Reservations.Data;
4 | using Reservations.Data.Models;
5 | using System;
6 | using System.Drawing;
7 | using System.Linq;
8 | using System.Threading.Tasks;
9 | using Console = Colorful.Console;
10 |
11 |
12 | namespace Reservations.Service.Handlers
13 | {
14 | class ReservationCheckedoutHandler : IHandleMessages
15 | {
16 | public async Task Handle(IReservationCheckedout message, IMessageHandlerContext context)
17 | {
18 | /*
19 | * Creation of an order is not a Reservations responsibility.
20 | * It's probably much more a Sales responsibility, in which case
21 | * having this handler here is a clear boundaries violation.
22 | * The only reason to keep it here is that an Order does nothing
23 | * in this demo. Its creation completes the payment process and
24 | * nothing else. It made little sense to increase even more the
25 | * complexity of the demo, creating a Sales endpoint just for the
26 | * purpose of hosting this message handler. If Sales had more
27 | * responsibilities in the demo it would have deserved its own
28 | * endpoint.
29 | */
30 | Console.WriteLine($"Ready to create order for reservation '{message.ReservationId}'.", Color.Green);
31 | await using var db = new ReservationsContext();
32 | var order = new Order
33 | {
34 | Id = Guid.NewGuid(),
35 | ReservationId = message.ReservationId
36 | };
37 | order.OrderedTickets = message.Tickets
38 | .GroupBy(t => t)
39 | .Select(g => new OrderedTicket()
40 | {
41 | OrderId = order.Id,
42 | TicketId = g.Key,
43 | Quantity = g.Count(),
44 | }).ToList();
45 |
46 | /*
47 | * Demo utilizes LearningTransport and SQL, with no
48 | * Outbox configured to simplify the F5 experience.
49 | * This, however, means that the Publish operation
50 | * and the below database transaction are not atomic.
51 | *
52 | * There isn't really a chance for the LearningTransport
53 | * to fail, but in production Outbox should be used when
54 | * using transports with no support for transactions.
55 | */
56 | await context.Publish(new Messages.OrderCreated()
57 | {
58 | OrderId = order.Id,
59 | ReservationId = message.ReservationId
60 | });
61 |
62 | db.Orders.Add(order);
63 | await db.SaveChangesAsync(context.CancellationToken);
64 |
65 | Console.WriteLine($"Order '{order.Id}' created, IOrderCreated event published.", Color.Green);
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Reservations.Service/Policies/ReservationPolicy.cs:
--------------------------------------------------------------------------------
1 | using NServiceBus;
2 | using Reservations.Messages.Commands;
3 | using Reservations.Messages.Events;
4 | using Reservations.Service.Messages;
5 | using System;
6 | using System.Drawing;
7 | using System.Threading.Tasks;
8 | using Console = Colorful.Console;
9 |
10 | namespace Reservations.Service.Policies
11 | {
12 | class ReservationPolicy : Saga,
13 | IAmStartedByMessages,
14 | IHandleMessages
15 | {
16 | public class State : ContainSagaData
17 | {
18 | public Guid ReservationId { get; set; }
19 | }
20 |
21 | protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper)
22 | {
23 | mapper.MapSaga(saga => saga.ReservationId)
24 | .ToMessage(m => m.ReservationId)
25 | .ToMessage(m => m.ReservationId);
26 | }
27 |
28 | public async Task Handle(ReserveTicket message, IMessageHandlerContext context)
29 | {
30 | Console.WriteLine($"Adding ticket '{message.TicketId}' to reservation '{message.ReservationId}'.", Color.Green);
31 |
32 | Data.ReservationId = message.ReservationId;
33 |
34 | /*
35 | * Tickets are reserved indefinitely. Usually tickets booking websites
36 | * Allow people to reserve a ticket, or keep it in the shopping cart for
37 | * a fixed amount of time, e.g. 10 minutes. After 10 minutes if tickets
38 | * are not purchased they are automatically released back to the pool
39 | * of available tickets. This can be easily achieved using a timeout in
40 | * this reservation saga. The timeout should be kicked of once the first
41 | * time a ticket is reserved. If the timeout expires and the reservation
42 | * is still existing all reserved tickets can be released.
43 | */
44 | await context.SendLocal(new MarkTicketAsReserved()
45 | {
46 | ReservationId = message.ReservationId,
47 | TicketId = message.TicketId
48 | });
49 |
50 | Console.WriteLine($"MarkTicketAsReserved request sent.", Color.Green);
51 | }
52 |
53 | public Task Handle(IReservationCheckedout message, IMessageHandlerContext context)
54 | {
55 | /*
56 | * We're done.
57 | *
58 | * We can debate if this should complete when the payment is authorized
59 | * or, as we are doing, a little earlier. By using the IReservationCheckedout
60 | * we could fall into the following scenario:
61 | * - the reservation is checked out
62 | * - the card authorization fails
63 | * - the reservation is gone.
64 | *
65 | * Poor experience. On the other end by waiting for the card authorization
66 | * we're locking tickets for more time. It's a business decision, not a
67 | * technical one.
68 | */
69 |
70 | Console.WriteLine($"IReservationCheckedout, I'm done.", Color.Green);
71 | MarkAsComplete();
72 | return Task.CompletedTask;
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/Website/Views/Shared/_Layout.cshtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | @ViewData["Title"] - Website
7 |
8 |
9 |
10 |
11 |
12 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
Website
25 |
27 |
28 |
29 |
39 |
40 |
41 |
42 |
43 |
44 | @RenderBody()
45 |
46 |
47 |
48 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
65 |
71 |
72 |
73 |
74 | @RenderSection("Scripts", required: false)
75 |
76 |
77 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Finance.PaymentGateway",
9 | "type": "coreclr",
10 | "request": "launch",
11 | "program": "${workspaceFolder}/src/Finance.PaymentGateway/bin/Debug/net8.0/Finance.PaymentGateway.dll",
12 | "args": [],
13 | "cwd": "${workspaceFolder}/src/Finance.PaymentGateway",
14 | "stopAtEntry": false,
15 | "console": "integratedTerminal",
16 | },
17 | {
18 | "name": "Finance.Service",
19 | "type": "coreclr",
20 | "request": "launch",
21 | "program": "${workspaceFolder}/src/Finance.Service/bin/Debug/net8.0/Finance.Service.dll",
22 | "args": [],
23 | "cwd": "${workspaceFolder}/src/Finance.Service",
24 | "stopAtEntry": false,
25 | "console": "integratedTerminal",
26 | },
27 | {
28 | "name": "Reservations.Service",
29 | "type": "coreclr",
30 | "request": "launch",
31 | "program": "${workspaceFolder}/src/Reservations.Service/bin/Debug/net8.0/Reservations.Service.dll",
32 | "args": [],
33 | "cwd": "${workspaceFolder}/src/Reservations.Service",
34 | "stopAtEntry": false,
35 | "console": "integratedTerminal",
36 | },
37 | {
38 | "name": "Shipping.Service",
39 | "type": "coreclr",
40 | "request": "launch",
41 | "program": "${workspaceFolder}/src/Shipping.Service/bin/Debug/net8.0/Shipping.Service.dll",
42 | "args": [],
43 | "cwd": "${workspaceFolder}/src/Shipping.Service",
44 | "stopAtEntry": false,
45 | "console": "integratedTerminal",
46 | },
47 | {
48 | "name": "Website",
49 | "type": "coreclr",
50 | "request": "launch",
51 | "program": "${workspaceFolder}/src/Website/bin/Debug/net8.0/Website.dll",
52 | "args": [],
53 | "cwd": "${workspaceFolder}/src/Website",
54 | "stopAtEntry": false,
55 | "console": "integratedTerminal",
56 | "launchBrowser": {
57 | "enabled": true,
58 | "args": "${auto-detect-url}",
59 | "windows": {
60 | "command": "cmd.exe",
61 | "args": "/C start ${auto-detect-url}"
62 | },
63 | "osx": {
64 | "command": "open"
65 | },
66 | "linux": {
67 | "command": "xdg-open"
68 | }
69 | },
70 | "env": {
71 | "ASPNETCORE_ENVIRONMENT": "Development"
72 | },
73 | "sourceFileMap": {
74 | "/Views": "${workspaceFolder}/src/Website/Views"
75 | }
76 | }
77 | ],
78 | "compounds": [
79 | {
80 | "name": "Demo - (build)",
81 | "preLaunchTask": "Build solution",
82 | "configurations": [
83 | "Finance.PaymentGateway",
84 | "Finance.Service",
85 | "Reservations.Service",
86 | "Shipping.Service",
87 | "Website"]
88 | },
89 | {
90 | "name": "Demo - (build & deploy data)",
91 | "preLaunchTask": "Build & create databases",
92 | "configurations": [
93 | "Finance.PaymentGateway",
94 | "Finance.Service",
95 | "Reservations.Service",
96 | "Shipping.Service",
97 | "Website"]
98 | },
99 | {
100 | "name": "Demo - (no build)",
101 | "configurations": [
102 | "Finance.PaymentGateway",
103 | "Finance.Service",
104 | "Reservations.Service",
105 | "Shipping.Service",
106 | "Website"]
107 | }
108 | ]
109 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to the (state) machine - Demos
2 |
3 | A ticket booking system - based on SOA principles.
4 |
5 | This demo aims to demonstrate how to use sagas to overcome the architectural limitations of Process Managers in distributed systems. This is the support demo of my [Welcome to the (state) machine](https://milestone.topics.it/talks/welcome-to-the-state-machine.html) talk.
6 |
7 | The demo assumes some knowledge of `service boundaries` and the role of `ViewModel Composition` in SOA-based systems. I recommend watching the following 2 talks to learn more about figuring out service boundaries and dealing with UI/ViewModel aspects in a microservices/SOA system without reintroducing coupling by making requests/responses between the services.
8 |
9 | - [Finding your service boundaries - a practical guide](https://www.youtube.com/watch?v=tVnIUZbsxWI) by [@adamralph](https://twitter.com/adamralph)
10 | - [All Our Aggregates Are Wrong](https://www.youtube.com/watch?v=KkzvQSuYd5I) by [@mauroservienti](https://twitter.com/mauroservienti) (myself)
11 |
12 | An exhaustive dissertation about `ViewModel Composition` is available on my blog in the [ViewModel Composition series](https://milestone.topics.it/categories/view-model-composition).
13 |
14 | ## Requirements
15 |
16 | The following requirements must be met to run the demos successfully:
17 |
18 | - [Visual Studio Code](https://code.visualstudio.com/) and the [Dev containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers).
19 | - [Docker](https://www.docker.com/get-started) must be pre-installed on the machine.
20 | - The repository `devcontainer` setup requires `docker-compose` to be installed on the machine.
21 |
22 | ## How to configure Visual Studio Code to run the demos
23 |
24 | - Clone the repository
25 | - On Windows, make sure to clone on a short path, e.g., `c:\dev`, to avoid any "path too long" error
26 | - Open one of the demo folders in Visual Studio Code
27 | - Make sure Docker is running
28 | - If you're using Docker for Windows with Hyper-V, make sure that the cloned folder, or a parent folder, is mapped in Docker
29 | - Open the Visual Studio Code command palette (`F1` on all supported operating systems, for more information on VS Code keyboard shortcuts, refer to [this page](https://www.arungudelli.com/microsoft/visual-studio-code-keyboard-shortcut-cheat-sheet-windows-mac-linux/))
30 | - Type `Reopen in Container`, the command palette supports auto-completion; the command should be available by typing `reop`
31 |
32 | Wait for Visual Studio Code Dev containers extension to:
33 |
34 | - download the required container images
35 | - configure the docker environment
36 | - configure the remote Visual Studio Code instance with the required extensions
37 |
38 | > Note: no changes will be made to your Visual Studio Code installation; all changes will be applied to the VS Code instance running in the remote container
39 |
40 | The repository `devcontainer` configuration will:
41 |
42 | - One or more container instances:
43 | - One RabbitMQ instance with management plugin support
44 | - One .NET-enabled container where the repository source code will be mapped
45 | - A few PostgreSQL instances
46 | - Configure the VS Code remote instance with:
47 | - The C# extension (`ms-dotnettools.csharp`)
48 | - The PostgreSQL Explorer extension (`ckolkman.vscode-postgres`)
49 |
50 | Once the configuration is completed, VS Code will show a new `Ports` tab in the bottom-docked terminal area. The `Ports` tab will list all the ports the remote containers expose.
51 |
52 | ## Containers connection information
53 |
54 | The default RabbitMQ credentials are:
55 |
56 | - Username: `guest`
57 | - Password: `guest`
58 |
59 | The default PostgreSQL credentials are:
60 |
61 | - User: `db_user`
62 | - Password: `P@ssw0rd`
63 |
64 | ## How to run the demos
65 |
66 | To execute the demo, open the root folder in VS Code, press `F1`, and search for `Reopen in container`. Wait for the Dev Container to complete the setup process.
67 |
68 | Once the demo content has been reopened in the dev container:
69 |
70 | 1. Press `F1`, search for `Run task`, and execute the desired task to build the solution or to build the solution and deploy the required data
71 | 2. Go to the `Run and Debug` VS Code section and select the command you want to execute.
72 |
73 | ### Disclaimer
74 |
75 | This demo is built using [NServiceBus Sagas](https://docs.particular.net/nservicebus/sagas/); I work for [Particular Software](https://particular.net/), the makers of NServiceBus.
76 |
--------------------------------------------------------------------------------
/src/Shipping.Service/Policies/ShippingPolicy.cs:
--------------------------------------------------------------------------------
1 | using Finance.Messages.Events;
2 | using NServiceBus;
3 | using Reservations.Messages.Events;
4 | using Shipping.Data;
5 | using Shipping.Messages.Commands;
6 | using Shipping.Service.Messages;
7 | using System;
8 | using System.Drawing;
9 | using System.Threading.Tasks;
10 | using Console = Colorful.Console;
11 |
12 | namespace Shipping.Service.Policies
13 | {
14 | class ShippingPolicy : Saga,
15 | IAmStartedByMessages,
16 | IAmStartedByMessages,
17 | IAmStartedByMessages
18 | {
19 | protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper)
20 | {
21 | mapper.MapSaga(saga => saga.ReservationId)
22 | .ToMessage(m => m.ReservationId)
23 | .ToMessage(m => m.ReservationId)
24 | .ToMessage(m => m.ReservationId);
25 | }
26 |
27 | public Task Handle(InitializeReservationShippingPolicy message, IMessageHandlerContext context)
28 | {
29 | Console.WriteLine($"DeliveryOption {message.DeliveryOption} set for reservation '{message.ReservationId}'. Verifying if shipment can started...", Color.Green);
30 |
31 | Data.DeliveryOption = message.DeliveryOption;
32 | Data.DeliveryOptionDefined = true;
33 |
34 | return StartShipmentProcessIfEverythingIsOk(context);
35 | }
36 |
37 | public Task Handle(IPaymentSucceeded message, IMessageHandlerContext context)
38 | {
39 | Console.WriteLine($"Payment for reservation '{message.ReservationId}' succeeded. Verifying if shipment can started...", Color.Green);
40 |
41 | Data.PaymentSucceeded = true;
42 |
43 | return StartShipmentProcessIfEverythingIsOk(context);
44 | }
45 |
46 | public Task Handle(IOrderCreated message, IMessageHandlerContext context)
47 | {
48 | Console.WriteLine($"Order '{message.OrderId}' for reservation '{message.ReservationId}' created. Verifying if shipment can started...", Color.Green);
49 |
50 | Data.ReservationId = message.ReservationId;
51 | Data.OrderId = message.OrderId;
52 | Data.OrderCreated = true;
53 |
54 | return StartShipmentProcessIfEverythingIsOk(context);
55 | }
56 |
57 | private Task StartShipmentProcessIfEverythingIsOk(IMessageHandlerContext context)
58 | {
59 | if (Data.OrderCreated && Data.PaymentSucceeded && Data.DeliveryOptionDefined)
60 | {
61 | MarkAsComplete();
62 |
63 | switch (Data.DeliveryOption)
64 | {
65 | case DeliveryOptions.ShipAtHome:
66 | /*
67 | * Send a message locally to the courier
68 | * gateway to request a pick-up. And finally
69 | * publish the OrderShipped event.
70 | */
71 | Console.WriteLine($"Order '{Data.OrderId}' will be shipped ASAP...", Color.Green);
72 | return context.Publish(new OrderShipped()
73 | {
74 | ShipmentId = Guid.NewGuid(),
75 | OrderId = Data.OrderId,
76 | ReservationId = Data.ReservationId
77 | });
78 |
79 | case DeliveryOptions.CollectAtTheVenue:
80 | /*
81 | * Send a message locally to kick-off another
82 | * saga to handle the physical delivery, this
83 | * second saga (outside the scope of this demo)
84 | * is responsible to handle the delivery to
85 | * venues.
86 | * The complexity is that an order can contain
87 | * tickets for different events that should be
88 | * delivered at different venues at different
89 | * times.
90 | *
91 | * In this demo this message is never handled and
92 | * goes nowhere.
93 | */
94 | Console.WriteLine($"Order '{Data.OrderId}' will be shipped at the venue...", Color.Green);
95 | return context.SendLocal(new StoreReservationForVenueDelivery()
96 | {
97 | OrderId = Data.OrderId,
98 | ReservationId = Data.ReservationId
99 | });
100 | }
101 | }
102 |
103 | Console.WriteLine($"Shipment for Order '{Data.OrderId}' cannot be started yet...", Color.Yellow);
104 |
105 | return Task.CompletedTask;
106 | }
107 | }
108 |
109 | class ShippingPolicyState : ContainSagaData
110 | {
111 | public Guid ReservationId { get; set; }
112 | public Guid OrderId { get; set; }
113 | public bool PaymentSucceeded { get; set; }
114 | public bool OrderCreated { get; set; }
115 | public bool DeliveryOptionDefined { get; set; }
116 | public DeliveryOptions DeliveryOption { get; set; }
117 | }
118 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Remove the line below if you want to inherit .editorconfig settings from higher directories
2 | root = true
3 |
4 | # C# files
5 | [*.cs]
6 |
7 | #### Core EditorConfig Options ####
8 |
9 | # Indentation and spacing
10 | indent_size = 4
11 | indent_style = space
12 | tab_width = 4
13 |
14 | # New line preferences
15 | end_of_line = crlf
16 | insert_final_newline = false
17 |
18 | #### .NET Coding Conventions ####
19 |
20 | # this. and Me. preferences
21 | dotnet_style_qualification_for_event = false:silent
22 | dotnet_style_qualification_for_field = false:silent
23 | dotnet_style_qualification_for_method = false:silent
24 | dotnet_style_qualification_for_property = false:silent
25 |
26 | # Language keywords vs BCL types preferences
27 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent
28 | dotnet_style_predefined_type_for_member_access = true:silent
29 |
30 | # Parentheses preferences
31 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
32 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
33 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
34 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
35 |
36 | # Modifier preferences
37 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
38 |
39 | # Expression-level preferences
40 | csharp_style_deconstructed_variable_declaration = true:suggestion
41 | csharp_style_inlined_variable_declaration = true:suggestion
42 | csharp_style_throw_expression = true:suggestion
43 | dotnet_style_coalesce_expression = true:suggestion
44 | dotnet_style_collection_initializer = true:suggestion
45 | dotnet_style_explicit_tuple_names = true:suggestion
46 | dotnet_style_null_propagation = true:suggestion
47 | dotnet_style_object_initializer = true:suggestion
48 | dotnet_style_prefer_auto_properties = true:silent
49 | dotnet_style_prefer_compound_assignment = true:suggestion
50 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent
51 | dotnet_style_prefer_conditional_expression_over_return = true:silent
52 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
53 | dotnet_style_prefer_inferred_tuple_names = true:suggestion
54 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
55 |
56 | # Field preferences
57 | dotnet_style_readonly_field = true:suggestion
58 |
59 | # Parameter preferences
60 | dotnet_code_quality_unused_parameters = all:suggestion
61 |
62 | #### C# Coding Conventions ####
63 |
64 | # var preferences
65 | csharp_style_var_elsewhere = false:silent
66 | csharp_style_var_for_built_in_types = false:silent
67 | csharp_style_var_when_type_is_apparent = false:silent
68 |
69 | # Expression-bodied members
70 | csharp_style_expression_bodied_accessors = true:silent
71 | csharp_style_expression_bodied_constructors = false:silent
72 | csharp_style_expression_bodied_indexers = true:silent
73 | csharp_style_expression_bodied_lambdas = true:silent
74 | csharp_style_expression_bodied_local_functions = false:silent
75 | csharp_style_expression_bodied_methods = false:silent
76 | csharp_style_expression_bodied_operators = false:silent
77 | csharp_style_expression_bodied_properties = true:silent
78 |
79 | # Pattern matching preferences
80 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
81 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
82 |
83 | # Null-checking preferences
84 | csharp_style_conditional_delegate_call = true:suggestion
85 |
86 | # Modifier preferences
87 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async
88 |
89 | # Code-block preferences
90 | csharp_prefer_braces = true:silent
91 |
92 | # Expression-level preferences
93 | csharp_prefer_simple_default_expression = true:suggestion
94 | csharp_style_pattern_local_over_anonymous_function = true:suggestion
95 | csharp_style_prefer_index_operator = true:suggestion
96 | csharp_style_prefer_range_operator = true:suggestion
97 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion
98 | csharp_style_unused_value_expression_statement_preference = discard_variable:silent
99 |
100 | #### C# Formatting Rules ####
101 |
102 | # New line preferences
103 | csharp_new_line_before_catch = true
104 | csharp_new_line_before_else = true
105 | csharp_new_line_before_finally = true
106 | csharp_new_line_before_members_in_anonymous_types = true
107 | csharp_new_line_before_members_in_object_initializers = true
108 | csharp_new_line_before_open_brace = all
109 | csharp_new_line_between_query_expression_clauses = true
110 |
111 | # Indentation preferences
112 | csharp_indent_block_contents = true
113 | csharp_indent_braces = false
114 | csharp_indent_case_contents = true
115 | csharp_indent_case_contents_when_block = true
116 | csharp_indent_labels = one_less_than_current
117 | csharp_indent_switch_labels = true
118 |
119 | # Space preferences
120 | csharp_space_after_cast = false
121 | csharp_space_after_colon_in_inheritance_clause = true
122 | csharp_space_after_comma = true
123 | csharp_space_after_dot = false
124 | csharp_space_after_keywords_in_control_flow_statements = true
125 | csharp_space_after_semicolon_in_for_statement = true
126 | csharp_space_around_binary_operators = before_and_after
127 | csharp_space_around_declaration_statements = false
128 | csharp_space_before_colon_in_inheritance_clause = true
129 | csharp_space_before_comma = false
130 | csharp_space_before_dot = false
131 | csharp_space_before_open_square_brackets = false
132 | csharp_space_before_semicolon_in_for_statement = false
133 | csharp_space_between_empty_square_brackets = false
134 | csharp_space_between_method_call_empty_parameter_list_parentheses = false
135 | csharp_space_between_method_call_name_and_opening_parenthesis = false
136 | csharp_space_between_method_call_parameter_list_parentheses = false
137 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
138 | csharp_space_between_method_declaration_name_and_open_parenthesis = false
139 | csharp_space_between_method_declaration_parameter_list_parentheses = false
140 | csharp_space_between_parentheses = false
141 | csharp_space_between_square_brackets = false
142 |
143 | # Wrapping preferences
144 | csharp_preserve_single_line_blocks = true
145 | csharp_preserve_single_line_statements = true
146 |
147 |
--------------------------------------------------------------------------------
/src/Finance.Service/Policies/PaymentPolicy.cs:
--------------------------------------------------------------------------------
1 | using Finance.Messages.Commands;
2 | using Finance.PaymentGateway.Messages;
3 | using Finance.Service.Messages;
4 | using NServiceBus;
5 | using Reservations.Messages.Events;
6 | using System;
7 | using System.Drawing;
8 | using System.Threading.Tasks;
9 | using Console = Colorful.Console;
10 |
11 | namespace Finance.Service.Policies
12 | {
13 | class PaymentPolicy : Saga,
14 | IAmStartedByMessages,
15 | IAmStartedByMessages,
16 | IHandleMessages,
17 | IHandleMessages,
18 | IHandleTimeouts,
19 | IHandleMessages,
20 | IHandleMessages
21 | {
22 | protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper)
23 | {
24 | mapper.MapSaga(saga => saga.ReservationId)
25 | .ToMessage(m => m.ReservationId)
26 | .ToMessage(m => m.ReservationId)
27 | .ToMessage(m => m.ReservationId)
28 | .ToMessage(m => m.ReservationId);
29 | }
30 |
31 | public Task Handle(IReservationCheckedout message, IMessageHandlerContext context)
32 | {
33 | Console.WriteLine($"Reservation '{message.ReservationId}' checked out.", Color.Green);
34 |
35 | Data.ReservationId = message.ReservationId;
36 | Data.ReservationCheckedOut = true;
37 |
38 | return InitiatePaymentProcessing(context);
39 | }
40 |
41 | public Task Handle(InitializeReservationPaymentPolicy message, IMessageHandlerContext context)
42 | {
43 | Console.WriteLine($"Adding payment method '{message.PaymentMethodId}' to reservation '{message.ReservationId}'.", Color.Green);
44 |
45 | Data.PaymentMethodId = message.PaymentMethodId;
46 | Data.PaymentMethodSet = true;
47 |
48 | return InitiatePaymentProcessing(context);
49 | }
50 |
51 | Task InitiatePaymentProcessing(IMessageHandlerContext context)
52 | {
53 | Console.WriteLine($"Going to check if payment processing can be started.", Color.Green);
54 |
55 | if (Data.ReservationCheckedOut && Data.PaymentMethodSet)
56 | {
57 | Console.WriteLine($"All information required to start the payment process have been collected.", Color.Green);
58 |
59 | return context.SendLocal(new InitiatePaymentProcessing()
60 | {
61 | ReservationId = Data.ReservationId
62 | });
63 | }
64 |
65 | Console.WriteLine($"Not all information are available to start the payment process.", Color.Yellow);
66 |
67 | return Task.CompletedTask;
68 | }
69 |
70 | public async Task Handle(InitiatePaymentProcessing message, IMessageHandlerContext context)
71 | {
72 | Console.WriteLine($"Ready to start the payment process for reservation '{message.ReservationId}'. First step is to authorize the credit card with Id '{Data.PaymentMethodId}'.", Color.Green);
73 |
74 | Data.CardAuthorizationRequested = true;
75 |
76 | await context.Send(new AuthorizeCard()
77 | {
78 | ReservationId = Data.ReservationId,
79 | PaymentMethodId = Data.PaymentMethodId
80 | });
81 |
82 | Console.WriteLine($"Authorization requested.", Color.Green);
83 | }
84 |
85 | public async Task Handle(CardAuthorizedResponse message, IMessageHandlerContext context)
86 | {
87 | Console.WriteLine($"Card authorized.", Color.Green);
88 |
89 | /*
90 | * Intentionally ignoring authorization failures
91 | * --------------------------------------------------
92 | * The demo starts from the assumption that card
93 | * authorization never fails. To handle such scenario
94 | * a couple more messages are needed and one more
95 | * interaction with Reservation to release tickets.
96 | * Or a timeout in Reservation to handle payment
97 | * missing events.
98 | */
99 | Data.CardAuthorized = true;
100 | Data.PaymentAuthorizationId = message.AuthorizationId;
101 |
102 | await RequestTimeout(context, TimeSpan.FromMinutes(20));
103 | await context.Publish(new PaymentAuthorized() { ReservationId = Data.ReservationId });
104 |
105 | Console.WriteLine($"CardAuthorizationTimeout set, and PaymentAutorized published.", Color.Green);
106 | }
107 |
108 | public Task Timeout(CardAuthorizationTimeout state, IMessageHandlerContext context)
109 | {
110 | Console.WriteLine($"Card Authorization timed out, going to release money.", Color.Green);
111 |
112 | MarkAsComplete();
113 | return context.Send(new ReleaseCardAuthorization()
114 | {
115 | AuthorizationId = Data.PaymentAuthorizationId,
116 | ReservationId = Data.ReservationId
117 | });
118 | }
119 |
120 | public Task Handle(IOrderCreated message, IMessageHandlerContext context)
121 | {
122 | Console.WriteLine($"Order '{message.OrderId}' for reservation '{message.ReservationId}' created, going to confirm card payment.", Color.Green);
123 |
124 | Data.OrderId = message.OrderId;
125 | return context.Send(new ChargeCard()
126 | {
127 | AuthorizationId = Data.PaymentAuthorizationId,
128 | ReservationId = Data.ReservationId
129 | });
130 | }
131 |
132 | public Task Handle(CardChargedResponse message, IMessageHandlerContext context)
133 | {
134 | Console.WriteLine($"Card charged, I'm done. Publishing PaymentSucceeded event.", Color.Green);
135 |
136 | MarkAsComplete();
137 | return context.Publish(new PaymentSucceeded()
138 | {
139 | OrderId = Data.OrderId,
140 | ReservationId = Data.ReservationId
141 | });
142 | }
143 | }
144 |
145 | class CardAuthorizationTimeout { }
146 |
147 | class PaymentPolicyState : ContainSagaData
148 | {
149 | public Guid ReservationId { get; set; }
150 | public bool CardCharged { get; set; }
151 | public bool CardAuthorized { get; set; }
152 | public bool CardAuthorizationRequested { get; set; }
153 | public int PaymentMethodId { get; set; }
154 | public bool ReservationCheckedOut { get; set; }
155 | public bool PaymentMethodSet { get; set; }
156 | public Guid PaymentAuthorizationId { get; set; }
157 | public Guid OrderId { get; set; }
158 | }
159 | }
--------------------------------------------------------------------------------
/src/welcome-to-the-state-machine-demos.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.28803.352
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{900D7EA7-8C57-4CF7-94B5-1D8625367EC9}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NServiceBus.Shared", "NServiceBus.Shared\NServiceBus.Shared.csproj", "{4882F331-74B9-43AA-89E5-E87E240AD0FD}"
9 | EndProject
10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Finance", "Finance", "{1D1589DF-CEB2-4B1C-B8BF-A86CD7A7B4A6}"
11 | EndProject
12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Ticketing", "Ticketing", "{82F40981-48CC-4B75-9738-B394AB990E7A}"
13 | EndProject
14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Reservation", "Reservation", "{32C8E39D-93AC-4AF9-981B-152478ADFCA5}"
15 | EndProject
16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Website", "Website\Website.csproj", "{4A60D764-E055-410B-8C64-5359031D433D}"
17 | EndProject
18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ticketing.Data", "Ticketing.Data\Ticketing.Data.csproj", "{6EF30F98-537C-47BD-979A-82156C1BB5A5}"
19 | EndProject
20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Finance.ViewModelComposition", "Finance.ViewModelComposition\Finance.ViewModelComposition.csproj", "{5AC969AF-777E-478B-85B9-66634B8125E2}"
21 | EndProject
22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ticketing.ViewModelComposition", "Ticketing.ViewModelComposition\Ticketing.ViewModelComposition.csproj", "{DDE1A4D9-C920-4E6F-BB5D-61EFA8BB7C44}"
23 | EndProject
24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ticketing.ViewModelComposition.Events", "Ticketing.ViewModelComposition.Events\Ticketing.ViewModelComposition.Events.csproj", "{A0B0540B-4B1C-4307-B094-8AEF0B62F514}"
25 | EndProject
26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Finance.Data", "Finance.Data\Finance.Data.csproj", "{B2638542-666E-4A9D-AAD4-CAC77D948871}"
27 | EndProject
28 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reservations.Data", "Reservations.Data\Reservations.Data.csproj", "{A0B7A7DB-5474-412F-A4E0-4010132F2CE9}"
29 | EndProject
30 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reservations.ViewModelComposition", "Reservations.ViewModelComposition\Reservations.ViewModelComposition.csproj", "{FDA6D9B8-9373-4E3A-9F39-3C97BA421470}"
31 | EndProject
32 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reservations.Service", "Reservations.Service\Reservations.Service.csproj", "{7D172217-98C8-4311-8318-6900F6F8D50D}"
33 | EndProject
34 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reservations.Messages.Commands", "Reservations.Messages.Commands\Reservations.Messages.Commands.csproj", "{02D3071F-EF79-4FD9-9752-4B041099C459}"
35 | EndProject
36 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Finance.Messages.Events", "Finance.Messages.Events\Finance.Messages.Events.csproj", "{D80CBA22-4E4A-49E5-B875-61558962FD53}"
37 | EndProject
38 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reservations.ViewModelComposition.Events", "Reservations.ViewModelComposition.Events\Reservations.ViewModelComposition.Events.csproj", "{03B6E372-5BC9-488B-91F9-C01A7BCAF07A}"
39 | EndProject
40 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Finance.Service", "Finance.Service\Finance.Service.csproj", "{6A46B867-16ED-4700-AC6E-FBB6FC49B280}"
41 | EndProject
42 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Finance.Messages.Commands", "Finance.Messages.Commands\Finance.Messages.Commands.csproj", "{0C6DDAD3-BD78-49AF-AFD3-17FFC8846167}"
43 | EndProject
44 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reservations.Messages.Events", "Reservations.Messages.Events\Reservations.Messages.Events.csproj", "{C88C6770-CFF4-48E7-A313-6905A6C14AC2}"
45 | EndProject
46 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Finance.PaymentGateway", "Finance.PaymentGateway\Finance.PaymentGateway.csproj", "{FB207873-8FE2-42F9-8AEE-A1DCD3E48035}"
47 | EndProject
48 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Finance.PaymentGateway.Messages", "Finance.PaymentGateway.Messages\Finance.PaymentGateway.Messages.csproj", "{165570E1-C782-47F4-AA6D-00546682AC13}"
49 | EndProject
50 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shipping", "Shipping", "{1EFDBC80-D09C-4B00-9984-6E21E6422D74}"
51 | EndProject
52 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shipping.Service", "Shipping.Service\Shipping.Service.csproj", "{5F07AED9-3A33-4A54-80D1-A0164C886EF8}"
53 | EndProject
54 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shipping.Messages.Events", "Shipping.Messages.Events\Shipping.Messages.Events.csproj", "{47A17268-A0B1-48C9-93FD-7DB05D472FA4}"
55 | EndProject
56 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shipping.ViewModelComposition", "Shipping.ViewModelComposition\Shipping.ViewModelComposition.csproj", "{6C0D692C-4CD3-4C3F-83FF-476F59CCEE17}"
57 | EndProject
58 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shipping.Data", "Shipping.Data\Shipping.Data.csproj", "{94A40C0D-51B6-4CA7-842D-52136B21C4F1}"
59 | EndProject
60 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shipping.Messages.Commands", "Shipping.Messages.Commands\Shipping.Messages.Commands.csproj", "{3EE401ED-9AE7-4AB5-B017-9CBAE39B1C1C}"
61 | EndProject
62 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreateRequiredDatabases", "CreateRequiredDatabases\CreateRequiredDatabases.csproj", "{0C31BB6B-27BD-4081-AB3F-3096E5CFFAD0}"
63 | EndProject
64 | Global
65 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
66 | Debug|Any CPU = Debug|Any CPU
67 | Release|Any CPU = Release|Any CPU
68 | EndGlobalSection
69 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
70 | {4882F331-74B9-43AA-89E5-E87E240AD0FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
71 | {4882F331-74B9-43AA-89E5-E87E240AD0FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
72 | {4882F331-74B9-43AA-89E5-E87E240AD0FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
73 | {4882F331-74B9-43AA-89E5-E87E240AD0FD}.Release|Any CPU.Build.0 = Release|Any CPU
74 | {4A60D764-E055-410B-8C64-5359031D433D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
75 | {4A60D764-E055-410B-8C64-5359031D433D}.Debug|Any CPU.Build.0 = Debug|Any CPU
76 | {4A60D764-E055-410B-8C64-5359031D433D}.Release|Any CPU.ActiveCfg = Release|Any CPU
77 | {4A60D764-E055-410B-8C64-5359031D433D}.Release|Any CPU.Build.0 = Release|Any CPU
78 | {6EF30F98-537C-47BD-979A-82156C1BB5A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
79 | {6EF30F98-537C-47BD-979A-82156C1BB5A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
80 | {6EF30F98-537C-47BD-979A-82156C1BB5A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
81 | {6EF30F98-537C-47BD-979A-82156C1BB5A5}.Release|Any CPU.Build.0 = Release|Any CPU
82 | {5AC969AF-777E-478B-85B9-66634B8125E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
83 | {5AC969AF-777E-478B-85B9-66634B8125E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
84 | {5AC969AF-777E-478B-85B9-66634B8125E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
85 | {5AC969AF-777E-478B-85B9-66634B8125E2}.Release|Any CPU.Build.0 = Release|Any CPU
86 | {DDE1A4D9-C920-4E6F-BB5D-61EFA8BB7C44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
87 | {DDE1A4D9-C920-4E6F-BB5D-61EFA8BB7C44}.Debug|Any CPU.Build.0 = Debug|Any CPU
88 | {DDE1A4D9-C920-4E6F-BB5D-61EFA8BB7C44}.Release|Any CPU.ActiveCfg = Release|Any CPU
89 | {DDE1A4D9-C920-4E6F-BB5D-61EFA8BB7C44}.Release|Any CPU.Build.0 = Release|Any CPU
90 | {A0B0540B-4B1C-4307-B094-8AEF0B62F514}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
91 | {A0B0540B-4B1C-4307-B094-8AEF0B62F514}.Debug|Any CPU.Build.0 = Debug|Any CPU
92 | {A0B0540B-4B1C-4307-B094-8AEF0B62F514}.Release|Any CPU.ActiveCfg = Release|Any CPU
93 | {A0B0540B-4B1C-4307-B094-8AEF0B62F514}.Release|Any CPU.Build.0 = Release|Any CPU
94 | {B2638542-666E-4A9D-AAD4-CAC77D948871}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
95 | {B2638542-666E-4A9D-AAD4-CAC77D948871}.Debug|Any CPU.Build.0 = Debug|Any CPU
96 | {B2638542-666E-4A9D-AAD4-CAC77D948871}.Release|Any CPU.ActiveCfg = Release|Any CPU
97 | {B2638542-666E-4A9D-AAD4-CAC77D948871}.Release|Any CPU.Build.0 = Release|Any CPU
98 | {A0B7A7DB-5474-412F-A4E0-4010132F2CE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
99 | {A0B7A7DB-5474-412F-A4E0-4010132F2CE9}.Debug|Any CPU.Build.0 = Debug|Any CPU
100 | {A0B7A7DB-5474-412F-A4E0-4010132F2CE9}.Release|Any CPU.ActiveCfg = Release|Any CPU
101 | {A0B7A7DB-5474-412F-A4E0-4010132F2CE9}.Release|Any CPU.Build.0 = Release|Any CPU
102 | {FDA6D9B8-9373-4E3A-9F39-3C97BA421470}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
103 | {FDA6D9B8-9373-4E3A-9F39-3C97BA421470}.Debug|Any CPU.Build.0 = Debug|Any CPU
104 | {FDA6D9B8-9373-4E3A-9F39-3C97BA421470}.Release|Any CPU.ActiveCfg = Release|Any CPU
105 | {FDA6D9B8-9373-4E3A-9F39-3C97BA421470}.Release|Any CPU.Build.0 = Release|Any CPU
106 | {7D172217-98C8-4311-8318-6900F6F8D50D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
107 | {7D172217-98C8-4311-8318-6900F6F8D50D}.Debug|Any CPU.Build.0 = Debug|Any CPU
108 | {7D172217-98C8-4311-8318-6900F6F8D50D}.Release|Any CPU.ActiveCfg = Release|Any CPU
109 | {7D172217-98C8-4311-8318-6900F6F8D50D}.Release|Any CPU.Build.0 = Release|Any CPU
110 | {02D3071F-EF79-4FD9-9752-4B041099C459}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
111 | {02D3071F-EF79-4FD9-9752-4B041099C459}.Debug|Any CPU.Build.0 = Debug|Any CPU
112 | {02D3071F-EF79-4FD9-9752-4B041099C459}.Release|Any CPU.ActiveCfg = Release|Any CPU
113 | {02D3071F-EF79-4FD9-9752-4B041099C459}.Release|Any CPU.Build.0 = Release|Any CPU
114 | {D80CBA22-4E4A-49E5-B875-61558962FD53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
115 | {D80CBA22-4E4A-49E5-B875-61558962FD53}.Debug|Any CPU.Build.0 = Debug|Any CPU
116 | {D80CBA22-4E4A-49E5-B875-61558962FD53}.Release|Any CPU.ActiveCfg = Release|Any CPU
117 | {D80CBA22-4E4A-49E5-B875-61558962FD53}.Release|Any CPU.Build.0 = Release|Any CPU
118 | {03B6E372-5BC9-488B-91F9-C01A7BCAF07A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
119 | {03B6E372-5BC9-488B-91F9-C01A7BCAF07A}.Debug|Any CPU.Build.0 = Debug|Any CPU
120 | {03B6E372-5BC9-488B-91F9-C01A7BCAF07A}.Release|Any CPU.ActiveCfg = Release|Any CPU
121 | {03B6E372-5BC9-488B-91F9-C01A7BCAF07A}.Release|Any CPU.Build.0 = Release|Any CPU
122 | {6A46B867-16ED-4700-AC6E-FBB6FC49B280}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
123 | {6A46B867-16ED-4700-AC6E-FBB6FC49B280}.Debug|Any CPU.Build.0 = Debug|Any CPU
124 | {6A46B867-16ED-4700-AC6E-FBB6FC49B280}.Release|Any CPU.ActiveCfg = Release|Any CPU
125 | {6A46B867-16ED-4700-AC6E-FBB6FC49B280}.Release|Any CPU.Build.0 = Release|Any CPU
126 | {0C6DDAD3-BD78-49AF-AFD3-17FFC8846167}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
127 | {0C6DDAD3-BD78-49AF-AFD3-17FFC8846167}.Debug|Any CPU.Build.0 = Debug|Any CPU
128 | {0C6DDAD3-BD78-49AF-AFD3-17FFC8846167}.Release|Any CPU.ActiveCfg = Release|Any CPU
129 | {0C6DDAD3-BD78-49AF-AFD3-17FFC8846167}.Release|Any CPU.Build.0 = Release|Any CPU
130 | {C88C6770-CFF4-48E7-A313-6905A6C14AC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
131 | {C88C6770-CFF4-48E7-A313-6905A6C14AC2}.Debug|Any CPU.Build.0 = Debug|Any CPU
132 | {C88C6770-CFF4-48E7-A313-6905A6C14AC2}.Release|Any CPU.ActiveCfg = Release|Any CPU
133 | {C88C6770-CFF4-48E7-A313-6905A6C14AC2}.Release|Any CPU.Build.0 = Release|Any CPU
134 | {FB207873-8FE2-42F9-8AEE-A1DCD3E48035}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
135 | {FB207873-8FE2-42F9-8AEE-A1DCD3E48035}.Debug|Any CPU.Build.0 = Debug|Any CPU
136 | {FB207873-8FE2-42F9-8AEE-A1DCD3E48035}.Release|Any CPU.ActiveCfg = Release|Any CPU
137 | {FB207873-8FE2-42F9-8AEE-A1DCD3E48035}.Release|Any CPU.Build.0 = Release|Any CPU
138 | {165570E1-C782-47F4-AA6D-00546682AC13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
139 | {165570E1-C782-47F4-AA6D-00546682AC13}.Debug|Any CPU.Build.0 = Debug|Any CPU
140 | {165570E1-C782-47F4-AA6D-00546682AC13}.Release|Any CPU.ActiveCfg = Release|Any CPU
141 | {165570E1-C782-47F4-AA6D-00546682AC13}.Release|Any CPU.Build.0 = Release|Any CPU
142 | {5F07AED9-3A33-4A54-80D1-A0164C886EF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
143 | {5F07AED9-3A33-4A54-80D1-A0164C886EF8}.Debug|Any CPU.Build.0 = Debug|Any CPU
144 | {5F07AED9-3A33-4A54-80D1-A0164C886EF8}.Release|Any CPU.ActiveCfg = Release|Any CPU
145 | {5F07AED9-3A33-4A54-80D1-A0164C886EF8}.Release|Any CPU.Build.0 = Release|Any CPU
146 | {47A17268-A0B1-48C9-93FD-7DB05D472FA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
147 | {47A17268-A0B1-48C9-93FD-7DB05D472FA4}.Debug|Any CPU.Build.0 = Debug|Any CPU
148 | {47A17268-A0B1-48C9-93FD-7DB05D472FA4}.Release|Any CPU.ActiveCfg = Release|Any CPU
149 | {47A17268-A0B1-48C9-93FD-7DB05D472FA4}.Release|Any CPU.Build.0 = Release|Any CPU
150 | {6C0D692C-4CD3-4C3F-83FF-476F59CCEE17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
151 | {6C0D692C-4CD3-4C3F-83FF-476F59CCEE17}.Debug|Any CPU.Build.0 = Debug|Any CPU
152 | {6C0D692C-4CD3-4C3F-83FF-476F59CCEE17}.Release|Any CPU.ActiveCfg = Release|Any CPU
153 | {6C0D692C-4CD3-4C3F-83FF-476F59CCEE17}.Release|Any CPU.Build.0 = Release|Any CPU
154 | {94A40C0D-51B6-4CA7-842D-52136B21C4F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
155 | {94A40C0D-51B6-4CA7-842D-52136B21C4F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
156 | {94A40C0D-51B6-4CA7-842D-52136B21C4F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
157 | {94A40C0D-51B6-4CA7-842D-52136B21C4F1}.Release|Any CPU.Build.0 = Release|Any CPU
158 | {3EE401ED-9AE7-4AB5-B017-9CBAE39B1C1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
159 | {3EE401ED-9AE7-4AB5-B017-9CBAE39B1C1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
160 | {3EE401ED-9AE7-4AB5-B017-9CBAE39B1C1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
161 | {3EE401ED-9AE7-4AB5-B017-9CBAE39B1C1C}.Release|Any CPU.Build.0 = Release|Any CPU
162 | {0C31BB6B-27BD-4081-AB3F-3096E5CFFAD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
163 | {0C31BB6B-27BD-4081-AB3F-3096E5CFFAD0}.Debug|Any CPU.Build.0 = Debug|Any CPU
164 | {0C31BB6B-27BD-4081-AB3F-3096E5CFFAD0}.Release|Any CPU.ActiveCfg = Release|Any CPU
165 | {0C31BB6B-27BD-4081-AB3F-3096E5CFFAD0}.Release|Any CPU.Build.0 = Release|Any CPU
166 | EndGlobalSection
167 | GlobalSection(SolutionProperties) = preSolution
168 | HideSolutionNode = FALSE
169 | EndGlobalSection
170 | GlobalSection(NestedProjects) = preSolution
171 | {4882F331-74B9-43AA-89E5-E87E240AD0FD} = {900D7EA7-8C57-4CF7-94B5-1D8625367EC9}
172 | {6EF30F98-537C-47BD-979A-82156C1BB5A5} = {82F40981-48CC-4B75-9738-B394AB990E7A}
173 | {5AC969AF-777E-478B-85B9-66634B8125E2} = {1D1589DF-CEB2-4B1C-B8BF-A86CD7A7B4A6}
174 | {DDE1A4D9-C920-4E6F-BB5D-61EFA8BB7C44} = {82F40981-48CC-4B75-9738-B394AB990E7A}
175 | {A0B0540B-4B1C-4307-B094-8AEF0B62F514} = {82F40981-48CC-4B75-9738-B394AB990E7A}
176 | {B2638542-666E-4A9D-AAD4-CAC77D948871} = {1D1589DF-CEB2-4B1C-B8BF-A86CD7A7B4A6}
177 | {A0B7A7DB-5474-412F-A4E0-4010132F2CE9} = {32C8E39D-93AC-4AF9-981B-152478ADFCA5}
178 | {FDA6D9B8-9373-4E3A-9F39-3C97BA421470} = {32C8E39D-93AC-4AF9-981B-152478ADFCA5}
179 | {7D172217-98C8-4311-8318-6900F6F8D50D} = {32C8E39D-93AC-4AF9-981B-152478ADFCA5}
180 | {02D3071F-EF79-4FD9-9752-4B041099C459} = {32C8E39D-93AC-4AF9-981B-152478ADFCA5}
181 | {D80CBA22-4E4A-49E5-B875-61558962FD53} = {1D1589DF-CEB2-4B1C-B8BF-A86CD7A7B4A6}
182 | {03B6E372-5BC9-488B-91F9-C01A7BCAF07A} = {32C8E39D-93AC-4AF9-981B-152478ADFCA5}
183 | {6A46B867-16ED-4700-AC6E-FBB6FC49B280} = {1D1589DF-CEB2-4B1C-B8BF-A86CD7A7B4A6}
184 | {0C6DDAD3-BD78-49AF-AFD3-17FFC8846167} = {1D1589DF-CEB2-4B1C-B8BF-A86CD7A7B4A6}
185 | {C88C6770-CFF4-48E7-A313-6905A6C14AC2} = {32C8E39D-93AC-4AF9-981B-152478ADFCA5}
186 | {FB207873-8FE2-42F9-8AEE-A1DCD3E48035} = {1D1589DF-CEB2-4B1C-B8BF-A86CD7A7B4A6}
187 | {165570E1-C782-47F4-AA6D-00546682AC13} = {1D1589DF-CEB2-4B1C-B8BF-A86CD7A7B4A6}
188 | {5F07AED9-3A33-4A54-80D1-A0164C886EF8} = {1EFDBC80-D09C-4B00-9984-6E21E6422D74}
189 | {47A17268-A0B1-48C9-93FD-7DB05D472FA4} = {1EFDBC80-D09C-4B00-9984-6E21E6422D74}
190 | {6C0D692C-4CD3-4C3F-83FF-476F59CCEE17} = {1EFDBC80-D09C-4B00-9984-6E21E6422D74}
191 | {94A40C0D-51B6-4CA7-842D-52136B21C4F1} = {1EFDBC80-D09C-4B00-9984-6E21E6422D74}
192 | {3EE401ED-9AE7-4AB5-B017-9CBAE39B1C1C} = {1EFDBC80-D09C-4B00-9984-6E21E6422D74}
193 | EndGlobalSection
194 | GlobalSection(ExtensibilityGlobals) = postSolution
195 | SolutionGuid = {1D0078F3-1E11-49D2-AE2C-6EA7D61E1B06}
196 | EndGlobalSection
197 | EndGlobal
198 |
--------------------------------------------------------------------------------