diff --git a/EventSourcing.Demo.csproj b/EventSourcing.Demo.csproj new file mode 100644 index 0000000..8d29ea7 --- /dev/null +++ b/EventSourcing.Demo.csproj @@ -0,0 +1,12 @@ + + + + Exe + net5.0 + + + + + + + diff --git a/EventSourcing.Demo.csproj b/EventSourcing.Demo.csproj new file mode 100644 index 0000000..8d29ea7 --- /dev/null +++ b/EventSourcing.Demo.csproj @@ -0,0 +1,12 @@ + + + + Exe + net5.0 + + + + + + + diff --git a/Events.cs b/Events.cs new file mode 100644 index 0000000..6184ee1 --- /dev/null +++ b/Events.cs @@ -0,0 +1,12 @@ +using System; + +namespace EventSourcing.Demo +{ + public interface IEvent {} + + public record ProductShipped(string Sku, int Quantity, DateTime DateTime) : IEvent; + + public record ProductReceived(string Sku, int Quantity, DateTime DateTime) : IEvent; + + public record InventoryAdjusted(string Sku, int Quantity, string Reason, DateTime DateTime) : IEvent; +} \ No newline at end of file diff --git a/EventSourcing.Demo.csproj b/EventSourcing.Demo.csproj new file mode 100644 index 0000000..8d29ea7 --- /dev/null +++ b/EventSourcing.Demo.csproj @@ -0,0 +1,12 @@ + + + + Exe + net5.0 + + + + + + + diff --git a/Events.cs b/Events.cs new file mode 100644 index 0000000..6184ee1 --- /dev/null +++ b/Events.cs @@ -0,0 +1,12 @@ +using System; + +namespace EventSourcing.Demo +{ + public interface IEvent {} + + public record ProductShipped(string Sku, int Quantity, DateTime DateTime) : IEvent; + + public record ProductReceived(string Sku, int Quantity, DateTime DateTime) : IEvent; + + public record InventoryAdjusted(string Sku, int Quantity, string Reason, DateTime DateTime) : IEvent; +} \ No newline at end of file diff --git a/ProductDbContext.cs b/ProductDbContext.cs new file mode 100644 index 0000000..2d4c360 --- /dev/null +++ b/ProductDbContext.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; + +namespace EventSourcing.Demo +{ + public class Product + { + public string Sku { get; set; } + public int Received { get; set; } + public int Shipped { get; set; } + } + + public class ProductDbContext : DbContext + { + public DbSet Products { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(x => x.Sku); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseInMemoryDatabase("Demo"); + } + } +} \ No newline at end of file diff --git a/EventSourcing.Demo.csproj b/EventSourcing.Demo.csproj new file mode 100644 index 0000000..8d29ea7 --- /dev/null +++ b/EventSourcing.Demo.csproj @@ -0,0 +1,12 @@ + + + + Exe + net5.0 + + + + + + + diff --git a/Events.cs b/Events.cs new file mode 100644 index 0000000..6184ee1 --- /dev/null +++ b/Events.cs @@ -0,0 +1,12 @@ +using System; + +namespace EventSourcing.Demo +{ + public interface IEvent {} + + public record ProductShipped(string Sku, int Quantity, DateTime DateTime) : IEvent; + + public record ProductReceived(string Sku, int Quantity, DateTime DateTime) : IEvent; + + public record InventoryAdjusted(string Sku, int Quantity, string Reason, DateTime DateTime) : IEvent; +} \ No newline at end of file diff --git a/ProductDbContext.cs b/ProductDbContext.cs new file mode 100644 index 0000000..2d4c360 --- /dev/null +++ b/ProductDbContext.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; + +namespace EventSourcing.Demo +{ + public class Product + { + public string Sku { get; set; } + public int Received { get; set; } + public int Shipped { get; set; } + } + + public class ProductDbContext : DbContext + { + public DbSet Products { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(x => x.Sku); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseInMemoryDatabase("Demo"); + } + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..a356ae7 --- /dev/null +++ b/Program.cs @@ -0,0 +1,122 @@ +using System; + +namespace EventSourcing.Demo +{ + class Program + { + static void Main() + { + var warehouseProductRepository = new WarehouseProductRepository(); + + var projection = new Projection(new ProductDbContext()); + warehouseProductRepository.Subscribe(projection.ReceiveEvent); + + var key = string.Empty; + while (key != "X") + { + Console.WriteLine("R: Receive Inventory"); + Console.WriteLine("S: Ship Inventory"); + Console.WriteLine("A: Inventory Adjustment"); + Console.WriteLine("Q: Quantity On Hand"); + Console.WriteLine("E: Events"); + Console.WriteLine("P: Projection"); + Console.Write("> "); + key = Console.ReadLine()?.ToUpperInvariant(); + Console.WriteLine(); + + var sku = GetSkuFromConsole(); + var warehouseProduct = warehouseProductRepository.Get(sku); + + switch (key) + { + case "R": + var receiveInput = GetQuantity(); + if (receiveInput.IsValid) + { + warehouseProduct.ReceiveProduct(receiveInput.Quantity); + Console.WriteLine($"{sku} Received: {receiveInput.Quantity}"); + } + break; + case "S": + var shipInput = GetQuantity(); + if (shipInput.IsValid) + { + warehouseProduct.ShipProduct(shipInput.Quantity); + Console.WriteLine($"{sku} Shipped: {shipInput.Quantity}"); + } + break; + case "A": + var adjustmentInput = GetQuantity(); + if (adjustmentInput.IsValid) + { + var reason = GetAdjustmentReason(); + warehouseProduct.AdjustInventory(adjustmentInput.Quantity, reason); + Console.WriteLine($"{sku} Adjusted: {adjustmentInput.Quantity} {reason}"); + } + break; + case "Q": + var currentQuantityOnHand = warehouseProduct.GetQuantityOnHand(); + Console.WriteLine($"{sku} Quantity On Hand: {currentQuantityOnHand}"); + break; + case "E": + Console.WriteLine($"Events: {sku}"); + foreach (var evnt in warehouseProduct.GetAllEvents()) + { + switch (evnt) + { + case ProductShipped shipProduct: + Console.WriteLine($"{shipProduct.DateTime:u} {sku} Shipped: {shipProduct.Quantity}"); + break; + case ProductReceived receiveProduct: + Console.WriteLine($"{receiveProduct.DateTime:u} {sku} Received: {receiveProduct.Quantity}"); + break; + case InventoryAdjusted inventoryAdjusted: + Console.WriteLine($"{inventoryAdjusted.DateTime:u} {sku} Adjusted: {inventoryAdjusted.Quantity} {inventoryAdjusted.Reason}"); + break; + } + + } + break; + case "P": + Console.WriteLine($"Projection: {sku}"); + var productProjection = projection.GetProduct(sku); + Console.WriteLine($"{sku} Received: {productProjection.Received}"); + Console.WriteLine($"{sku} Shipped: {productProjection.Shipped}"); + break; + } + + warehouseProductRepository.Save(warehouseProduct); + + Console.ReadLine(); + Console.WriteLine(); + } + } + + + private static string GetSkuFromConsole() + { + Console.Write("SKU: "); + return Console.ReadLine(); + } + + private static string GetAdjustmentReason() + { + Console.Write("Reason: "); + return Console.ReadLine(); + } + + private static (int Quantity, bool IsValid) GetQuantity() + { + Console.Write("Quantity: "); + if (int.TryParse(Console.ReadLine(), out var quantity)) + { + return (quantity, true); + } + else + { + Console.WriteLine("Invalid Quantity."); + return (0, false); + } + } + } +} diff --git a/EventSourcing.Demo.csproj b/EventSourcing.Demo.csproj new file mode 100644 index 0000000..8d29ea7 --- /dev/null +++ b/EventSourcing.Demo.csproj @@ -0,0 +1,12 @@ + + + + Exe + net5.0 + + + + + + + diff --git a/Events.cs b/Events.cs new file mode 100644 index 0000000..6184ee1 --- /dev/null +++ b/Events.cs @@ -0,0 +1,12 @@ +using System; + +namespace EventSourcing.Demo +{ + public interface IEvent {} + + public record ProductShipped(string Sku, int Quantity, DateTime DateTime) : IEvent; + + public record ProductReceived(string Sku, int Quantity, DateTime DateTime) : IEvent; + + public record InventoryAdjusted(string Sku, int Quantity, string Reason, DateTime DateTime) : IEvent; +} \ No newline at end of file diff --git a/ProductDbContext.cs b/ProductDbContext.cs new file mode 100644 index 0000000..2d4c360 --- /dev/null +++ b/ProductDbContext.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; + +namespace EventSourcing.Demo +{ + public class Product + { + public string Sku { get; set; } + public int Received { get; set; } + public int Shipped { get; set; } + } + + public class ProductDbContext : DbContext + { + public DbSet Products { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(x => x.Sku); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseInMemoryDatabase("Demo"); + } + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..a356ae7 --- /dev/null +++ b/Program.cs @@ -0,0 +1,122 @@ +using System; + +namespace EventSourcing.Demo +{ + class Program + { + static void Main() + { + var warehouseProductRepository = new WarehouseProductRepository(); + + var projection = new Projection(new ProductDbContext()); + warehouseProductRepository.Subscribe(projection.ReceiveEvent); + + var key = string.Empty; + while (key != "X") + { + Console.WriteLine("R: Receive Inventory"); + Console.WriteLine("S: Ship Inventory"); + Console.WriteLine("A: Inventory Adjustment"); + Console.WriteLine("Q: Quantity On Hand"); + Console.WriteLine("E: Events"); + Console.WriteLine("P: Projection"); + Console.Write("> "); + key = Console.ReadLine()?.ToUpperInvariant(); + Console.WriteLine(); + + var sku = GetSkuFromConsole(); + var warehouseProduct = warehouseProductRepository.Get(sku); + + switch (key) + { + case "R": + var receiveInput = GetQuantity(); + if (receiveInput.IsValid) + { + warehouseProduct.ReceiveProduct(receiveInput.Quantity); + Console.WriteLine($"{sku} Received: {receiveInput.Quantity}"); + } + break; + case "S": + var shipInput = GetQuantity(); + if (shipInput.IsValid) + { + warehouseProduct.ShipProduct(shipInput.Quantity); + Console.WriteLine($"{sku} Shipped: {shipInput.Quantity}"); + } + break; + case "A": + var adjustmentInput = GetQuantity(); + if (adjustmentInput.IsValid) + { + var reason = GetAdjustmentReason(); + warehouseProduct.AdjustInventory(adjustmentInput.Quantity, reason); + Console.WriteLine($"{sku} Adjusted: {adjustmentInput.Quantity} {reason}"); + } + break; + case "Q": + var currentQuantityOnHand = warehouseProduct.GetQuantityOnHand(); + Console.WriteLine($"{sku} Quantity On Hand: {currentQuantityOnHand}"); + break; + case "E": + Console.WriteLine($"Events: {sku}"); + foreach (var evnt in warehouseProduct.GetAllEvents()) + { + switch (evnt) + { + case ProductShipped shipProduct: + Console.WriteLine($"{shipProduct.DateTime:u} {sku} Shipped: {shipProduct.Quantity}"); + break; + case ProductReceived receiveProduct: + Console.WriteLine($"{receiveProduct.DateTime:u} {sku} Received: {receiveProduct.Quantity}"); + break; + case InventoryAdjusted inventoryAdjusted: + Console.WriteLine($"{inventoryAdjusted.DateTime:u} {sku} Adjusted: {inventoryAdjusted.Quantity} {inventoryAdjusted.Reason}"); + break; + } + + } + break; + case "P": + Console.WriteLine($"Projection: {sku}"); + var productProjection = projection.GetProduct(sku); + Console.WriteLine($"{sku} Received: {productProjection.Received}"); + Console.WriteLine($"{sku} Shipped: {productProjection.Shipped}"); + break; + } + + warehouseProductRepository.Save(warehouseProduct); + + Console.ReadLine(); + Console.WriteLine(); + } + } + + + private static string GetSkuFromConsole() + { + Console.Write("SKU: "); + return Console.ReadLine(); + } + + private static string GetAdjustmentReason() + { + Console.Write("Reason: "); + return Console.ReadLine(); + } + + private static (int Quantity, bool IsValid) GetQuantity() + { + Console.Write("Quantity: "); + if (int.TryParse(Console.ReadLine(), out var quantity)) + { + return (quantity, true); + } + else + { + Console.WriteLine("Invalid Quantity."); + return (0, false); + } + } + } +} diff --git a/Projection.cs b/Projection.cs new file mode 100644 index 0000000..77213e8 --- /dev/null +++ b/Projection.cs @@ -0,0 +1,56 @@ +using System.Linq; + +namespace EventSourcing.Demo +{ + public class Projection + { + private readonly ProductDbContext _dbContext; + + public Projection(ProductDbContext dbContext) + { + _dbContext = dbContext; + } + + public void ReceiveEvent(IEvent evnt) + { + switch (evnt) + { + case ProductShipped shipProduct: + Apply(shipProduct); + break; + case ProductReceived receiveProduct: + Apply(receiveProduct); + break; + } + } + + public Product GetProduct(string sku) + { + var product = _dbContext.Products.SingleOrDefault(x => x.Sku == sku); + if (product == null) + { + product = new Product + { + Sku = sku + }; + _dbContext.Products.Add(product); + } + + return product; + } + + private void Apply(ProductShipped shipProduct) + { + var product = GetProduct(shipProduct.Sku); + product.Shipped += shipProduct.Quantity; + _dbContext.SaveChanges(); + } + + private void Apply(ProductReceived productReceived) + { + var state = GetProduct(productReceived.Sku); + state.Received += productReceived.Quantity; + _dbContext.SaveChanges(); + } + } +} \ No newline at end of file diff --git a/EventSourcing.Demo.csproj b/EventSourcing.Demo.csproj new file mode 100644 index 0000000..8d29ea7 --- /dev/null +++ b/EventSourcing.Demo.csproj @@ -0,0 +1,12 @@ + + + + Exe + net5.0 + + + + + + + diff --git a/Events.cs b/Events.cs new file mode 100644 index 0000000..6184ee1 --- /dev/null +++ b/Events.cs @@ -0,0 +1,12 @@ +using System; + +namespace EventSourcing.Demo +{ + public interface IEvent {} + + public record ProductShipped(string Sku, int Quantity, DateTime DateTime) : IEvent; + + public record ProductReceived(string Sku, int Quantity, DateTime DateTime) : IEvent; + + public record InventoryAdjusted(string Sku, int Quantity, string Reason, DateTime DateTime) : IEvent; +} \ No newline at end of file diff --git a/ProductDbContext.cs b/ProductDbContext.cs new file mode 100644 index 0000000..2d4c360 --- /dev/null +++ b/ProductDbContext.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; + +namespace EventSourcing.Demo +{ + public class Product + { + public string Sku { get; set; } + public int Received { get; set; } + public int Shipped { get; set; } + } + + public class ProductDbContext : DbContext + { + public DbSet Products { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(x => x.Sku); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseInMemoryDatabase("Demo"); + } + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..a356ae7 --- /dev/null +++ b/Program.cs @@ -0,0 +1,122 @@ +using System; + +namespace EventSourcing.Demo +{ + class Program + { + static void Main() + { + var warehouseProductRepository = new WarehouseProductRepository(); + + var projection = new Projection(new ProductDbContext()); + warehouseProductRepository.Subscribe(projection.ReceiveEvent); + + var key = string.Empty; + while (key != "X") + { + Console.WriteLine("R: Receive Inventory"); + Console.WriteLine("S: Ship Inventory"); + Console.WriteLine("A: Inventory Adjustment"); + Console.WriteLine("Q: Quantity On Hand"); + Console.WriteLine("E: Events"); + Console.WriteLine("P: Projection"); + Console.Write("> "); + key = Console.ReadLine()?.ToUpperInvariant(); + Console.WriteLine(); + + var sku = GetSkuFromConsole(); + var warehouseProduct = warehouseProductRepository.Get(sku); + + switch (key) + { + case "R": + var receiveInput = GetQuantity(); + if (receiveInput.IsValid) + { + warehouseProduct.ReceiveProduct(receiveInput.Quantity); + Console.WriteLine($"{sku} Received: {receiveInput.Quantity}"); + } + break; + case "S": + var shipInput = GetQuantity(); + if (shipInput.IsValid) + { + warehouseProduct.ShipProduct(shipInput.Quantity); + Console.WriteLine($"{sku} Shipped: {shipInput.Quantity}"); + } + break; + case "A": + var adjustmentInput = GetQuantity(); + if (adjustmentInput.IsValid) + { + var reason = GetAdjustmentReason(); + warehouseProduct.AdjustInventory(adjustmentInput.Quantity, reason); + Console.WriteLine($"{sku} Adjusted: {adjustmentInput.Quantity} {reason}"); + } + break; + case "Q": + var currentQuantityOnHand = warehouseProduct.GetQuantityOnHand(); + Console.WriteLine($"{sku} Quantity On Hand: {currentQuantityOnHand}"); + break; + case "E": + Console.WriteLine($"Events: {sku}"); + foreach (var evnt in warehouseProduct.GetAllEvents()) + { + switch (evnt) + { + case ProductShipped shipProduct: + Console.WriteLine($"{shipProduct.DateTime:u} {sku} Shipped: {shipProduct.Quantity}"); + break; + case ProductReceived receiveProduct: + Console.WriteLine($"{receiveProduct.DateTime:u} {sku} Received: {receiveProduct.Quantity}"); + break; + case InventoryAdjusted inventoryAdjusted: + Console.WriteLine($"{inventoryAdjusted.DateTime:u} {sku} Adjusted: {inventoryAdjusted.Quantity} {inventoryAdjusted.Reason}"); + break; + } + + } + break; + case "P": + Console.WriteLine($"Projection: {sku}"); + var productProjection = projection.GetProduct(sku); + Console.WriteLine($"{sku} Received: {productProjection.Received}"); + Console.WriteLine($"{sku} Shipped: {productProjection.Shipped}"); + break; + } + + warehouseProductRepository.Save(warehouseProduct); + + Console.ReadLine(); + Console.WriteLine(); + } + } + + + private static string GetSkuFromConsole() + { + Console.Write("SKU: "); + return Console.ReadLine(); + } + + private static string GetAdjustmentReason() + { + Console.Write("Reason: "); + return Console.ReadLine(); + } + + private static (int Quantity, bool IsValid) GetQuantity() + { + Console.Write("Quantity: "); + if (int.TryParse(Console.ReadLine(), out var quantity)) + { + return (quantity, true); + } + else + { + Console.WriteLine("Invalid Quantity."); + return (0, false); + } + } + } +} diff --git a/Projection.cs b/Projection.cs new file mode 100644 index 0000000..77213e8 --- /dev/null +++ b/Projection.cs @@ -0,0 +1,56 @@ +using System.Linq; + +namespace EventSourcing.Demo +{ + public class Projection + { + private readonly ProductDbContext _dbContext; + + public Projection(ProductDbContext dbContext) + { + _dbContext = dbContext; + } + + public void ReceiveEvent(IEvent evnt) + { + switch (evnt) + { + case ProductShipped shipProduct: + Apply(shipProduct); + break; + case ProductReceived receiveProduct: + Apply(receiveProduct); + break; + } + } + + public Product GetProduct(string sku) + { + var product = _dbContext.Products.SingleOrDefault(x => x.Sku == sku); + if (product == null) + { + product = new Product + { + Sku = sku + }; + _dbContext.Products.Add(product); + } + + return product; + } + + private void Apply(ProductShipped shipProduct) + { + var product = GetProduct(shipProduct.Sku); + product.Shipped += shipProduct.Quantity; + _dbContext.SaveChanges(); + } + + private void Apply(ProductReceived productReceived) + { + var state = GetProduct(productReceived.Sku); + state.Received += productReceived.Quantity; + _dbContext.SaveChanges(); + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index 86387d9..ec2136a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,7 @@ -EventSourcingProjection -=============== +# Event Sourcing: Projection + +## YouTube +https://www.youtube.com/channel/UC3RKA4vunFAfrfxiJhPEplw + +## Prerequisite +None \ No newline at end of file diff --git a/EventSourcing.Demo.csproj b/EventSourcing.Demo.csproj new file mode 100644 index 0000000..8d29ea7 --- /dev/null +++ b/EventSourcing.Demo.csproj @@ -0,0 +1,12 @@ + + + + Exe + net5.0 + + + + + + + diff --git a/Events.cs b/Events.cs new file mode 100644 index 0000000..6184ee1 --- /dev/null +++ b/Events.cs @@ -0,0 +1,12 @@ +using System; + +namespace EventSourcing.Demo +{ + public interface IEvent {} + + public record ProductShipped(string Sku, int Quantity, DateTime DateTime) : IEvent; + + public record ProductReceived(string Sku, int Quantity, DateTime DateTime) : IEvent; + + public record InventoryAdjusted(string Sku, int Quantity, string Reason, DateTime DateTime) : IEvent; +} \ No newline at end of file diff --git a/ProductDbContext.cs b/ProductDbContext.cs new file mode 100644 index 0000000..2d4c360 --- /dev/null +++ b/ProductDbContext.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; + +namespace EventSourcing.Demo +{ + public class Product + { + public string Sku { get; set; } + public int Received { get; set; } + public int Shipped { get; set; } + } + + public class ProductDbContext : DbContext + { + public DbSet Products { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(x => x.Sku); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseInMemoryDatabase("Demo"); + } + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..a356ae7 --- /dev/null +++ b/Program.cs @@ -0,0 +1,122 @@ +using System; + +namespace EventSourcing.Demo +{ + class Program + { + static void Main() + { + var warehouseProductRepository = new WarehouseProductRepository(); + + var projection = new Projection(new ProductDbContext()); + warehouseProductRepository.Subscribe(projection.ReceiveEvent); + + var key = string.Empty; + while (key != "X") + { + Console.WriteLine("R: Receive Inventory"); + Console.WriteLine("S: Ship Inventory"); + Console.WriteLine("A: Inventory Adjustment"); + Console.WriteLine("Q: Quantity On Hand"); + Console.WriteLine("E: Events"); + Console.WriteLine("P: Projection"); + Console.Write("> "); + key = Console.ReadLine()?.ToUpperInvariant(); + Console.WriteLine(); + + var sku = GetSkuFromConsole(); + var warehouseProduct = warehouseProductRepository.Get(sku); + + switch (key) + { + case "R": + var receiveInput = GetQuantity(); + if (receiveInput.IsValid) + { + warehouseProduct.ReceiveProduct(receiveInput.Quantity); + Console.WriteLine($"{sku} Received: {receiveInput.Quantity}"); + } + break; + case "S": + var shipInput = GetQuantity(); + if (shipInput.IsValid) + { + warehouseProduct.ShipProduct(shipInput.Quantity); + Console.WriteLine($"{sku} Shipped: {shipInput.Quantity}"); + } + break; + case "A": + var adjustmentInput = GetQuantity(); + if (adjustmentInput.IsValid) + { + var reason = GetAdjustmentReason(); + warehouseProduct.AdjustInventory(adjustmentInput.Quantity, reason); + Console.WriteLine($"{sku} Adjusted: {adjustmentInput.Quantity} {reason}"); + } + break; + case "Q": + var currentQuantityOnHand = warehouseProduct.GetQuantityOnHand(); + Console.WriteLine($"{sku} Quantity On Hand: {currentQuantityOnHand}"); + break; + case "E": + Console.WriteLine($"Events: {sku}"); + foreach (var evnt in warehouseProduct.GetAllEvents()) + { + switch (evnt) + { + case ProductShipped shipProduct: + Console.WriteLine($"{shipProduct.DateTime:u} {sku} Shipped: {shipProduct.Quantity}"); + break; + case ProductReceived receiveProduct: + Console.WriteLine($"{receiveProduct.DateTime:u} {sku} Received: {receiveProduct.Quantity}"); + break; + case InventoryAdjusted inventoryAdjusted: + Console.WriteLine($"{inventoryAdjusted.DateTime:u} {sku} Adjusted: {inventoryAdjusted.Quantity} {inventoryAdjusted.Reason}"); + break; + } + + } + break; + case "P": + Console.WriteLine($"Projection: {sku}"); + var productProjection = projection.GetProduct(sku); + Console.WriteLine($"{sku} Received: {productProjection.Received}"); + Console.WriteLine($"{sku} Shipped: {productProjection.Shipped}"); + break; + } + + warehouseProductRepository.Save(warehouseProduct); + + Console.ReadLine(); + Console.WriteLine(); + } + } + + + private static string GetSkuFromConsole() + { + Console.Write("SKU: "); + return Console.ReadLine(); + } + + private static string GetAdjustmentReason() + { + Console.Write("Reason: "); + return Console.ReadLine(); + } + + private static (int Quantity, bool IsValid) GetQuantity() + { + Console.Write("Quantity: "); + if (int.TryParse(Console.ReadLine(), out var quantity)) + { + return (quantity, true); + } + else + { + Console.WriteLine("Invalid Quantity."); + return (0, false); + } + } + } +} diff --git a/Projection.cs b/Projection.cs new file mode 100644 index 0000000..77213e8 --- /dev/null +++ b/Projection.cs @@ -0,0 +1,56 @@ +using System.Linq; + +namespace EventSourcing.Demo +{ + public class Projection + { + private readonly ProductDbContext _dbContext; + + public Projection(ProductDbContext dbContext) + { + _dbContext = dbContext; + } + + public void ReceiveEvent(IEvent evnt) + { + switch (evnt) + { + case ProductShipped shipProduct: + Apply(shipProduct); + break; + case ProductReceived receiveProduct: + Apply(receiveProduct); + break; + } + } + + public Product GetProduct(string sku) + { + var product = _dbContext.Products.SingleOrDefault(x => x.Sku == sku); + if (product == null) + { + product = new Product + { + Sku = sku + }; + _dbContext.Products.Add(product); + } + + return product; + } + + private void Apply(ProductShipped shipProduct) + { + var product = GetProduct(shipProduct.Sku); + product.Shipped += shipProduct.Quantity; + _dbContext.SaveChanges(); + } + + private void Apply(ProductReceived productReceived) + { + var state = GetProduct(productReceived.Sku); + state.Received += productReceived.Quantity; + _dbContext.SaveChanges(); + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index 86387d9..ec2136a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,7 @@ -EventSourcingProjection -=============== +# Event Sourcing: Projection + +## YouTube +https://www.youtube.com/channel/UC3RKA4vunFAfrfxiJhPEplw + +## Prerequisite +None \ No newline at end of file diff --git a/WarehouseProduct.cs b/WarehouseProduct.cs new file mode 100644 index 0000000..fbd50be --- /dev/null +++ b/WarehouseProduct.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; + +namespace EventSourcing.Demo +{ + public class CurrentState + { + public int QuantityOnHand { get; set; } + } + + public class WarehouseProduct + { + public string Sku { get; } + private readonly IList _allEvents = new List(); + private readonly IList _uncommittedEvents = new List(); + + // Projection (Current State) + private readonly CurrentState _currentState = new(); + + public WarehouseProduct(string sku) + { + Sku = sku; + } + + public void ShipProduct(int quantity) + { + if (quantity > _currentState.QuantityOnHand) + { + throw new InvalidDomainException("Ah... we don't have enough product to ship?"); + } + + AddEvent(new ProductShipped(Sku, quantity, DateTime.UtcNow)); + } + + public void ReceiveProduct(int quantity) + { + AddEvent(new ProductReceived(Sku, quantity, DateTime.UtcNow)); + } + + public void AdjustInventory(int quantity, string reason) + { + if (_currentState.QuantityOnHand + quantity < 0) + { + throw new InvalidDomainException("Cannot adjust to a negative quantity on hand."); + } + + AddEvent(new InventoryAdjusted(Sku, quantity, reason, DateTime.UtcNow)); + } + + private void Apply(ProductShipped evnt) + { + _currentState.QuantityOnHand -= evnt.Quantity; + } + + private void Apply(ProductReceived evnt) + { + _currentState.QuantityOnHand += evnt.Quantity; + } + + private void Apply(InventoryAdjusted evnt) + { + _currentState.QuantityOnHand += evnt.Quantity; + } + + public void ApplyEvent(IEvent evnt) + { + switch (evnt) + { + case ProductShipped shipProduct: + Apply(shipProduct); + break; + case ProductReceived receiveProduct: + Apply(receiveProduct); + break; + case InventoryAdjusted inventoryAdjusted: + Apply(inventoryAdjusted); + break; + default: + throw new InvalidOperationException("Unsupported Event."); + } + + _allEvents.Add(evnt); + } + + public void AddEvent(IEvent evnt) + { + ApplyEvent(evnt); + _uncommittedEvents.Add(evnt); + } + + public IList GetUncommittedEvents() + { + return new List(_uncommittedEvents); + } + + public IList GetAllEvents() + { + return new List(_allEvents); + } + + public void EventsCommitted() + { + _uncommittedEvents.Clear(); + } + + public int GetQuantityOnHand() + { + return _currentState.QuantityOnHand; + } + } + + public class InvalidDomainException : Exception + { + public InvalidDomainException(string message) : base(message) + { + + } + } +} \ No newline at end of file diff --git a/EventSourcing.Demo.csproj b/EventSourcing.Demo.csproj new file mode 100644 index 0000000..8d29ea7 --- /dev/null +++ b/EventSourcing.Demo.csproj @@ -0,0 +1,12 @@ + + + + Exe + net5.0 + + + + + + + diff --git a/Events.cs b/Events.cs new file mode 100644 index 0000000..6184ee1 --- /dev/null +++ b/Events.cs @@ -0,0 +1,12 @@ +using System; + +namespace EventSourcing.Demo +{ + public interface IEvent {} + + public record ProductShipped(string Sku, int Quantity, DateTime DateTime) : IEvent; + + public record ProductReceived(string Sku, int Quantity, DateTime DateTime) : IEvent; + + public record InventoryAdjusted(string Sku, int Quantity, string Reason, DateTime DateTime) : IEvent; +} \ No newline at end of file diff --git a/ProductDbContext.cs b/ProductDbContext.cs new file mode 100644 index 0000000..2d4c360 --- /dev/null +++ b/ProductDbContext.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; + +namespace EventSourcing.Demo +{ + public class Product + { + public string Sku { get; set; } + public int Received { get; set; } + public int Shipped { get; set; } + } + + public class ProductDbContext : DbContext + { + public DbSet Products { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(x => x.Sku); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseInMemoryDatabase("Demo"); + } + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..a356ae7 --- /dev/null +++ b/Program.cs @@ -0,0 +1,122 @@ +using System; + +namespace EventSourcing.Demo +{ + class Program + { + static void Main() + { + var warehouseProductRepository = new WarehouseProductRepository(); + + var projection = new Projection(new ProductDbContext()); + warehouseProductRepository.Subscribe(projection.ReceiveEvent); + + var key = string.Empty; + while (key != "X") + { + Console.WriteLine("R: Receive Inventory"); + Console.WriteLine("S: Ship Inventory"); + Console.WriteLine("A: Inventory Adjustment"); + Console.WriteLine("Q: Quantity On Hand"); + Console.WriteLine("E: Events"); + Console.WriteLine("P: Projection"); + Console.Write("> "); + key = Console.ReadLine()?.ToUpperInvariant(); + Console.WriteLine(); + + var sku = GetSkuFromConsole(); + var warehouseProduct = warehouseProductRepository.Get(sku); + + switch (key) + { + case "R": + var receiveInput = GetQuantity(); + if (receiveInput.IsValid) + { + warehouseProduct.ReceiveProduct(receiveInput.Quantity); + Console.WriteLine($"{sku} Received: {receiveInput.Quantity}"); + } + break; + case "S": + var shipInput = GetQuantity(); + if (shipInput.IsValid) + { + warehouseProduct.ShipProduct(shipInput.Quantity); + Console.WriteLine($"{sku} Shipped: {shipInput.Quantity}"); + } + break; + case "A": + var adjustmentInput = GetQuantity(); + if (adjustmentInput.IsValid) + { + var reason = GetAdjustmentReason(); + warehouseProduct.AdjustInventory(adjustmentInput.Quantity, reason); + Console.WriteLine($"{sku} Adjusted: {adjustmentInput.Quantity} {reason}"); + } + break; + case "Q": + var currentQuantityOnHand = warehouseProduct.GetQuantityOnHand(); + Console.WriteLine($"{sku} Quantity On Hand: {currentQuantityOnHand}"); + break; + case "E": + Console.WriteLine($"Events: {sku}"); + foreach (var evnt in warehouseProduct.GetAllEvents()) + { + switch (evnt) + { + case ProductShipped shipProduct: + Console.WriteLine($"{shipProduct.DateTime:u} {sku} Shipped: {shipProduct.Quantity}"); + break; + case ProductReceived receiveProduct: + Console.WriteLine($"{receiveProduct.DateTime:u} {sku} Received: {receiveProduct.Quantity}"); + break; + case InventoryAdjusted inventoryAdjusted: + Console.WriteLine($"{inventoryAdjusted.DateTime:u} {sku} Adjusted: {inventoryAdjusted.Quantity} {inventoryAdjusted.Reason}"); + break; + } + + } + break; + case "P": + Console.WriteLine($"Projection: {sku}"); + var productProjection = projection.GetProduct(sku); + Console.WriteLine($"{sku} Received: {productProjection.Received}"); + Console.WriteLine($"{sku} Shipped: {productProjection.Shipped}"); + break; + } + + warehouseProductRepository.Save(warehouseProduct); + + Console.ReadLine(); + Console.WriteLine(); + } + } + + + private static string GetSkuFromConsole() + { + Console.Write("SKU: "); + return Console.ReadLine(); + } + + private static string GetAdjustmentReason() + { + Console.Write("Reason: "); + return Console.ReadLine(); + } + + private static (int Quantity, bool IsValid) GetQuantity() + { + Console.Write("Quantity: "); + if (int.TryParse(Console.ReadLine(), out var quantity)) + { + return (quantity, true); + } + else + { + Console.WriteLine("Invalid Quantity."); + return (0, false); + } + } + } +} diff --git a/Projection.cs b/Projection.cs new file mode 100644 index 0000000..77213e8 --- /dev/null +++ b/Projection.cs @@ -0,0 +1,56 @@ +using System.Linq; + +namespace EventSourcing.Demo +{ + public class Projection + { + private readonly ProductDbContext _dbContext; + + public Projection(ProductDbContext dbContext) + { + _dbContext = dbContext; + } + + public void ReceiveEvent(IEvent evnt) + { + switch (evnt) + { + case ProductShipped shipProduct: + Apply(shipProduct); + break; + case ProductReceived receiveProduct: + Apply(receiveProduct); + break; + } + } + + public Product GetProduct(string sku) + { + var product = _dbContext.Products.SingleOrDefault(x => x.Sku == sku); + if (product == null) + { + product = new Product + { + Sku = sku + }; + _dbContext.Products.Add(product); + } + + return product; + } + + private void Apply(ProductShipped shipProduct) + { + var product = GetProduct(shipProduct.Sku); + product.Shipped += shipProduct.Quantity; + _dbContext.SaveChanges(); + } + + private void Apply(ProductReceived productReceived) + { + var state = GetProduct(productReceived.Sku); + state.Received += productReceived.Quantity; + _dbContext.SaveChanges(); + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index 86387d9..ec2136a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,7 @@ -EventSourcingProjection -=============== +# Event Sourcing: Projection + +## YouTube +https://www.youtube.com/channel/UC3RKA4vunFAfrfxiJhPEplw + +## Prerequisite +None \ No newline at end of file diff --git a/WarehouseProduct.cs b/WarehouseProduct.cs new file mode 100644 index 0000000..fbd50be --- /dev/null +++ b/WarehouseProduct.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; + +namespace EventSourcing.Demo +{ + public class CurrentState + { + public int QuantityOnHand { get; set; } + } + + public class WarehouseProduct + { + public string Sku { get; } + private readonly IList _allEvents = new List(); + private readonly IList _uncommittedEvents = new List(); + + // Projection (Current State) + private readonly CurrentState _currentState = new(); + + public WarehouseProduct(string sku) + { + Sku = sku; + } + + public void ShipProduct(int quantity) + { + if (quantity > _currentState.QuantityOnHand) + { + throw new InvalidDomainException("Ah... we don't have enough product to ship?"); + } + + AddEvent(new ProductShipped(Sku, quantity, DateTime.UtcNow)); + } + + public void ReceiveProduct(int quantity) + { + AddEvent(new ProductReceived(Sku, quantity, DateTime.UtcNow)); + } + + public void AdjustInventory(int quantity, string reason) + { + if (_currentState.QuantityOnHand + quantity < 0) + { + throw new InvalidDomainException("Cannot adjust to a negative quantity on hand."); + } + + AddEvent(new InventoryAdjusted(Sku, quantity, reason, DateTime.UtcNow)); + } + + private void Apply(ProductShipped evnt) + { + _currentState.QuantityOnHand -= evnt.Quantity; + } + + private void Apply(ProductReceived evnt) + { + _currentState.QuantityOnHand += evnt.Quantity; + } + + private void Apply(InventoryAdjusted evnt) + { + _currentState.QuantityOnHand += evnt.Quantity; + } + + public void ApplyEvent(IEvent evnt) + { + switch (evnt) + { + case ProductShipped shipProduct: + Apply(shipProduct); + break; + case ProductReceived receiveProduct: + Apply(receiveProduct); + break; + case InventoryAdjusted inventoryAdjusted: + Apply(inventoryAdjusted); + break; + default: + throw new InvalidOperationException("Unsupported Event."); + } + + _allEvents.Add(evnt); + } + + public void AddEvent(IEvent evnt) + { + ApplyEvent(evnt); + _uncommittedEvents.Add(evnt); + } + + public IList GetUncommittedEvents() + { + return new List(_uncommittedEvents); + } + + public IList GetAllEvents() + { + return new List(_allEvents); + } + + public void EventsCommitted() + { + _uncommittedEvents.Clear(); + } + + public int GetQuantityOnHand() + { + return _currentState.QuantityOnHand; + } + } + + public class InvalidDomainException : Exception + { + public InvalidDomainException(string message) : base(message) + { + + } + } +} \ No newline at end of file diff --git a/WarehouseProductRepository.cs b/WarehouseProductRepository.cs new file mode 100644 index 0000000..672afcb --- /dev/null +++ b/WarehouseProductRepository.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; + +namespace EventSourcing.Demo +{ + public class WarehouseProductRepository + { + private readonly List> _projectionCallbacks = new(); + private readonly Dictionary> _inMemoryStreams = new(); + + public WarehouseProduct Get(string sku) + { + var warehouseProduct = new WarehouseProduct(sku); + + if (_inMemoryStreams.ContainsKey(sku)) + { + foreach (var evnt in _inMemoryStreams[sku]) + { + warehouseProduct.ApplyEvent(evnt); + } + } + + return warehouseProduct; + } + + public void Save(WarehouseProduct warehouseProduct) + { + if (_inMemoryStreams.ContainsKey(warehouseProduct.Sku) == false) + { + _inMemoryStreams.Add(warehouseProduct.Sku, new List()); + } + + var newEvents = warehouseProduct.GetUncommittedEvents(); + _inMemoryStreams[warehouseProduct.Sku].AddRange(newEvents); + warehouseProduct.EventsCommitted(); + + foreach (var newEvent in newEvents) + { + foreach (var callback in _projectionCallbacks) + { + callback(newEvent); + } + } + } + + public void Subscribe(Action callback) + { + _projectionCallbacks.Add(callback); + } + } +} \ No newline at end of file