diff --git a/sample/AspireDatabaseInstaller.AppHost/.aspire/settings.json b/sample/AspireDatabaseInstaller.AppHost/.aspire/settings.json new file mode 100644 index 0000000..2c9df65 --- /dev/null +++ b/sample/AspireDatabaseInstaller.AppHost/.aspire/settings.json @@ -0,0 +1,3 @@ +{ + "appHostPath": "../AspireDatabaseInstaller.AppHost.csproj" +} \ No newline at end of file diff --git a/sample/AspireDatabaseInstaller.AppHost/AspireDatabaseInstaller.AppHost.csproj b/sample/AspireDatabaseInstaller.AppHost/AspireDatabaseInstaller.AppHost.csproj index 5e7ea3c..d164b66 100644 --- a/sample/AspireDatabaseInstaller.AppHost/AspireDatabaseInstaller.AppHost.csproj +++ b/sample/AspireDatabaseInstaller.AppHost/AspireDatabaseInstaller.AppHost.csproj @@ -1,6 +1,6 @@ - + Exe @@ -12,8 +12,8 @@ - - + + diff --git a/sample/AspireDatabaseInstaller.AppHost/Program.cs b/sample/AspireDatabaseInstaller.AppHost/Program.cs index 559b116..140f68b 100644 --- a/sample/AspireDatabaseInstaller.AppHost/Program.cs +++ b/sample/AspireDatabaseInstaller.AppHost/Program.cs @@ -2,7 +2,6 @@ var builder = DistributedApplication.CreateBuilder(args); - var sqlServer = builder.AddSqlServer("TestDb") .WithDataVolume() .WithLifetime(ContainerLifetime.Persistent); diff --git a/sample/InstallationSampleConsoleApp/InstallationSampleConsoleApp.csproj b/sample/InstallationSampleConsoleApp/InstallationSampleConsoleApp.csproj index e57c15c..e6a4059 100644 --- a/sample/InstallationSampleConsoleApp/InstallationSampleConsoleApp.csproj +++ b/sample/InstallationSampleConsoleApp/InstallationSampleConsoleApp.csproj @@ -13,7 +13,7 @@ - + diff --git a/sample/InstallationSampleConsoleApp/Program.cs b/sample/InstallationSampleConsoleApp/Program.cs index e96daaf..61e336d 100644 --- a/sample/InstallationSampleConsoleApp/Program.cs +++ b/sample/InstallationSampleConsoleApp/Program.cs @@ -1,7 +1,11 @@ -using Microsoft.Extensions.Configuration; +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Rinsen.DatabaseInstaller; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Data.SqlClient; +using System.Data; namespace InstallationSampleConsoleApp { @@ -9,11 +13,23 @@ class Program { static Task Main(string[] args) { - return InstallerHost.Start(); + var installerHostBuilder = InstallerHost.CreateBuilder(); + + installerHostBuilder.AddServices(services => + { + // Add application specific services here + services.AddSingleton(); + }); + + installerHostBuilder.AddDatabaseSetup(); + + installerHostBuilder.AddDataSeed(); + + return installerHostBuilder.Start(); } } - public class InstallerStartup : IInstallerStartup + public class DatabaseSetup : IDatabaseSetup { public void DatabaseVersionsToInstall(List databaseVersions, IConfiguration configuration) { @@ -21,4 +37,86 @@ public void DatabaseVersionsToInstall(List databaseVersions, IC databaseVersions.Add(new CreateTables()); } } + + public class DataSeed : IDataSeed + { + private readonly InstallerOptions _installerOptions; + private readonly Dependency _dependency; + + public DataSeed(InstallerOptions installerOptions, + Dependency dependency) + { + _installerOptions = installerOptions; + _dependency = dependency; + } + + public async Task SeedData() + { + Console.WriteLine("Seeding data..."); + + using var connection = new SqlConnection(_installerOptions.ConnectionString); + await connection.OpenAsync(); + + // Check if data already exists to avoid duplicate seeding + var checkQuery = $"SELECT COUNT(*) FROM [{_installerOptions.DatabaseName}].[{_installerOptions.Schema}].[NullableDatas]"; + using var checkCommand = new SqlCommand(checkQuery, connection); + var existingRowCount = (int)await checkCommand.ExecuteScalarAsync(); + + if (existingRowCount >= _dependency.CreateCount) + { + Console.WriteLine($"Data already exists ({existingRowCount} rows). Skipping seeding."); + return; + } + + // Seed 10 rows of data + var insertQuery = $@" + INSERT INTO [{_installerOptions.DatabaseName}].[{_installerOptions.Schema}].[NullableDatas] + (NotNullableBool, NullableBool, NullableByte, NullableByteArray, NullableDateTime, + NullableDateTimeOffset, NullableDecimal, NullableDouble, NullableGuid, NullableInt, + NullableLong, NullableShort) + VALUES + (@NotNullableBool, @NullableBool, @NullableByte, @NullableByteArray, @NullableDateTime, + @NullableDateTimeOffset, @NullableDecimal, @NullableDouble, @NullableGuid, @NullableInt, + @NullableLong, @NullableShort)"; + + var count = _dependency.CreateCount - existingRowCount; + for (int i = 1; i <= count; i++) + { + using var insertCommand = new SqlCommand(insertQuery, connection); + + // Add parameters with varied data including some nulls + insertCommand.Parameters.AddWithValue("@NotNullableBool", i % 2 == 0); + insertCommand.Parameters.AddWithValue("@NullableBool", i % 3 == 0 ? DBNull.Value : (object)(i % 2 == 0)); + insertCommand.Parameters.AddWithValue("@NullableByte", i % 4 == 0 ? DBNull.Value : (object)(byte)(i * 10)); + + // Handle byte array properly for varbinary column + if (i % 5 == 0) + { + insertCommand.Parameters.Add("@NullableByteArray", System.Data.SqlDbType.VarBinary).Value = DBNull.Value; + } + else + { + insertCommand.Parameters.Add("@NullableByteArray", System.Data.SqlDbType.VarBinary).Value = new byte[] { (byte)i, (byte)(i * 2) }; + } + + insertCommand.Parameters.AddWithValue("@NullableDateTime", i % 6 == 0 ? DBNull.Value : (object)DateTime.Now.AddDays(i)); + insertCommand.Parameters.AddWithValue("@NullableDateTimeOffset", i % 7 == 0 ? DBNull.Value : (object)DateTimeOffset.Now.AddHours(i)); + insertCommand.Parameters.AddWithValue("@NullableDecimal", i % 8 == 0 ? DBNull.Value : (object)(decimal)(i * 100.50m)); + insertCommand.Parameters.AddWithValue("@NullableDouble", i % 9 == 0 ? DBNull.Value : (object)(double)(i * 3.14)); + insertCommand.Parameters.AddWithValue("@NullableGuid", i % 10 == 0 ? DBNull.Value : (object)Guid.NewGuid()); + insertCommand.Parameters.AddWithValue("@NullableInt", i % 2 == 0 ? DBNull.Value : (object)(i * 1000)); + insertCommand.Parameters.AddWithValue("@NullableLong", i % 3 == 0 ? DBNull.Value : (object)((long)i * 1000000)); + insertCommand.Parameters.AddWithValue("@NullableShort", i % 4 == 0 ? DBNull.Value : (object)(short)(i * 10)); + + await insertCommand.ExecuteNonQueryAsync(); + } + + Console.WriteLine($"Successfully seeded {count} rows of data into NullableDatas table."); + } + } + + public class Dependency + { + public int CreateCount { get; set; } = 20; + } } diff --git a/src/Rinsen.DatabaseInstaller/IDataSeed.cs b/src/Rinsen.DatabaseInstaller/IDataSeed.cs new file mode 100644 index 0000000..47a6872 --- /dev/null +++ b/src/Rinsen.DatabaseInstaller/IDataSeed.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Rinsen.DatabaseInstaller +{ + public interface IDataSeed + { + Task SeedData(); + } +} diff --git a/src/Rinsen.DatabaseInstaller/IInstallerStartup.cs b/src/Rinsen.DatabaseInstaller/IDatabaseSetup.cs similarity index 86% rename from src/Rinsen.DatabaseInstaller/IInstallerStartup.cs rename to src/Rinsen.DatabaseInstaller/IDatabaseSetup.cs index eaa48a0..1ff37b8 100644 --- a/src/Rinsen.DatabaseInstaller/IInstallerStartup.cs +++ b/src/Rinsen.DatabaseInstaller/IDatabaseSetup.cs @@ -3,7 +3,7 @@ namespace Rinsen.DatabaseInstaller { - public interface IInstallerStartup + public interface IDatabaseSetup { void DatabaseVersionsToInstall(List databaseVersions, IConfiguration configuration); } diff --git a/src/Rinsen.DatabaseInstaller/InstallationProgram.cs b/src/Rinsen.DatabaseInstaller/InstallationProgram.cs index a99e652..3110e12 100644 --- a/src/Rinsen.DatabaseInstaller/InstallationProgram.cs +++ b/src/Rinsen.DatabaseInstaller/InstallationProgram.cs @@ -10,39 +10,45 @@ namespace Rinsen.DatabaseInstaller { internal class InstallationProgram { - /// - /// Database installer host. - /// - /// This can be used to create databases, db users and schemas from c# fluent code definitions. - /// - /// Requires configuration for the following settings to work: - /// * Command: Install, Preview, ShowAll, CurrentState - /// * DatabaseName: Name of the database to install. - /// * Schema: Name of the schema to install. - /// * ConnectionString: Connection string to the database server. - /// - /// - /// Installation assembly type - /// Task. - public static async Task StartDatabaseInstaller() where T : class, IInstallerStartup, new() + private static Action? _addServices; + private static Type? _databaseSetupType; + private static Type? _dataSeedType; + + internal static async Task StartDatabaseInstaller() { var databaseVersionsToInstall = new List(); - var serviceProvider = BootstrapApplication(); + var serviceProvider = BootstrapApplication(); - var installerstartup = serviceProvider.GetRequiredService(); var configuration = serviceProvider.GetRequiredService(); + var logger = serviceProvider.GetRequiredService>(); + + var completed = await InstallDatabase(databaseVersionsToInstall, serviceProvider, configuration, logger); + + if (completed && _dataSeedType is not null) + { + await SeedData(serviceProvider, configuration, logger); + } + + logger.LogInformation($"Done"); + } + private static async Task InstallDatabase(List databaseVersionsToInstall, ServiceProvider serviceProvider, IConfiguration configuration, ILogger logger) + { + if (_databaseSetupType == null) + { + throw new InvalidOperationException("Database setup type not configured. Call AddDatabaseSetup() first."); + } + + var installerstartup = (IDatabaseSetup)serviceProvider.GetRequiredService(_databaseSetupType); installerstartup.DatabaseVersionsToInstall(databaseVersionsToInstall, configuration); - var installationHandler = serviceProvider.GetService(); - var logger = serviceProvider.GetService>(); - + var installationHandler = serviceProvider.GetRequiredService(); try { if (!IsConfigurationValid(logger, configuration)) { - return; + return false; } switch (configuration["Command"]) @@ -67,9 +73,11 @@ internal class InstallationProgram catch (Exception e) { logger.LogError(e, "Failed to run installer"); + + return false; } - logger.LogInformation($"Done"); + return true; } private static bool IsConfigurationValid(ILogger logger, IConfiguration configuration) @@ -92,13 +100,14 @@ private static bool IsConfigurationValid(ILogger logger, IC return false; } - if (string.IsNullOrEmpty(configuration["ConnectionStringName"])) + var connectionStringName = configuration["ConnectionStringName"]; + if (string.IsNullOrEmpty(connectionStringName)) { logger.LogError("ConnectionStringName is required"); return false; } - if (string.IsNullOrEmpty(configuration.GetConnectionString(configuration["ConnectionStringName"]))) + if (string.IsNullOrEmpty(configuration.GetConnectionString(connectionStringName))) { logger.LogError("ConnectionString is required"); return false; @@ -107,7 +116,29 @@ private static bool IsConfigurationValid(ILogger logger, IC return true; } - private static ServiceProvider BootstrapApplication() where T : class + private static async Task SeedData(ServiceProvider serviceProvider, IConfiguration configuration, ILogger logger) + { + if (_dataSeedType == null) + { + logger.LogWarning("Data seed type is not configured"); + return; + } + + try + { + var dataSeed = (IDataSeed)serviceProvider.GetRequiredService(_dataSeedType); + logger.LogInformation("Starting data seeding"); + await dataSeed.SeedData(); + logger.LogInformation("Data seeding completed successfully"); + } + catch (Exception e) + { + logger.LogError(e, "Failed to seed data"); + throw; + } + } + + private static ServiceProvider BootstrapApplication() { var environmentName = "Production"; #if DEBUG @@ -132,18 +163,68 @@ private static ServiceProvider BootstrapApplication() where T : class serviceCollection.AddSingleton(config); var connectionStringName = config["ConnectionStringName"]; + if (string.IsNullOrEmpty(connectionStringName)) + { + throw new InvalidOperationException("ConnectionStringName is required in configuration"); + } + + var connectionString = config.GetConnectionString(connectionStringName); + if (string.IsNullOrEmpty(connectionString)) + { + throw new InvalidOperationException($"Connection string '{connectionStringName}' not found in configuration"); + } + + var databaseName = config["DatabaseName"]; + if (string.IsNullOrEmpty(databaseName)) + { + throw new InvalidOperationException("DatabaseName is required in configuration"); + } + + var schema = config["Schema"]; + if (string.IsNullOrEmpty(schema)) + { + throw new InvalidOperationException("Schema is required in configuration"); + } + serviceCollection.AddSingleton(new InstallerOptions { ConnectionStringName = connectionStringName, - ConnectionString = config.GetConnectionString(connectionStringName), - DatabaseName = config["DatabaseName"], - Schema = config["Schema"] + ConnectionString = connectionString, + DatabaseName = databaseName, + Schema = schema }); - serviceCollection.AddTransient(); + if (_databaseSetupType != null) + { + serviceCollection.AddTransient(_databaseSetupType); + } + + if (_dataSeedType != null) + { + serviceCollection.AddTransient(_dataSeedType); + } + serviceCollection.AddDatabaseInstaller(); + // Add custom services if configured + _addServices?.Invoke(serviceCollection); + return serviceCollection.BuildServiceProvider(); } + + internal static void AddServices(Action value) + { + _addServices = value; + } + + internal static void AddDatabaseSetup() where T : class, IDatabaseSetup, new() + { + _databaseSetupType = typeof(T); + } + + internal static void AddDataSeed() where T : class, IDataSeed + { + _dataSeedType = typeof(T); + } } } diff --git a/src/Rinsen.DatabaseInstaller/InstallerHost.cs b/src/Rinsen.DatabaseInstaller/InstallerHost.cs index b7a6def..0d453b4 100644 --- a/src/Rinsen.DatabaseInstaller/InstallerHost.cs +++ b/src/Rinsen.DatabaseInstaller/InstallerHost.cs @@ -4,23 +4,16 @@ namespace Rinsen.DatabaseInstaller { public class InstallerHost { + /// - /// Database installer host. - /// - /// This can be used to create databases, db users and schemas from c# fluent code definitions. - /// - /// Requires configuration for the following settings to work: - /// * Command: Install, Preview, ShowAll, CurrentState - /// * DatabaseName: Name of the database to install. - /// * Schema: Name of the schema to install. - /// * ConnectionString: Connection string to the database server. - /// + /// Creates a new instance of an installer host builder for configuring and constructing installer hosts. /// - /// Installation assembly type - /// Task. - public static Task Start() where T : class, IInstallerStartup, new() + /// An instance that can be used to configure and build an installer host. + public static IInstallerHostBuilder CreateBuilder() { - return InstallationProgram.StartDatabaseInstaller(); + return new InstallerHostBuilder(); } + + } } diff --git a/src/Rinsen.DatabaseInstaller/InstallerHostBuilder.cs b/src/Rinsen.DatabaseInstaller/InstallerHostBuilder.cs new file mode 100644 index 0000000..6367e6d --- /dev/null +++ b/src/Rinsen.DatabaseInstaller/InstallerHostBuilder.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace Rinsen.DatabaseInstaller +{ + public interface IInstallerHostBuilder + { + /// + /// Registers a database setup of the specified type for use in the installer host. + /// + /// Use this method to add custom database setup logic by providing a type that + /// implements . Only one type can be registered. + /// The type of database setup to register. Must be a non-abstract class that implements . + void AddDatabaseSetup() where T : class, IDatabaseSetup, new(); + + /// + /// Registers a data seed of the specified type for initialization during application startup. + /// + /// Use this method to add custom data seeding logic to the application's startup + /// process. + /// The type of data seed to register. Must be a non-abstract class that implements . + void AddDataSeed() where T : class, IDataSeed; + + /// + /// Configures additional services by invoking the specified delegate on the service collection. + /// + /// Use this method to register custom services or modify existing service registrations + /// before the service provider is built. The delegate is called with the current service collection, allowing + /// for flexible configuration. + /// A delegate that receives the to which services can be added or configured. + void AddServices(Action serviceCollection); + + /// + /// Database installer host. + /// + /// This can be used to create databases, db users and schemas from c# fluent code definitions. + /// + /// Requires configuration for the following settings to work: + /// * Command: Install, Preview, ShowAll, CurrentState + /// * DatabaseName: Name of the database to install. + /// * Schema: Name of the schema to install. + /// * ConnectionString: Connection string to the database server. + /// + /// + /// Task. + Task Start(); + } + + public class InstallerHostBuilder : IInstallerHostBuilder + { + public InstallerHostBuilder() + { + } + + + public void AddServices(Action serviceCollection) + { + InstallationProgram.AddServices(serviceCollection); + } + + /// > + public Task Start() + { + return InstallationProgram.StartDatabaseInstaller(); + } + + public void AddDatabaseSetup() where T : class, IDatabaseSetup, new() + { + InstallationProgram.AddDatabaseSetup(); + } + + public void AddDataSeed() where T : class, IDataSeed + { + InstallationProgram.AddDataSeed(); + } + } +} diff --git a/src/Rinsen.DatabaseInstaller/Rinsen.DatabaseInstaller.csproj b/src/Rinsen.DatabaseInstaller/Rinsen.DatabaseInstaller.csproj index ecca19d..1245671 100644 --- a/src/Rinsen.DatabaseInstaller/Rinsen.DatabaseInstaller.csproj +++ b/src/Rinsen.DatabaseInstaller/Rinsen.DatabaseInstaller.csproj @@ -2,6 +2,7 @@ net9.0 + enable 0.7.0 Rinsen Database Installer Fredrik Rinsén @@ -18,20 +19,20 @@ - + - - - - - - - - - - + + + + + + + + + + diff --git a/test/Rinsen.DatabaseInstaller.Tests/Rinsen.DatabaseInstaller.Tests.csproj b/test/Rinsen.DatabaseInstaller.Tests/Rinsen.DatabaseInstaller.Tests.csproj index 3571ec3..8e142e9 100644 --- a/test/Rinsen.DatabaseInstaller.Tests/Rinsen.DatabaseInstaller.Tests.csproj +++ b/test/Rinsen.DatabaseInstaller.Tests/Rinsen.DatabaseInstaller.Tests.csproj @@ -11,8 +11,8 @@ - - + + all runtime; build; native; contentfiles; analyzers