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