From 1d8c7fbb3daf416dff2354a5332d48c36e55d4e8 Mon Sep 17 00:00:00 2001 From: Lynn Orrell Date: Wed, 5 May 2021 02:36:36 -0500 Subject: [PATCH] Initial cut of the AccountingService. Initial cut of the Bootstrapper. --- .vscode/launch.json | 31 +++++ .vscode/tasks.json | 61 ++++++++ RedDog.AccountingModel/AccountingContext.cs | 14 ++ RedDog.AccountingModel/Customer.cs | 21 +++ RedDog.AccountingModel/Order.cs | 38 +++++ RedDog.AccountingModel/OrderItem.cs | 31 +++++ .../RedDog.AccountingModel.csproj | 15 ++ .../Controllers/AccountingController.cs | 57 +++++++- .../Models/OrderItemSummary.cs | 22 +++ .../Models/OrderMetric.cs | 10 ++ .../Models/OrderSummary.cs | 33 +++++ RedDog.AccountingService/Program.cs | 13 ++ .../RedDog.AccountingService.csproj | 5 + RedDog.AccountingService/Startup.cs | 3 + .../20210505072819_InitialCreate.Designer.cs | 131 ++++++++++++++++++ .../20210505072819_InitialCreate.cs | 92 ++++++++++++ .../AccountingContextModelSnapshot.cs | 129 +++++++++++++++++ RedDog.Bootstrapper/Program.cs | 35 +++++ RedDog.Bootstrapper/README.md | 12 ++ .../RedDog.Bootstrapper.csproj | 20 +++ .../Controllers/LoyaltyController.cs | 8 +- RedDog.sln | 28 ++++ rest-samples/order-service.rest | 6 +- 23 files changed, 808 insertions(+), 7 deletions(-) create mode 100644 RedDog.AccountingModel/AccountingContext.cs create mode 100644 RedDog.AccountingModel/Customer.cs create mode 100644 RedDog.AccountingModel/Order.cs create mode 100644 RedDog.AccountingModel/OrderItem.cs create mode 100644 RedDog.AccountingModel/RedDog.AccountingModel.csproj create mode 100644 RedDog.AccountingService/Models/OrderItemSummary.cs create mode 100644 RedDog.AccountingService/Models/OrderMetric.cs create mode 100644 RedDog.AccountingService/Models/OrderSummary.cs create mode 100644 RedDog.Bootstrapper/Migrations/20210505072819_InitialCreate.Designer.cs create mode 100644 RedDog.Bootstrapper/Migrations/20210505072819_InitialCreate.cs create mode 100644 RedDog.Bootstrapper/Migrations/AccountingContextModelSnapshot.cs create mode 100644 RedDog.Bootstrapper/Program.cs create mode 100644 RedDog.Bootstrapper/README.md create mode 100644 RedDog.Bootstrapper/RedDog.Bootstrapper.csproj diff --git a/.vscode/launch.json b/.vscode/launch.json index 28641f3..86ff7d3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -99,6 +99,37 @@ "DAPR_GRPC_PORT": "5601" } }, + { + "name": "Debug AccountingService", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "Build AccountingService", + "program": "${workspaceFolder}/RedDog.AccountingService/bin/Debug/net5.0/RedDog.AccountingService.dll", + "args": [], + "cwd": "${workspaceFolder}/RedDog.AccountingService", + "stopAtEntry": false, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_URLS": "http://127.0.0.1:5700", + "DAPR_HTTP_PORT": "5780", + "DAPR_GRPC_PORT": "5701" + } + }, + { + "name": "Debug Bootstrapper", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "Build Bootstrapper", + "program": "${workspaceFolder}/RedDog.Bootstrapper/bin/Debug/net5.0/RedDog.Bootstrapper.dll", + "args": [], + "cwd": "${workspaceFolder}/RedDog.Bootstrapper", + "stopAtEntry": false, + "env": { + "DAPR_HTTP_PORT": "5880", + "DAPR_GRPC_PORT": "5801" + } + + }, ], "compounds": [ { diff --git a/.vscode/tasks.json b/.vscode/tasks.json index a67da1b..61946a2 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -79,6 +79,32 @@ "problemMatcher": "$msCompile", "group": "build" }, + { + "label": "Build AccountingService", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/RedDog.AccountingService", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile", + "group": "build" + }, + { + "label": "Build Bootstrapper", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/RedDog.Bootstrapper", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile", + "group": "build" + }, { "label": "Build Solution", "command": "dotnet", @@ -167,6 +193,24 @@ ], "problemMatcher": [] }, + { + "label": "Dapr AccountingService", + "command": "dapr", + "args": [ + "run", + "--app-id", + "accounting-service", + "--components-path", + "${workspaceFolder}/components/local", + "--app-port", + "5700", + "--dapr-grpc-port", + "5701", + "--dapr-http-port", + "5780" + ], + "problemMatcher": [] + }, { "label": "Dapr VirtualWorker", "command": "dapr", @@ -201,6 +245,22 @@ ], "problemMatcher": [] }, + { + "label": "Dapr Bootstrapper", + "command": "dapr", + "args": [ + "run", + "--app-id", + "bootstrapper", + "--components-path", + "${workspaceFolder}/components/local", + "--dapr-grpc-port", + "5801", + "--dapr-http-port", + "5880" + ], + "problemMatcher": [] + }, { "label": "Dapr (All Services)", "dependsOn": [ @@ -208,6 +268,7 @@ "Dapr MakeLineService", "Dapr LoyaltyService", "Dapr ReceiptGenerationService", + "Dapr AccountingService", "Dapr VirtualWorker", "Dapr VirtualCustomers" ], diff --git a/RedDog.AccountingModel/AccountingContext.cs b/RedDog.AccountingModel/AccountingContext.cs new file mode 100644 index 0000000..0870928 --- /dev/null +++ b/RedDog.AccountingModel/AccountingContext.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore; + +namespace RedDog.AccountingModel +{ + public class AccountingContext : DbContext + { + public AccountingContext(DbContextOptions options) : base(options) + { + } + + public DbSet Customers { get; set; } + public DbSet Orders {get; set; } + } +} \ No newline at end of file diff --git a/RedDog.AccountingModel/Customer.cs b/RedDog.AccountingModel/Customer.cs new file mode 100644 index 0000000..88bd058 --- /dev/null +++ b/RedDog.AccountingModel/Customer.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RedDog.AccountingModel +{ + [Table(nameof(Customer))] + public class Customer + { + [Column(TypeName = "nvarchar(36)")] + [Key] + public string LoyaltyId { get; set; } + + [Column(TypeName = "nvarchar(50)")] + [Required] + public string FirstName { get; set; } + + [Column(TypeName = "nvarchar(50)")] + [Required] + public string LastName { get; set; } + } +} \ No newline at end of file diff --git a/RedDog.AccountingModel/Order.cs b/RedDog.AccountingModel/Order.cs new file mode 100644 index 0000000..3214448 --- /dev/null +++ b/RedDog.AccountingModel/Order.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RedDog.AccountingModel +{ + [Table(nameof(Order))] + public class Order + { + public Order() + { + OrderItems = new List(); + } + + [Key] + public Guid OrderId { get; set; } + + [Column(TypeName = "nvarchar(50)")] + [Required] + public string StoreId { get; set; } + + [Required] + public DateTime PlacedDate { get; set; } + + public DateTime? CompletedDate { get; set; } + + [Required] + public Customer Customer { get; set; } + + [Required] + public List OrderItems { get; set; } + + [Required] + [Column(TypeName = "decimal(18,2)")] + public decimal OrderTotal { get; set; } + } +} \ No newline at end of file diff --git a/RedDog.AccountingModel/OrderItem.cs b/RedDog.AccountingModel/OrderItem.cs new file mode 100644 index 0000000..f3f8638 --- /dev/null +++ b/RedDog.AccountingModel/OrderItem.cs @@ -0,0 +1,31 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RedDog.AccountingModel +{ + [Table(nameof(OrderItem))] + public class OrderItem + { + [Key] + public int OrderItemId { get; set; } + + public int ProductId { get; set; } + + [Column(TypeName = "nvarchar(50)")] + [Required] + public string ProductName { get; set; } + + public int Quantity { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal UnitCost { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal UnitPrice { get; set; } + + public Guid OrderId { get; set; } + + public Order Order { get; set; } + } +} \ No newline at end of file diff --git a/RedDog.AccountingModel/RedDog.AccountingModel.csproj b/RedDog.AccountingModel/RedDog.AccountingModel.csproj new file mode 100644 index 0000000..c332556 --- /dev/null +++ b/RedDog.AccountingModel/RedDog.AccountingModel.csproj @@ -0,0 +1,15 @@ + + + + net5.0 + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + diff --git a/RedDog.AccountingService/Controllers/AccountingController.cs b/RedDog.AccountingService/Controllers/AccountingController.cs index 01972f0..4a827bb 100644 --- a/RedDog.AccountingService/Controllers/AccountingController.cs +++ b/RedDog.AccountingService/Controllers/AccountingController.cs @@ -2,8 +2,12 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Dapr; +using Dapr.Client; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using RedDog.AccountingModel; +using RedDog.AccountingService.Models; namespace RedDog.AccountingService.Controllers { @@ -11,11 +15,62 @@ namespace RedDog.AccountingService.Controllers [Route("[controller]")] public class AccountingController : ControllerBase { + private const string PubSubName = "reddog.pubsub"; + private const string OrderTopic = "orders"; private readonly ILogger _logger; + private readonly DaprClient _daprClient; - public AccountingController(ILogger logger) + public AccountingController(ILogger logger, DaprClient daprClient) { _logger = logger; + _daprClient = daprClient; + } + + [Topic(PubSubName, OrderTopic)] + [HttpPost("orders")] + public async Task UpdateMetrics(OrderSummary orderSummary, [FromServices] AccountingContext dbContext) + { + _logger.LogInformation("Received Order Summary: {@OrderSummary}", orderSummary); + + Customer customer = dbContext.Customers.SingleOrDefault(c => c.LoyaltyId == orderSummary.LoyaltyId); + customer ??= new Customer() + { + FirstName = orderSummary.FirstName, + LastName = orderSummary.LastName, + LoyaltyId = orderSummary.LoyaltyId + }; + + Order order = new Order() + { + OrderId = orderSummary.OrderId, + StoreId = orderSummary.StoreId, + PlacedDate = orderSummary.OrderDate, + Customer = customer, + OrderTotal = orderSummary.OrderTotal + }; + + foreach(var orderItemSummary in orderSummary.OrderItems) + { + order.OrderItems.Add(new OrderItem() + { + ProductId = orderItemSummary.ProductId, + ProductName = orderItemSummary.ProductName, + Quantity = orderItemSummary.Quantity, + UnitCost = orderItemSummary.UnitCost, + UnitPrice = orderItemSummary.UnitPrice + }); + } + + dbContext.Add(order); + await dbContext.SaveChangesAsync(); + + return Ok(); + } + + [HttpGet("OrderMetrics")] + public async Task> GetOrderMetricsAsync() + { + return null; } } } diff --git a/RedDog.AccountingService/Models/OrderItemSummary.cs b/RedDog.AccountingService/Models/OrderItemSummary.cs new file mode 100644 index 0000000..4d26b79 --- /dev/null +++ b/RedDog.AccountingService/Models/OrderItemSummary.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace RedDog.AccountingService.Models +{ + public class OrderItemSummary + { + [JsonPropertyName("productId")] + public int ProductId { get; set; } + + [JsonPropertyName("productName")] + public string ProductName { get; set; } + + [JsonPropertyName("quantity")] + public int Quantity { get; set; } + + [JsonPropertyName("unitCost")] + public decimal UnitCost { get; set; } + + [JsonPropertyName("unitPrice")] + public decimal UnitPrice { get; set; } + } +} \ No newline at end of file diff --git a/RedDog.AccountingService/Models/OrderMetric.cs b/RedDog.AccountingService/Models/OrderMetric.cs new file mode 100644 index 0000000..490493e --- /dev/null +++ b/RedDog.AccountingService/Models/OrderMetric.cs @@ -0,0 +1,10 @@ +using System; + +namespace RedDog.AccountingService.Models +{ + public class OrderMetric + { + public string StoreId { get; set; } + public DateTime Date { get; set; } + } +} \ No newline at end of file diff --git a/RedDog.AccountingService/Models/OrderSummary.cs b/RedDog.AccountingService/Models/OrderSummary.cs new file mode 100644 index 0000000..71cebc6 --- /dev/null +++ b/RedDog.AccountingService/Models/OrderSummary.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace RedDog.AccountingService.Models +{ + public class OrderSummary + { + [JsonPropertyName("orderId")] + public Guid OrderId { get; set; } + + [JsonPropertyName("orderDate")] + public DateTime OrderDate { get; set; } + + [JsonPropertyName("storeId")] + public string StoreId { get; set; } + + [JsonPropertyName("firstName")] + public string FirstName { get; set; } + + [JsonPropertyName("lastName")] + public string LastName { get; set; } + + [JsonPropertyName("loyaltyId")] + public string LoyaltyId { get; set; } + + [JsonPropertyName("orderItems")] + public List OrderItems { get; set; } + + [JsonPropertyName("orderTotal")] + public decimal OrderTotal { get; set; } + } +} \ No newline at end of file diff --git a/RedDog.AccountingService/Program.cs b/RedDog.AccountingService/Program.cs index 07646f6..e31464d 100644 --- a/RedDog.AccountingService/Program.cs +++ b/RedDog.AccountingService/Program.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Dapr.Client; +using Dapr.Extensions.Configuration; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; @@ -14,6 +16,8 @@ namespace RedDog.AccountingService { public class Program { + private const string SecretStoreName = "reddog.secretstore"; + public static void Main(string[] args) { Log.Logger = new LoggerConfiguration() @@ -44,6 +48,15 @@ namespace RedDog.AccountingService public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .UseSerilog() + .ConfigureAppConfiguration(config => + { + var daprClient = new DaprClientBuilder().Build(); + var secretDescriptors = new List + { + new DaprSecretDescriptor("reddog-sql") + }; + config.AddDaprSecretStore(SecretStoreName, secretDescriptors, daprClient); + }) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); diff --git a/RedDog.AccountingService/RedDog.AccountingService.csproj b/RedDog.AccountingService/RedDog.AccountingService.csproj index 6ad0e3d..152fa3e 100644 --- a/RedDog.AccountingService/RedDog.AccountingService.csproj +++ b/RedDog.AccountingService/RedDog.AccountingService.csproj @@ -6,8 +6,13 @@ + + + + + diff --git a/RedDog.AccountingService/Startup.cs b/RedDog.AccountingService/Startup.cs index 0af30a6..8afba4d 100644 --- a/RedDog.AccountingService/Startup.cs +++ b/RedDog.AccountingService/Startup.cs @@ -5,11 +5,13 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; +using RedDog.AccountingModel; using Serilog; namespace RedDog.AccountingService @@ -31,6 +33,7 @@ namespace RedDog.AccountingService { c.SwaggerDoc("v1", new OpenApiInfo { Title = "RedDog.AccountingService", Version = "v1" }); }); + services.AddDbContext(options => options.UseSqlServer(Configuration["reddog-sql"])); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) diff --git a/RedDog.Bootstrapper/Migrations/20210505072819_InitialCreate.Designer.cs b/RedDog.Bootstrapper/Migrations/20210505072819_InitialCreate.Designer.cs new file mode 100644 index 0000000..da88d5c --- /dev/null +++ b/RedDog.Bootstrapper/Migrations/20210505072819_InitialCreate.Designer.cs @@ -0,0 +1,131 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RedDog.AccountingModel; + +namespace RedDog.Bootstrapper.Migrations +{ + [DbContext(typeof(AccountingContext))] + [Migration("20210505072819_InitialCreate")] + partial class InitialCreate + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("ProductVersion", "5.0.5") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("RedDog.AccountingModel.Customer", b => + { + b.Property("LoyaltyId") + .HasColumnType("nvarchar(36)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(50)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(50)"); + + b.HasKey("LoyaltyId"); + + b.ToTable("Customer"); + }); + + modelBuilder.Entity("RedDog.AccountingModel.Order", b => + { + b.Property("OrderId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CompletedDate") + .HasColumnType("datetime2"); + + b.Property("CustomerLoyaltyId") + .HasColumnType("nvarchar(36)"); + + b.Property("OrderTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("PlacedDate") + .HasColumnType("datetime2"); + + b.Property("StoreId") + .IsRequired() + .HasColumnType("nvarchar(50)"); + + b.HasKey("OrderId"); + + b.HasIndex("CustomerLoyaltyId"); + + b.ToTable("Order"); + }); + + modelBuilder.Entity("RedDog.AccountingModel.OrderItem", b => + { + b.Property("OrderItemId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("OrderId") + .HasColumnType("uniqueidentifier"); + + b.Property("ProductId") + .HasColumnType("int"); + + b.Property("ProductName") + .IsRequired() + .HasColumnType("nvarchar(50)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("UnitCost") + .HasColumnType("decimal(18,2)"); + + b.Property("UnitPrice") + .HasColumnType("decimal(18,2)"); + + b.HasKey("OrderItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItem"); + }); + + modelBuilder.Entity("RedDog.AccountingModel.Order", b => + { + b.HasOne("RedDog.AccountingModel.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerLoyaltyId"); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("RedDog.AccountingModel.OrderItem", b => + { + b.HasOne("RedDog.AccountingModel.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("RedDog.AccountingModel.Order", b => + { + b.Navigation("OrderItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RedDog.Bootstrapper/Migrations/20210505072819_InitialCreate.cs b/RedDog.Bootstrapper/Migrations/20210505072819_InitialCreate.cs new file mode 100644 index 0000000..d0a9ccd --- /dev/null +++ b/RedDog.Bootstrapper/Migrations/20210505072819_InitialCreate.cs @@ -0,0 +1,92 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace RedDog.Bootstrapper.Migrations +{ + public partial class InitialCreate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Customer", + columns: table => new + { + LoyaltyId = table.Column(type: "nvarchar(36)", nullable: false), + FirstName = table.Column(type: "nvarchar(50)", nullable: false), + LastName = table.Column(type: "nvarchar(50)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Customer", x => x.LoyaltyId); + }); + + migrationBuilder.CreateTable( + name: "Order", + columns: table => new + { + OrderId = table.Column(type: "uniqueidentifier", nullable: false), + StoreId = table.Column(type: "nvarchar(50)", nullable: false), + PlacedDate = table.Column(type: "datetime2", nullable: false), + CompletedDate = table.Column(type: "datetime2", nullable: true), + CustomerLoyaltyId = table.Column(type: "nvarchar(36)", nullable: true), + OrderTotal = table.Column(type: "decimal(18,2)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Order", x => x.OrderId); + table.ForeignKey( + name: "FK_Order_Customer_CustomerLoyaltyId", + column: x => x.CustomerLoyaltyId, + principalTable: "Customer", + principalColumn: "LoyaltyId", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "OrderItem", + columns: table => new + { + OrderItemId = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + ProductId = table.Column(type: "int", nullable: false), + ProductName = table.Column(type: "nvarchar(50)", nullable: false), + Quantity = table.Column(type: "int", nullable: false), + UnitCost = table.Column(type: "decimal(18,2)", nullable: false), + UnitPrice = table.Column(type: "decimal(18,2)", nullable: false), + OrderId = table.Column(type: "uniqueidentifier", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrderItem", x => x.OrderItemId); + table.ForeignKey( + name: "FK_OrderItem_Order_OrderId", + column: x => x.OrderId, + principalTable: "Order", + principalColumn: "OrderId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Order_CustomerLoyaltyId", + table: "Order", + column: "CustomerLoyaltyId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderItem_OrderId", + table: "OrderItem", + column: "OrderId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OrderItem"); + + migrationBuilder.DropTable( + name: "Order"); + + migrationBuilder.DropTable( + name: "Customer"); + } + } +} diff --git a/RedDog.Bootstrapper/Migrations/AccountingContextModelSnapshot.cs b/RedDog.Bootstrapper/Migrations/AccountingContextModelSnapshot.cs new file mode 100644 index 0000000..049fdd6 --- /dev/null +++ b/RedDog.Bootstrapper/Migrations/AccountingContextModelSnapshot.cs @@ -0,0 +1,129 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RedDog.AccountingModel; + +namespace RedDog.Bootstrapper.Migrations +{ + [DbContext(typeof(AccountingContext))] + partial class AccountingContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("ProductVersion", "5.0.5") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("RedDog.AccountingModel.Customer", b => + { + b.Property("LoyaltyId") + .HasColumnType("nvarchar(36)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(50)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(50)"); + + b.HasKey("LoyaltyId"); + + b.ToTable("Customer"); + }); + + modelBuilder.Entity("RedDog.AccountingModel.Order", b => + { + b.Property("OrderId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CompletedDate") + .HasColumnType("datetime2"); + + b.Property("CustomerLoyaltyId") + .HasColumnType("nvarchar(36)"); + + b.Property("OrderTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("PlacedDate") + .HasColumnType("datetime2"); + + b.Property("StoreId") + .IsRequired() + .HasColumnType("nvarchar(50)"); + + b.HasKey("OrderId"); + + b.HasIndex("CustomerLoyaltyId"); + + b.ToTable("Order"); + }); + + modelBuilder.Entity("RedDog.AccountingModel.OrderItem", b => + { + b.Property("OrderItemId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("OrderId") + .HasColumnType("uniqueidentifier"); + + b.Property("ProductId") + .HasColumnType("int"); + + b.Property("ProductName") + .IsRequired() + .HasColumnType("nvarchar(50)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("UnitCost") + .HasColumnType("decimal(18,2)"); + + b.Property("UnitPrice") + .HasColumnType("decimal(18,2)"); + + b.HasKey("OrderItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItem"); + }); + + modelBuilder.Entity("RedDog.AccountingModel.Order", b => + { + b.HasOne("RedDog.AccountingModel.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerLoyaltyId"); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("RedDog.AccountingModel.OrderItem", b => + { + b.HasOne("RedDog.AccountingModel.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("RedDog.AccountingModel.Order", b => + { + b.Navigation("OrderItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RedDog.Bootstrapper/Program.cs b/RedDog.Bootstrapper/Program.cs new file mode 100644 index 0000000..fa4ca30 --- /dev/null +++ b/RedDog.Bootstrapper/Program.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using Dapr.Client; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using RedDog.AccountingModel; + +namespace RedDog.Bootstrapper +{ + class Program : IDesignTimeDbContextFactory + { + private const string SecretStoreName = "reddog.secretstore"; + + static async Task Main(string[] args) + { + Program p = new Program(); + + using AccountingContext context = p.CreateDbContext(null); + await context.Database.MigrateAsync(); + } + + public AccountingContext CreateDbContext(string[] args) + { + var daprClient = new DaprClientBuilder().Build(); + var connectionString = daprClient.GetSecretAsync(SecretStoreName, "reddog-sql").GetAwaiter().GetResult(); + + DbContextOptionsBuilder optionsBuilder = new DbContextOptionsBuilder().UseSqlServer(connectionString["reddog-sql"], b => + { + b.MigrationsAssembly("RedDog.Bootstrapper"); + b.EnableRetryOnFailure(); + }); + + return new AccountingContext(optionsBuilder.Options); + } + } +} diff --git a/RedDog.Bootstrapper/README.md b/RedDog.Bootstrapper/README.md new file mode 100644 index 0000000..d8f1865 --- /dev/null +++ b/RedDog.Bootstrapper/README.md @@ -0,0 +1,12 @@ +create user reddog with password = [PASSWORD]; +go; + +grant create table to reddog; +go; + +grant control on schema::dbo to reddog; +go; + + +Running migrations +DAPR_GRPC_PORT=5801 dotnet ef migrations add InitialCreate \ No newline at end of file diff --git a/RedDog.Bootstrapper/RedDog.Bootstrapper.csproj b/RedDog.Bootstrapper/RedDog.Bootstrapper.csproj new file mode 100644 index 0000000..73c529a --- /dev/null +++ b/RedDog.Bootstrapper/RedDog.Bootstrapper.csproj @@ -0,0 +1,20 @@ + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + Exe + net5.0 + + + diff --git a/RedDog.LoyaltyService/Controllers/LoyaltyController.cs b/RedDog.LoyaltyService/Controllers/LoyaltyController.cs index d613763..a95e08d 100644 --- a/RedDog.LoyaltyService/Controllers/LoyaltyController.cs +++ b/RedDog.LoyaltyService/Controllers/LoyaltyController.cs @@ -16,15 +16,17 @@ namespace RedDog.LoyaltyService.Controllers private const string PubSubName = "reddog.pubsub"; private const string LoyaltyStateStoreName = "reddog.state.loyalty"; private readonly ILogger _logger; + private readonly DaprClient _daprClient; - public LoyaltyController(ILogger logger) + public LoyaltyController(ILogger logger, DaprClient daprClient) { _logger = logger; + _daprClient = daprClient; } [Topic(PubSubName, OrderTopic)] [HttpPost("orders")] - public async Task UpdateLoyalty(OrderSummary orderSummary, [FromServices] DaprClient daprClient) + public async Task UpdateLoyalty(OrderSummary orderSummary) { _logger.LogInformation("Received Order Summary: {@OrderSummary}", orderSummary); @@ -34,7 +36,7 @@ namespace RedDog.LoyaltyService.Controllers StateEntry stateEntry = null; try { - stateEntry = await daprClient.GetStateEntryAsync(LoyaltyStateStoreName, orderSummary.LoyaltyId); + stateEntry = await _daprClient.GetStateEntryAsync(LoyaltyStateStoreName, orderSummary.LoyaltyId); stateEntry.Value ??= new LoyaltySummary() { FirstName = orderSummary.FirstName, diff --git a/RedDog.sln b/RedDog.sln index 03b0b50..555be46 100644 --- a/RedDog.sln +++ b/RedDog.sln @@ -17,6 +17,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedDog.VirtualWorker", "Red EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedDog.AccountingService", "RedDog.AccountingService\RedDog.AccountingService.csproj", "{F8E0D2B2-5E21-4763-AC61-0074FA1297F9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedDog.Bootstrapper", "RedDog.Bootstrapper\RedDog.Bootstrapper.csproj", "{B2AE26A4-3A12-4BC8-824C-6C08F04A111F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedDog.AccountingModel", "RedDog.AccountingModel\RedDog.AccountingModel.csproj", "{775EACDA-CD1B-41BC-A1CE-85E7CB8CDB63}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -114,5 +118,29 @@ Global {F8E0D2B2-5E21-4763-AC61-0074FA1297F9}.Release|x64.Build.0 = Release|Any CPU {F8E0D2B2-5E21-4763-AC61-0074FA1297F9}.Release|x86.ActiveCfg = Release|Any CPU {F8E0D2B2-5E21-4763-AC61-0074FA1297F9}.Release|x86.Build.0 = Release|Any CPU + {B2AE26A4-3A12-4BC8-824C-6C08F04A111F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2AE26A4-3A12-4BC8-824C-6C08F04A111F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2AE26A4-3A12-4BC8-824C-6C08F04A111F}.Debug|x64.ActiveCfg = Debug|Any CPU + {B2AE26A4-3A12-4BC8-824C-6C08F04A111F}.Debug|x64.Build.0 = Debug|Any CPU + {B2AE26A4-3A12-4BC8-824C-6C08F04A111F}.Debug|x86.ActiveCfg = Debug|Any CPU + {B2AE26A4-3A12-4BC8-824C-6C08F04A111F}.Debug|x86.Build.0 = Debug|Any CPU + {B2AE26A4-3A12-4BC8-824C-6C08F04A111F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2AE26A4-3A12-4BC8-824C-6C08F04A111F}.Release|Any CPU.Build.0 = Release|Any CPU + {B2AE26A4-3A12-4BC8-824C-6C08F04A111F}.Release|x64.ActiveCfg = Release|Any CPU + {B2AE26A4-3A12-4BC8-824C-6C08F04A111F}.Release|x64.Build.0 = Release|Any CPU + {B2AE26A4-3A12-4BC8-824C-6C08F04A111F}.Release|x86.ActiveCfg = Release|Any CPU + {B2AE26A4-3A12-4BC8-824C-6C08F04A111F}.Release|x86.Build.0 = Release|Any CPU + {775EACDA-CD1B-41BC-A1CE-85E7CB8CDB63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {775EACDA-CD1B-41BC-A1CE-85E7CB8CDB63}.Debug|Any CPU.Build.0 = Debug|Any CPU + {775EACDA-CD1B-41BC-A1CE-85E7CB8CDB63}.Debug|x64.ActiveCfg = Debug|Any CPU + {775EACDA-CD1B-41BC-A1CE-85E7CB8CDB63}.Debug|x64.Build.0 = Debug|Any CPU + {775EACDA-CD1B-41BC-A1CE-85E7CB8CDB63}.Debug|x86.ActiveCfg = Debug|Any CPU + {775EACDA-CD1B-41BC-A1CE-85E7CB8CDB63}.Debug|x86.Build.0 = Debug|Any CPU + {775EACDA-CD1B-41BC-A1CE-85E7CB8CDB63}.Release|Any CPU.ActiveCfg = Release|Any CPU + {775EACDA-CD1B-41BC-A1CE-85E7CB8CDB63}.Release|Any CPU.Build.0 = Release|Any CPU + {775EACDA-CD1B-41BC-A1CE-85E7CB8CDB63}.Release|x64.ActiveCfg = Release|Any CPU + {775EACDA-CD1B-41BC-A1CE-85E7CB8CDB63}.Release|x64.Build.0 = Release|Any CPU + {775EACDA-CD1B-41BC-A1CE-85E7CB8CDB63}.Release|x86.ActiveCfg = Release|Any CPU + {775EACDA-CD1B-41BC-A1CE-85E7CB8CDB63}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/rest-samples/order-service.rest b/rest-samples/order-service.rest index ded1ad2..1bfeab8 100644 --- a/rest-samples/order-service.rest +++ b/rest-samples/order-service.rest @@ -14,15 +14,15 @@ Content-Type: application/json "loyaltyId": "42", "orderItems": [ { - "menuItemId": 1, + "productId": 1, "quantity": 1 }, { - "menuItemId": 2, + "productId": 2, "quantity": 1 }, { - "menuItemId": 3, + "productId": 3, "quantity": 3 } ]