Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions sample/AspireDatabaseInstaller.AppHost/.aspire/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"appHostPath": "../AspireDatabaseInstaller.AppHost.csproj"
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">

<Sdk Name="Aspire.AppHost.Sdk" Version="9.1.0" />
<Sdk Name="Aspire.AppHost.Sdk" Version="9.5.1" />

<PropertyGroup>
<OutputType>Exe</OutputType>
Expand All @@ -12,8 +12,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.2.0" />
<PackageReference Include="Aspire.Hosting.SqlServer" Version="9.2.0" />
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.5.1" />
<PackageReference Include="Aspire.Hosting.SqlServer" Version="9.5.1" />
</ItemGroup>

<ItemGroup>
Expand Down
1 change: 0 additions & 1 deletion sample/AspireDatabaseInstaller.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

var builder = DistributedApplication.CreateBuilder(args);


var sqlServer = builder.AddSqlServer("TestDb")
.WithDataVolume()
.WithLifetime(ContainerLifetime.Persistent);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.9" />
</ItemGroup>

<ItemGroup>
Expand Down
104 changes: 101 additions & 3 deletions sample/InstallationSampleConsoleApp/Program.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,122 @@
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
{
class Program
{
static Task Main(string[] args)
{
return InstallerHost.Start<InstallerStartup>();
var installerHostBuilder = InstallerHost.CreateBuilder();

installerHostBuilder.AddServices(services =>
{
// Add application specific services here
services.AddSingleton<Dependency>();
});

installerHostBuilder.AddDatabaseSetup<DatabaseSetup>();

installerHostBuilder.AddDataSeed<DataSeed>();

return installerHostBuilder.Start();
}
}

public class InstallerStartup : IInstallerStartup
public class DatabaseSetup : IDatabaseSetup
{
public void DatabaseVersionsToInstall(List<DatabaseVersion> databaseVersions, IConfiguration configuration)
{
databaseVersions.Add(new SetDatabaseSettingsVersion(configuration));
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;
}
}
13 changes: 13 additions & 0 deletions src/Rinsen.DatabaseInstaller/IDataSeed.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Rinsen.DatabaseInstaller
{
public interface IInstallerStartup
public interface IDatabaseSetup
{
void DatabaseVersionsToInstall(List<DatabaseVersion> databaseVersions, IConfiguration configuration);
}
Expand Down
139 changes: 110 additions & 29 deletions src/Rinsen.DatabaseInstaller/InstallationProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,39 +10,45 @@ namespace Rinsen.DatabaseInstaller
{
internal class InstallationProgram
{
/// <summary>
/// Database installer host.
///
/// This can be used to create databases, db users and schemas from c# fluent code definitions.
/// <para>
/// 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.
/// </para>
/// </summary>
/// <typeparam name="T">Installation assembly type</typeparam>
/// <returns>Task.</returns>
public static async Task StartDatabaseInstaller<T>() where T : class, IInstallerStartup, new()
private static Action<IServiceCollection>? _addServices;
private static Type? _databaseSetupType;
private static Type? _dataSeedType;

internal static async Task StartDatabaseInstaller()
{
var databaseVersionsToInstall = new List<DatabaseVersion>();

var serviceProvider = BootstrapApplication<T>();
var serviceProvider = BootstrapApplication();

var installerstartup = serviceProvider.GetRequiredService<T>();
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
var logger = serviceProvider.GetRequiredService<ILogger<InstallationProgram>>();

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<bool> InstallDatabase(List<DatabaseVersion> databaseVersionsToInstall, ServiceProvider serviceProvider, IConfiguration configuration, ILogger<InstallationProgram> 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<InstallationHandler>();
var logger = serviceProvider.GetService<ILogger<InstallationProgram>>();

var installationHandler = serviceProvider.GetRequiredService<InstallationHandler>();
try
{
if (!IsConfigurationValid(logger, configuration))
{
return;
return false;
}

switch (configuration["Command"])
Expand All @@ -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<InstallationProgram> logger, IConfiguration configuration)
Expand All @@ -92,13 +100,14 @@ private static bool IsConfigurationValid(ILogger<InstallationProgram> 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;
Expand All @@ -107,7 +116,29 @@ private static bool IsConfigurationValid(ILogger<InstallationProgram> logger, IC
return true;
}

private static ServiceProvider BootstrapApplication<T>() where T : class
private static async Task SeedData(ServiceProvider serviceProvider, IConfiguration configuration, ILogger<InstallationProgram> 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
Expand All @@ -132,18 +163,68 @@ private static ServiceProvider BootstrapApplication<T>() where T : class
serviceCollection.AddSingleton<IConfiguration>(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<T>();
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<IServiceCollection> value)
{
_addServices = value;
}

internal static void AddDatabaseSetup<T>() where T : class, IDatabaseSetup, new()
{
_databaseSetupType = typeof(T);
}

internal static void AddDataSeed<T>() where T : class, IDataSeed
{
_dataSeedType = typeof(T);
}
}
}
Loading
Loading