diff --git a/docker-compose.yml b/docker-compose.yml index babbf2b..f8ed1c1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -438,13 +438,15 @@ services: networks: - cloud-native smtp: - image: gessnerfl/fake-smtp-server + image: rnwood/smtp4dev container_name: smtp environment: - - SPRING_MAIL_USERNAME=cnadmin - - SPRING_MAIL_PASSWORD=somepassword + - ServerOptions__Port=5025 + - ServerOptions__HostName=smtp + - ServerOptions__LockSettings=true + - ServerOptions__TlsMode=StartTls ports: - - "5080:5080" + - "5080:80" networks: - cloud-native cadvisor: diff --git a/src/AdminCli/CliCommands/Products/ProductCommandsModule.cs b/src/AdminCli/CliCommands/Products/ProductCommandsModule.cs index a731624..38ee9e1 100644 --- a/src/AdminCli/CliCommands/Products/ProductCommandsModule.cs +++ b/src/AdminCli/CliCommands/Products/ProductCommandsModule.cs @@ -7,5 +7,6 @@ public static class ProductCommandsModule public static IServiceCollection AddProductCommands(this IServiceCollection services) => services.AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); } diff --git a/src/AdminCli/CliCommands/Products/ProductsCommand.cs b/src/AdminCli/CliCommands/Products/ProductsCommand.cs index 9b9a3b5..04c9f7a 100644 --- a/src/AdminCli/CliCommands/Products/ProductsCommand.cs +++ b/src/AdminCli/CliCommands/Products/ProductsCommand.cs @@ -14,6 +14,7 @@ public void ConfigureCommand(CommandLineApplication config) config.Description = "Allows you to list products or issue a price drop"; var context = new CommandConfigurationContext(config, ServiceProvider); context.ConfigureCommand("list"); + context.ConfigureCommand("sign-up"); context.ConfigureCommand("drop-price"); config.OnExecute(() => config.ShowHelp()); diff --git a/src/AdminCli/CliCommands/Products/SignUpForPriceDropCommand.cs b/src/AdminCli/CliCommands/Products/SignUpForPriceDropCommand.cs new file mode 100644 index 0000000..681e7ce --- /dev/null +++ b/src/AdminCli/CliCommands/Products/SignUpForPriceDropCommand.cs @@ -0,0 +1,78 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using AdminCli.HttpAccess; +using McMaster.Extensions.CommandLineUtils; + +namespace AdminCli.CliCommands.Products; + +public sealed class SignUpForPriceDropCommand : ICliCommand +{ + public SignUpForPriceDropCommand(IHttpService httpService) => HttpService = httpService; + + private IHttpService HttpService { get; } + + public void ConfigureCommand(CommandLineApplication config) + { + config.Description = "Sign up with your email address to get notified when a product's price drops."; + var emailOption = + config.Option("-e||--email ", + "Your email address. We will write an email to this address once the price drops.", + CommandOptionType.SingleValue) + .IsRequired(); + + var productIdOption = + config.Option("-p|--product-id ", + "The ID of the product that you want to watch the price on. Must be a GUID. Use the products list command to obtain the GUID of a product.", + CommandOptionType.SingleValue) + .IsRequired(); + + var priceOption = + config.Option("-t|--target-price ", + "The target price. When the product's price is dropped to a value equal or below the target price, an email will be sent.", + CommandOptionType.SingleValue); + + config.OnExecuteAsync(async cancellationToken => + { + var email = emailOption.Value(); + if (!ValidateEmail(email)) + { + Console.WriteLine("You did not provide a valid email address"); + return; + } + + var productIdText = productIdOption.Value(); + if (!Guid.TryParse(productIdText, out var parsedId)) + { + Console.WriteLine($"Could not parse \"{productIdText}\" to a GUID."); + return; + } + + var priceText = priceOption.Value(); + if (!double.TryParse(priceText, CultureInfo.CurrentCulture, out var parsedTargetPrice)) + { + Console.WriteLine($"Could not parse \"{priceText}\" to a decimal number."); + return; + } + + var dto = new SignUpForPriceDropDto(email, parsedId, parsedTargetPrice); + await HttpService.PostAsync("/pricewatcher/register", dto, cancellationToken); + Console.WriteLine("You signed up successfully."); + }); + } + + private static bool ValidateEmail([NotNullWhen(true)] string? email) + { + // We simply check if the email address contain an @ which is not placed at the beginning or end. + if (email is null || email.Length < 3) + return false; + + var indexOfAt = email.IndexOf('@'); + return indexOfAt > 0 && indexOfAt != email.Length - 1; + } +} + +// The properties of this record are read by the JSON serializer +// ReSharper disable NotAccessedPositionalProperty.Global +public sealed record SignUpForPriceDropDto(string Email, Guid ProductId, double Price); +// ReSharper restore NotAccessedPositionalProperty.Global diff --git a/src/PriceWatcherService/Entities/InMemoryProducts.cs b/src/PriceWatcherService/Entities/InMemoryProducts.cs new file mode 100644 index 0000000..44bb7b0 --- /dev/null +++ b/src/PriceWatcherService/Entities/InMemoryProducts.cs @@ -0,0 +1,94 @@ +namespace PriceWatcher.Entities; + +// This class is copied from ProductService. Everything you change here should also be changed over there. +public static class InMemoryProducts +{ + public static List Products { get; } = + new () + { + new Product( + Guid.Parse("b3b749d1-fd02-4b47-8e3c-540555439db6"), + "Milk", + "Good milk", + new List { "Food" }, + 0.99 + ), + new Product( + Guid.Parse("aaaaaaaa-fd02-4b47-8e3c-540555439db6"), + "Coffee", + "Delicious Coffee", + new List { "Food" }, + 1.99 + ), + new Product( + Guid.Parse("08c64d77-4e3e-45f0-8455-078fca893049"), + "Coke", + "Tasty coke", + new List { "Food" }, + 1.49 + ), + new Product( + Guid.Parse("f6877871-2a14-4f40-a61a-e1153592c0fb"), + "Beer", + "Good beer", + new List { "Food" }, + 2.99 + ), + new Product( + Guid.Parse("9dfeb719-32e1-49a9-b55d-539f5b116dd6"), + "Bread", + "Delicious bread", + new List { "Food" }, + 0.99 + ), + new Product( + Guid.Parse("1316ef5e-96b3-4976-adc4-ca97fd121078"), + "Sausage", + "Tasty sausage", + new List { "Food" }, + 1.49 + ), + new Product( + Guid.Parse("d06c4115-85d5-4448-b398-464850eebf4e"), + "Cheese", + "Good cheese", + new List { "Food" }, + 2.99 + ), + new Product( + Guid.Parse("4382ba39-c9e3-48bb-83b3-9f9171b4c33f"), + "Chocolate", + "Delicious chocolate", + new List { "Food" }, + 0.99 + ), + new Product( + Guid.Parse("9d428166-3cb7-4513-ae0d-e1cb18ac1416"), + "Candy", + "Tasty candy", + new List { "Food" }, + 1.49 + ), + new Product( + Guid.Parse("782080a1-7953-4ac0-92d8-59ec5497563b"), + "Ice cream", + "Good ice cream", + new List { "Food" }, + 2.99 + ), + new Product( + Guid.Parse("128cc5a0-9a73-4cb8-896b-7d1f8e9fb5f3"), + "Burger", + "Delicious burger", + new List { "Food" }, + 7.99 + ), + new Product( + Guid.Parse("a028d630-2da8-432d-ad8c-b4990d288841"), + "Pizza", + "Tasty pizza", + new List { "Food" }, + 9.99 + ), + }; +} diff --git a/src/PriceWatcherService/Entities/Product.cs b/src/PriceWatcherService/Entities/Product.cs new file mode 100644 index 0000000..f6ed88b --- /dev/null +++ b/src/PriceWatcherService/Entities/Product.cs @@ -0,0 +1,21 @@ +namespace PriceWatcher.Entities +{ + // This class is copied from ProductService. Everything you change here should also be changed over there. + public class Product + { + public Product(Guid id, string name, string description, IEnumerable categories, double price) + { + Id = id; + Name = name; + Description = description; + Categories = categories; + Price = price; + } + + public Guid Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public IEnumerable Categories { get; set; } + public double Price { get; set; } + } +} diff --git a/src/PriceWatcherService/Entities/Products.cs b/src/PriceWatcherService/Entities/Products.cs deleted file mode 100644 index 7490b76..0000000 --- a/src/PriceWatcherService/Entities/Products.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace PriceWatcher.Entities; - -public class Product -{ - - public Guid Id { get; set; } - public string Name { get; set; } - public string Description {get;set;} - public double Price { get; set; } -} diff --git a/src/PriceWatcherService/Repositories/PriceWatcherRepository.cs b/src/PriceWatcherService/Repositories/PriceWatcherRepository.cs index ab2ac73..77a86de 100644 --- a/src/PriceWatcherService/Repositories/PriceWatcherRepository.cs +++ b/src/PriceWatcherService/Repositories/PriceWatcherRepository.cs @@ -12,20 +12,7 @@ public class PriceWatcherRepository : IPriceWatcherRepository private readonly PriceWatcherServiceConfiguration _cfg; private readonly ILogger _logger; private readonly List _watchers = new List(); - - private static readonly List _products = new List { - new Product { Id = Guid.Parse("08ae4294-47e1-4b76-8cd3-052d6308d699"), Name = "Ice cream", Description = "Cool down on hot days", Price = 4.49 }, - new Product { Id = Guid.Parse("67611138-7dc1-42d9-b910-42c5d0247c52"), Name = "Bread", Description = "Yummy! Fresh bread smells super good", Price = 4.29 }, - new Product { Id = Guid.Parse("fed436f8-76a2-4ce0-83a2-6bdb0fed705b"), Name = "Coffee", Description = "Delicious Coffee", Price = 2.49 }, - new Product { Id = Guid.Parse("870d8ca1-1936-41a2-9f40-7d399f29ac38"), Name = "Bacon Burger", Description = "Everything is better with bacon", Price = 8.99 }, - new Product { Id = Guid.Parse("d96798d2-b429-4842-9a29-9a2a448d4ff2"), Name = "Whisky", Description = "Gentle drink for cold evenings", Price = 49.99 }, - new Product { Id = Guid.Parse("83fc59d6-9e20-450a-84c0-c8bc8fd80ee1"), Name = "Coke", Description = "Tasty coke", Price = 1.99 }, - new Product { Id = Guid.Parse("e2810857-327d-47d1-918c-cf3e3709d2d8"), Name = "Sausage", Description = "Time for some BBQ", Price = 3.79 }, - new Product { Id = Guid.Parse("525f1786-c045-46b1-aac2-d06da196bac4"), Name = "Beer", Description = "Tasty craft beer", Price = 3.99 }, - new Product { Id = Guid.Parse("9b699928-4600-44bf-9923-ec41a428b809"), Name = "Coffee", Description = "Delicious", Price = 2.49 }, - new Product { Id = Guid.Parse("2620540e-bfcb-4a06-87a4-f6ed2b3c069b"), Name = "Pizza", Description = "It comes with Bacon. You know! Because everything is better with bacon", Price = 7.99 } - }; - + public PriceWatcherRepository(DaprClient dapr, PriceWatcherServiceConfiguration cfg, ILogger logger) { _dapr = dapr; @@ -33,9 +20,11 @@ public PriceWatcherRepository(DaprClient dapr, PriceWatcherServiceConfiguration _logger = logger; } + private static List Products => InMemoryProducts.Products; + public bool Register(string email, Guid productId, double price) { - if (!_products.Any(p => p.Id.Equals(productId))) + if (!Products.Any(p => p.Id.Equals(productId))) { _logger.LogInformation("Product {ProductId} not found", productId); return false; @@ -61,7 +50,7 @@ public bool Register(string email, Guid productId, double price) public bool DropPrice(Guid productId, double dropBy) { - var found = _products.FirstOrDefault(p => p.Id.Equals(productId)); + var found = Products.FirstOrDefault(p => p.Id.Equals(productId)); if (found == null) { _logger.LogInformation("Product {ProductId} not found", productId); diff --git a/src/ProductsService/Data/Entities/Product.cs b/src/ProductsService/Data/Entities/Product.cs index d3def30..da36d6e 100644 --- a/src/ProductsService/Data/Entities/Product.cs +++ b/src/ProductsService/Data/Entities/Product.cs @@ -1,20 +1,20 @@ -namespace ProductsService.Data.Entities +namespace ProductsService.Data.Entities; + +// This class is copied to PriceWatcher. Everything you change here should also be changed over there. +public class Product { - public class Product + public Product(Guid id, string name, string description, IEnumerable categories, double price) { - public Product(Guid id, string name, string description, IEnumerable categories, double price) - { - Id = id; - Name = name; - Description = description; - Categories = categories; - Price = price; - } - - public Guid Id { get; set; } - public string Name { get; set; } - public string Description { get; set; } - public IEnumerable Categories { get; set; } - public double Price { get; set; } + Id = id; + Name = name; + Description = description; + Categories = categories; + Price = price; } + + public Guid Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public IEnumerable Categories { get; set; } + public double Price { get; set; } } diff --git a/src/ProductsService/Data/InMemoryProducts.cs b/src/ProductsService/Data/InMemoryProducts.cs new file mode 100644 index 0000000..31f8d15 --- /dev/null +++ b/src/ProductsService/Data/InMemoryProducts.cs @@ -0,0 +1,96 @@ +using ProductsService.Data.Entities; + +namespace ProductsService.Data; + +// This class is copied to PriceWatcher. Everything you change here should also be changed over there. +public static class InMemoryProducts +{ + public static List Products { get; } = + new () + { + new Product( + Guid.Parse("b3b749d1-fd02-4b47-8e3c-540555439db6"), + "Milk", + "Good milk", + new List { "Food" }, + 0.99 + ), + new Product( + Guid.Parse("aaaaaaaa-fd02-4b47-8e3c-540555439db6"), + "Coffee", + "Delicious Coffee", + new List { "Food" }, + 1.99 + ), + new Product( + Guid.Parse("08c64d77-4e3e-45f0-8455-078fca893049"), + "Coke", + "Tasty coke", + new List { "Food" }, + 1.49 + ), + new Product( + Guid.Parse("f6877871-2a14-4f40-a61a-e1153592c0fb"), + "Beer", + "Good beer", + new List { "Food" }, + 2.99 + ), + new Product( + Guid.Parse("9dfeb719-32e1-49a9-b55d-539f5b116dd6"), + "Bread", + "Delicious bread", + new List { "Food" }, + 0.99 + ), + new Product( + Guid.Parse("1316ef5e-96b3-4976-adc4-ca97fd121078"), + "Sausage", + "Tasty sausage", + new List { "Food" }, + 1.49 + ), + new Product( + Guid.Parse("d06c4115-85d5-4448-b398-464850eebf4e"), + "Cheese", + "Good cheese", + new List { "Food" }, + 2.99 + ), + new Product( + Guid.Parse("4382ba39-c9e3-48bb-83b3-9f9171b4c33f"), + "Chocolate", + "Delicious chocolate", + new List { "Food" }, + 0.99 + ), + new Product( + Guid.Parse("9d428166-3cb7-4513-ae0d-e1cb18ac1416"), + "Candy", + "Tasty candy", + new List { "Food" }, + 1.49 + ), + new Product( + Guid.Parse("782080a1-7953-4ac0-92d8-59ec5497563b"), + "Ice cream", + "Good ice cream", + new List { "Food" }, + 2.99 + ), + new Product( + Guid.Parse("128cc5a0-9a73-4cb8-896b-7d1f8e9fb5f3"), + "Burger", + "Delicious burger", + new List { "Food" }, + 7.99 + ), + new Product( + Guid.Parse("a028d630-2da8-432d-ad8c-b4990d288841"), + "Pizza", + "Tasty pizza", + new List { "Food" }, + 9.99 + ), + }; +} diff --git a/src/ProductsService/Data/Repositories/InMemoryProductsRepository.cs b/src/ProductsService/Data/Repositories/InMemoryProductsRepository.cs index 9f756c9..909c672 100644 --- a/src/ProductsService/Data/Repositories/InMemoryProductsRepository.cs +++ b/src/ProductsService/Data/Repositories/InMemoryProductsRepository.cs @@ -5,33 +5,7 @@ namespace ProductsService.Data.Repositories public class InMemoryProductsRepository : IProductsRepository { private readonly ILogger _logger; - private readonly List _products = new() - { - new Product(Guid.Parse("b3b749d1-fd02-4b47-8e3c-540555439db6"), "Milk", "Good milk", - new List { "Food" }, 0.99), - new Product(Guid.Parse("aaaaaaaa-fd02-4b47-8e3c-540555439db6"), "Coffee", "Delicious Coffee", - new List { "Food" }, 1.99), - new Product(Guid.Parse("08c64d77-4e3e-45f0-8455-078fca893049"), "Coke", "Tasty coke", - new List { "Food" }, 1.49), - new Product(Guid.Parse("f6877871-2a14-4f40-a61a-e1153592c0fb"), "Beer", "Good beer", - new List { "Food" }, 2.99), - new Product(Guid.Parse("9dfeb719-32e1-49a9-b55d-539f5b116dd6"), "Bread", "Delicious bread", - new List { "Food" }, 0.99), - new Product(Guid.Parse("1316ef5e-96b3-4976-adc4-ca97fd121078"), "Sausage", "Tasty sausage", - new List { "Food" }, 1.49), - new Product(Guid.Parse("d06c4115-85d5-4448-b398-464850eebf4e"), "Cheese", "Good cheese", - new List { "Food" }, 2.99), - new Product(Guid.Parse("4382ba39-c9e3-48bb-83b3-9f9171b4c33f"), "Chocolate", "Delicious chocolate", - new List { "Food" }, 0.99), - new Product(Guid.Parse("9d428166-3cb7-4513-ae0d-e1cb18ac1416"), "Candy", "Tasty candy", - new List { "Food" }, 1.49), - new Product(Guid.Parse("782080a1-7953-4ac0-92d8-59ec5497563b"), "Ice cream", "Good ice cream", - new List { "Food" }, 2.99), - new Product(Guid.Parse("128cc5a0-9a73-4cb8-896b-7d1f8e9fb5f3"), "Burger", "Delicious burger", - new List { "Food" }, 7.99), - new Product(Guid.Parse("a028d630-2da8-432d-ad8c-b4990d288841"), "Pizza", "Tasty pizza", - new List { "Food" }, 9.99), - }; + private readonly List _products = InMemoryProducts.Products; public InMemoryProductsRepository(ILogger logger) { diff --git a/src/ProductsService/Migrations/IMigration.cs b/src/ProductsService/Migrations/IMigration.cs index 21eaf1d..37865df 100644 --- a/src/ProductsService/Migrations/IMigration.cs +++ b/src/ProductsService/Migrations/IMigration.cs @@ -5,6 +5,6 @@ namespace ProductsService.Migrations; public interface IMigration { int Version { get; } - string Script {get;} - void PostMigrate(SqlConnection con); + string Script { get; } + void PostMigrate(SqlConnection connection, SqlTransaction transaction); } diff --git a/src/ProductsService/Migrations/InitialMigration.cs b/src/ProductsService/Migrations/InitialMigration.cs index 692cb16..f210f25 100644 --- a/src/ProductsService/Migrations/InitialMigration.cs +++ b/src/ProductsService/Migrations/InitialMigration.cs @@ -22,33 +22,24 @@ Tags VARCHAR(255) NOT NULL, Price DECIMAL(2) );"; - public async void PostMigrate(SqlConnection con) + public void PostMigrate(SqlConnection connection, SqlTransaction transaction) { var products = new List { new Product (Guid.NewGuid(), "Beer", "Tasty craft beer", new List { "Drinks", "Food" }, 3.99), new Product (Guid.NewGuid(), "Whisky", "Gentle drink for cold evenings", new List { "Drinks", "Food" }, 49.99), new Product (Guid.NewGuid(), "Bacon Burger", "Everything is better with bacon", new List { "Food" }, 8.99), }; - var tx = con.BeginTransaction(); - try + + products.ForEach(p => { - products.ForEach(p => - { - var cmd = new SqlCommand("INSERT INTO Products (Name, Description, Tags, Price) VALUES (@Name, @Description, @Tags, @Price)"); - cmd.Connection = con; - cmd.Transaction = tx; - cmd.Parameters.AddWithValue("@Name", p.Name); - cmd.Parameters.AddWithValue("@Description", p.Description); - cmd.Parameters.AddWithValue("@Tags", string.Join(',', p.Categories)); - cmd.Parameters.AddWithValue("@Price", p.Price); - cmd.ExecuteNonQuery(); - }); - tx.Commit(); - } - catch - { - tx.Rollback(); - throw; - } + var cmd = new SqlCommand("INSERT INTO Products (Name, Description, Tags, Price) VALUES (@Name, @Description, @Tags, @Price)"); + cmd.Connection = connection; + cmd.Transaction = transaction; + cmd.Parameters.AddWithValue("@Name", p.Name); + cmd.Parameters.AddWithValue("@Description", p.Description); + cmd.Parameters.AddWithValue("@Tags", string.Join(',', p.Categories)); + cmd.Parameters.AddWithValue("@Price", p.Price); + cmd.ExecuteNonQuery(); + }); } } diff --git a/src/ProductsService/Migrations/Migrations.cs b/src/ProductsService/Migrations/Migrations.cs index 8b89afc..d254afb 100644 --- a/src/ProductsService/Migrations/Migrations.cs +++ b/src/ProductsService/Migrations/Migrations.cs @@ -36,9 +36,8 @@ public void Migrate() con.Open(); var currentVersion = GetCurrentDatabaseVersion(con); - _FindMigrations(currentVersion).ForEach(async m => { + _FindMigrations(currentVersion).ForEach(m => { ExecuteMigration(con, m); - m.PostMigrate(con); }); } catch (Exception) @@ -48,7 +47,7 @@ public void Migrate() } - private int GetCurrentDatabaseVersion(SqlConnection con) + private static int GetCurrentDatabaseVersion(SqlConnection con) { using var cmd = new SqlCommand("SELECT Version FROM DatabaseVersion"); cmd.Connection = con; @@ -65,30 +64,41 @@ private int GetCurrentDatabaseVersion(SqlConnection con) { return 0; } - } - private void ExecuteMigration(SqlConnection con, IMigration m) + private void ExecuteMigration(SqlConnection connection, IMigration migration) { - using var tx = con.BeginTransaction(); - using var cmd = new SqlCommand(m.Script); - cmd.Connection = con; - cmd.Transaction = tx; - cmd.ExecuteNonQuery(); - using var updateVersionCmd = new SqlCommand("UPDATE DatabaseVersion SET Version = @Version"); - updateVersionCmd.Connection = con; - updateVersionCmd.Transaction = tx; - updateVersionCmd.Parameters.AddWithValue("@Version", m.Version); - updateVersionCmd.ExecuteNonQuery(); + SqlTransaction? transaction = null; try { - tx.Commit(); + transaction = connection.BeginTransaction(); + if (!string.IsNullOrWhiteSpace(migration.Script)) + { + using var cmd = new SqlCommand(migration.Script); + cmd.Connection = connection; + cmd.Transaction = transaction; + cmd.ExecuteNonQuery(); + } + + migration.PostMigrate(connection, transaction); + + using var updateVersionCmd = new SqlCommand("UPDATE DatabaseVersion SET Version = @Version"); + updateVersionCmd.Connection = connection; + updateVersionCmd.Transaction = transaction; + updateVersionCmd.Parameters.AddWithValue("@Version", migration.Version); + updateVersionCmd.ExecuteNonQuery(); + + transaction.Commit(); } - catch (Exception) + catch (Exception exception) { - tx.Rollback(); + _logger.LogError(exception, "Could not apply migration {MigrationId}", migration.Version); + transaction?.Rollback(); throw; - + } + finally + { + transaction?.Dispose(); } } } diff --git a/src/ProductsService/Migrations/ProductsAlignmentMigration.cs b/src/ProductsService/Migrations/ProductsAlignmentMigration.cs new file mode 100644 index 0000000..db93eae --- /dev/null +++ b/src/ProductsService/Migrations/ProductsAlignmentMigration.cs @@ -0,0 +1,40 @@ +using System.Data; +using System.Data.SqlClient; +using ProductsService.Data; + +namespace ProductsService.Migrations; + +// This migration imports the same products into the database +// that are used in in-memory scenarios. This way the price watcher +// service and the product service will use the same product IDs. +// The best solution would be to get the products in the price watcher +// service instead of having its own in-memory collection. +public sealed class ProductsAlignmentMigration : IMigration +{ + public int Version => 2; + public string Script => "DELETE FROM Products;"; + + public void PostMigrate(SqlConnection connection, SqlTransaction transaction) + { + var products = InMemoryProducts.Products; + foreach (var product in products) + { + using var command = new SqlCommand( + """ + INSERT INTO Products (Id, Name, Description, Tags, Price) + VALUES (@Id, @Name, @Description, @Tags, @Price); + """, + connection, + transaction + ); + + command.Parameters.Add("@Id", SqlDbType.UniqueIdentifier).Value = product.Id; + command.Parameters.Add("@Name", SqlDbType.VarChar).Value = product.Name; + command.Parameters.Add("@Description", SqlDbType.VarChar).Value = product.Description; + command.Parameters.Add("@Tags", SqlDbType.VarChar).Value = string.Join(',', product.Categories); + command.Parameters.Add("@Price", SqlDbType.Decimal).Value = product.Price; + + command.ExecuteNonQuery(); + } + } +} diff --git a/src/ProductsService/ProductsService.csproj b/src/ProductsService/ProductsService.csproj index baf2aba..1a60e69 100644 --- a/src/ProductsService/ProductsService.csproj +++ b/src/ProductsService/ProductsService.csproj @@ -21,15 +21,5 @@ - - - - - - - - - - - +