diff --git a/src/Rinsen.DatabaseInstaller/AddValue.cs b/src/Rinsen.DatabaseInstaller/AddValue.cs index 37c2050..e28ec58 100644 --- a/src/Rinsen.DatabaseInstaller/AddValue.cs +++ b/src/Rinsen.DatabaseInstaller/AddValue.cs @@ -7,17 +7,24 @@ public class AddValue : IDbChange { public string TableName { get; } - public string ColumnName { get; set; } + public string ColumnName { get; set; } = string.Empty; public AddValue(string tableName) { + ArgumentNullException.ThrowIfNull(tableName); + TableName = tableName; - } + } public IReadOnlyList GetUpScript(InstallerOptions installerOptions) { - return new List { $"UPDATE [{installerOptions.DatabaseName}].[{installerOptions.Schema}].[{TableName}]{Environment.NewLine}SET {ColumnName} = NEWID(){Environment.NewLine}WHERE {ColumnName} is NULL" }; + if (string.IsNullOrEmpty(ColumnName)) + { + throw new NotSupportedException("Empty column name is not supported."); + } + + return [$"UPDATE [{installerOptions.DatabaseName}].[{installerOptions.Schema}].[{TableName}]{Environment.NewLine}SET {ColumnName} = NEWID(){Environment.NewLine}WHERE {ColumnName} is NULL"]; } public void GuidColumn(string columnName) diff --git a/src/Rinsen.DatabaseInstaller/AdoNetVersionStorage.cs b/src/Rinsen.DatabaseInstaller/AdoNetVersionStorage.cs index d6c95a1..8aa16d1 100644 --- a/src/Rinsen.DatabaseInstaller/AdoNetVersionStorage.cs +++ b/src/Rinsen.DatabaseInstaller/AdoNetVersionStorage.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using Microsoft.Data.SqlClient; using System.Threading.Tasks; -using System.Transactions; namespace Rinsen.DatabaseInstaller { @@ -25,10 +24,11 @@ public async Task CreateAsync(InstallationNameAndVersion installedNameAndVersion command.Parameters.Add(new SqlParameter("@PreviousVersion", installedNameAndVersion.PreviousVersion)); command.Parameters.Add(new SqlParameter("@StartedInstallingVersion", installedNameAndVersion.StartedInstallingVersion)); - installedNameAndVersion.Id = (int)await command.ExecuteScalarAsync(); + var result = await command.ExecuteScalarAsync(); + installedNameAndVersion.Id = result is int id ? id : throw new InvalidOperationException("Failed to insert and retrieve identity value."); } - public async Task GetAsync(string name, SqlConnection connection, SqlTransaction transaction) + public async Task GetAsync(string name, SqlConnection connection, SqlTransaction transaction) { var result = default(InstallationNameAndVersion); diff --git a/src/Rinsen.DatabaseInstaller/Column.cs b/src/Rinsen.DatabaseInstaller/Column.cs index 7d41cec..1c131e1 100644 --- a/src/Rinsen.DatabaseInstaller/Column.cs +++ b/src/Rinsen.DatabaseInstaller/Column.cs @@ -22,12 +22,12 @@ internal Column(string name, IDbType dbType) public bool PrimaryKey { get; internal set; } = false; - public ForeignKey ForeignKey { get; internal set; } = null; + public ForeignKey? ForeignKey { get; internal set; } = null; - public Check Check { get; internal set; } = null; + public Check? Check { get; internal set; } = null; - public DefaultValue DefaultValue { get; internal set; } = null; + public DefaultValue? DefaultValue { get; internal set; } = null; - public AutoIncrement AutoIncrement { get; internal set; } = null; + public AutoIncrement? AutoIncrement { get; internal set; } = null; } } diff --git a/src/Rinsen.DatabaseInstaller/ColumnBuilder.cs b/src/Rinsen.DatabaseInstaller/ColumnBuilder.cs index 1dfa60d..003df6e 100644 --- a/src/Rinsen.DatabaseInstaller/ColumnBuilder.cs +++ b/src/Rinsen.DatabaseInstaller/ColumnBuilder.cs @@ -7,7 +7,7 @@ namespace Rinsen.DatabaseInstaller { public class ColumnBuilder { - public Column Column { get; } + public Column? Column { get; } private readonly Table _table; @@ -32,18 +32,27 @@ public ColumnBuilder ForeignKey(Expression> propertyExpressio public ColumnBuilder NotNull() { + if (Column == null) + throw new InvalidOperationException("Column is not initialized. Use a constructor that creates a column or add a column first."); + Column.Null = false; return this; } public ColumnBuilder Null() { + if (Column == null) + throw new InvalidOperationException("Column is not initialized. Use a constructor that creates a column or add a column first."); + Column.Null = true; return this; } public ColumnBuilder Unique() { + if (Column == null) + throw new InvalidOperationException("Column is not initialized. Use a constructor that creates a column or add a column first."); + if (Column.PrimaryKey) { throw new InvalidOperationException("A unique constraint can not be combined with primary key"); @@ -55,6 +64,9 @@ public ColumnBuilder Unique() public ColumnBuilder Clustered() { + if (Column == null) + throw new InvalidOperationException("Column is not initialized. Use a constructor that creates a column or add a column first."); + if (_table.PrimaryKeyClustered) { throw new InvalidOperationException("A clustered column can only be added if the primary key is not clustered"); @@ -66,6 +78,9 @@ public ColumnBuilder Clustered() public ColumnBuilder Unique(string name) { + if (Column == null) + throw new InvalidOperationException("Column is not initialized. Use a constructor that creates a column or add a column first."); + if (Column.PrimaryKey || _table.NamedPrimaryKeys.Any(m => m.Key == Column.Name)) { throw new InvalidOperationException("A unique constraint can not be combined with a primary key on the same column"); @@ -77,6 +92,9 @@ public ColumnBuilder Unique(string name) public ColumnBuilder PrimaryKey() { + if (Column == null) + throw new InvalidOperationException("Column is not initialized. Use a constructor that creates a column or add a column first."); + if (Column.Unique || _table.NamedUniques.Any(m => m.Key == Column.Name)) { throw new InvalidOperationException("A primary key can not be combined with a unique constraint on the same column"); @@ -109,8 +127,11 @@ private void AddAnyExistingPrimaryKeysToNamedPrimaryKeys(string constraintName) public ColumnBuilder PrimaryKey(string name) { + if (Column == null) + throw new InvalidOperationException("Column is not initialized. Use a constructor that creates a column or add a column first."); + if (_table.NamedPrimaryKeys.Count > 0 && - !_table.NamedPrimaryKeys.Keys.Contains(name)) + !_table.NamedPrimaryKeys.ContainsKey(name)) { throw new ArgumentException("Ony one named primary key can exist"); } @@ -125,29 +146,44 @@ public ColumnBuilder PrimaryKey(string name) public ColumnBuilder ForeignKey(string tableName) { + if (Column == null) + throw new InvalidOperationException("Column is not initialized. Use a constructor that creates a column or add a column first."); + return ForeignKey(tableName, Column.Name); } public ColumnBuilder ForeignKey(string tableName, string columnName) { + if (Column == null) + throw new InvalidOperationException("Column is not initialized. Use a constructor that creates a column or add a column first."); + Column.ForeignKey = new ForeignKey(tableName, columnName); return this; } public ColumnBuilder Check() { + if (Column == null) + throw new InvalidOperationException("Column is not initialized. Use a constructor that creates a column or add a column first."); + Column.Check = new Check(); return this; } public ColumnBuilder DefaultValue() { + if (Column == null) + throw new InvalidOperationException("Column is not initialized. Use a constructor that creates a column or add a column first."); + Column.DefaultValue = new DefaultValue(); return this; } public ColumnBuilder AutoIncrement(int startValue = 1, int increment = 1, bool primaryKey = true) { + if (Column == null) + throw new InvalidOperationException("Column is not initialized. Use a constructor that creates a column or add a column first."); + if (primaryKey) { PrimaryKey(); @@ -156,4 +192,4 @@ public ColumnBuilder AutoIncrement(int startValue = 1, int increment = 1, bool p return this; } } -} \ No newline at end of file +} diff --git a/src/Rinsen.DatabaseInstaller/DatabaseVersion.cs b/src/Rinsen.DatabaseInstaller/DatabaseVersion.cs index 1ff7302..0a7a5cd 100644 --- a/src/Rinsen.DatabaseInstaller/DatabaseVersion.cs +++ b/src/Rinsen.DatabaseInstaller/DatabaseVersion.cs @@ -5,21 +5,34 @@ namespace Rinsen.DatabaseInstaller { public abstract class DatabaseVersion { + + /// + /// Initializes a new instance of the DatabaseVersion class with the specified version number. This will user the class namespace as installation name. + /// + /// The version number to assign to the database version. Must be a non-negative integer. + protected DatabaseVersion(int version) + : this(version, null) + { + + } + + /// /// Database version description /// /// Version number /// Installation name, if none specified the default will be this class namespace - public DatabaseVersion(int version, string installationName = null) + public DatabaseVersion(int version, string? installationName) { if (string.IsNullOrEmpty(installationName)) { - InstallationName = GetType().Namespace; + InstallationName = GetType().Namespace ?? throw new ArgumentException("Installation name cannot be null or empty"); } else - { + { InstallationName = installationName; } + Version = version; } diff --git a/src/Rinsen.DatabaseInstaller/IVersionStorage.cs b/src/Rinsen.DatabaseInstaller/IVersionStorage.cs index 6e035a4..77d2f17 100644 --- a/src/Rinsen.DatabaseInstaller/IVersionStorage.cs +++ b/src/Rinsen.DatabaseInstaller/IVersionStorage.cs @@ -10,7 +10,7 @@ public interface IVersionStorage Task IsInstalledAsync(SqlConnection connection, SqlTransaction transaction); - Task GetAsync(string name, SqlConnection connection, SqlTransaction transaction); + Task GetAsync(string name, SqlConnection connection, SqlTransaction transaction); Task> GetAllAsync(SqlConnection connection, SqlTransaction transaction); diff --git a/src/Rinsen.DatabaseInstaller/InstallVersionScope.cs b/src/Rinsen.DatabaseInstaller/InstallVersionScope.cs index 81110fb..9a6bf3b 100644 --- a/src/Rinsen.DatabaseInstaller/InstallVersionScope.cs +++ b/src/Rinsen.DatabaseInstaller/InstallVersionScope.cs @@ -11,7 +11,7 @@ internal class InstallVersionScope : IAsyncDisposable private readonly SqlTransaction _transaction; private readonly IVersionStorage _versionStorage; private bool _failed = true; - private Exception _e; + private Exception? _e; public InstallVersionScope(IVersionStorage versionStorage, DatabaseVersion databaseVersion, SqlConnection connection, SqlTransaction transaction) { @@ -42,7 +42,7 @@ public async ValueTask DisposeAsync() } } - private async Task GetCurrentInstalledVersionAndValidatePostInstallationState() + private async Task GetCurrentInstalledVersionAndValidatePostInstallationState() { // Get installation row from database var installedVersion = await _versionStorage.GetAsync(_databaseVersion.InstallationName, _connection, _transaction); diff --git a/src/Rinsen.DatabaseInstaller/InstallationNameAndVersion.cs b/src/Rinsen.DatabaseInstaller/InstallationNameAndVersion.cs index c242968..78a88df 100644 --- a/src/Rinsen.DatabaseInstaller/InstallationNameAndVersion.cs +++ b/src/Rinsen.DatabaseInstaller/InstallationNameAndVersion.cs @@ -4,7 +4,7 @@ public class InstallationNameAndVersion { public int Id { get; set; } - public string InstallationName { get; set; } + public string InstallationName { get; set; } = string.Empty; public int PreviousVersion { get; set; } @@ -12,4 +12,4 @@ public class InstallationNameAndVersion public int InstalledVersion { get; set; } } -} \ No newline at end of file +} diff --git a/src/Rinsen.DatabaseInstaller/InstallationProgram.cs b/src/Rinsen.DatabaseInstaller/InstallationProgram.cs index 3110e12..05bd267 100644 --- a/src/Rinsen.DatabaseInstaller/InstallationProgram.cs +++ b/src/Rinsen.DatabaseInstaller/InstallationProgram.cs @@ -186,13 +186,7 @@ private static ServiceProvider BootstrapApplication() throw new InvalidOperationException("Schema is required in configuration"); } - serviceCollection.AddSingleton(new InstallerOptions - { - ConnectionStringName = connectionStringName, - ConnectionString = connectionString, - DatabaseName = databaseName, - Schema = schema - }); + serviceCollection.AddSingleton(new InstallerOptions(databaseName, schema, connectionString, connectionStringName)); if (_databaseSetupType != null) { diff --git a/src/Rinsen.DatabaseInstaller/InstallerOptions.cs b/src/Rinsen.DatabaseInstaller/InstallerOptions.cs index 0e7b498..543b0af 100644 --- a/src/Rinsen.DatabaseInstaller/InstallerOptions.cs +++ b/src/Rinsen.DatabaseInstaller/InstallerOptions.cs @@ -5,21 +5,29 @@ public class InstallerOptions /// /// The database name to create or update. /// - public string DatabaseName { get; set; } + public string DatabaseName { get; } /// /// The database schema name to create or update. /// - public string Schema { get; set; } + public string Schema { get; } /// /// The connection string to the database server. /// - public string ConnectionString { get; set; } + public string ConnectionString { get; } /// /// The connection string name in configuration to use for the database server. /// - public string ConnectionStringName { get; internal set; } + public string ConnectionStringName { get; } + + public InstallerOptions(string databaseName, string schema, string connectionString, string connectionStringName) + { + DatabaseName = databaseName; + Schema = schema; + ConnectionString = connectionString; + ConnectionStringName = connectionStringName; + } } } diff --git a/src/Rinsen.DatabaseInstaller/SecurityBuilder.cs b/src/Rinsen.DatabaseInstaller/SecurityBuilder.cs index 0e6c99c..b44bbb6 100644 --- a/src/Rinsen.DatabaseInstaller/SecurityBuilder.cs +++ b/src/Rinsen.DatabaseInstaller/SecurityBuilder.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.WebUtilities; using Rinsen.DatabaseInstaller.Internal; +using System; using System.Collections.Generic; using System.Security.Cryptography; @@ -9,14 +10,27 @@ public class SecurityBuilder { private readonly RandomNumberGenerator _cryptoRandom = RandomNumberGenerator.Create(); - - internal SecurityBuilder() + /// + /// Initializes a new instance of the SecurityBuilder class. + /// + public SecurityBuilder() { - CreateNewLogin = false; } + /// + /// Initializes a new instance of the SecurityBuilder class using the specified login name and password. + /// + /// The login name to associate with the new security context. Cannot be null or empty. + /// The password to use for the security context. If null or empty, a random password is generated + /// automatically. + /// Thrown if is null or empty. internal SecurityBuilder(string loginName, string password = "") { + if (string.IsNullOrEmpty(loginName)) + { + throw new ArgumentException("Login name cannot be null or empty.", nameof(loginName)); + } + if (string.IsNullOrEmpty(password)) { Password = GetRandomString(40); @@ -42,16 +56,16 @@ public string LoginName } } - public bool CreateNewLogin { get; } - public string UserName { get; private set; } - public string Password { get; private set; } + public bool CreateNewLogin { get; } = false; + public string UserName { get; private set; } = string.Empty; + public string Password { get; private set; } = string.Empty; public bool CreateNewUser { get; private set; } = false; - private string _loginName; + private string _loginName = string.Empty; public IReadOnlyList RoleMemberships { get { return _roleMembershipsToAdd; } } - private readonly List _roleMembershipsToAdd = new(); + private readonly List _roleMembershipsToAdd = []; private string GetRandomString(int length) { @@ -64,6 +78,11 @@ private string GetRandomString(int length) public SecurityBuilder AddRoleMembershipDataWriter() { + if (string.IsNullOrWhiteSpace(UserName)) + { + throw new InvalidOperationException("UserName cannot be null or empty when adding role membership."); + } + _roleMembershipsToAdd.Add(new RoleMembership("db_datawriter", UserName)); return this; @@ -71,6 +90,11 @@ public SecurityBuilder AddRoleMembershipDataWriter() public SecurityBuilder AddRoleMembershipDataReader() { + if (string.IsNullOrWhiteSpace(UserName)) + { + throw new InvalidOperationException("UserName cannot be null or empty when adding role membership."); + } + _roleMembershipsToAdd.Add(new RoleMembership("db_datareader", UserName)); return this; @@ -78,6 +102,11 @@ public SecurityBuilder AddRoleMembershipDataReader() public SecurityBuilder WithUser(string userName) { + if (string.IsNullOrWhiteSpace(userName)) + { + throw new ArgumentException("User name cannot be null or empty.", nameof(userName)); + } + CreateNewUser = true; UserName = userName; @@ -86,6 +115,11 @@ public SecurityBuilder WithUser(string userName) public SecurityBuilder WithUser() { + if (string.IsNullOrWhiteSpace(LoginName)) + { + throw new InvalidOperationException("LoginName cannot be null or empty when creating user with login name."); + } + WithUser(LoginName); return this; @@ -93,11 +127,26 @@ public SecurityBuilder WithUser() public void ForLogin(string loginName) { + if (string.IsNullOrWhiteSpace(loginName)) + { + throw new ArgumentException("Login name cannot be null or empty.", nameof(loginName)); + } + _loginName = loginName; } public void ForLogin(string loginName, string password) { + if (string.IsNullOrWhiteSpace(loginName)) + { + throw new ArgumentException("Login name cannot be null or empty.", nameof(loginName)); + } + + if (string.IsNullOrWhiteSpace(password)) + { + throw new ArgumentException("Password cannot be null or empty.", nameof(password)); + } + _loginName = loginName; Password = password; } diff --git a/src/Rinsen.DatabaseInstaller/SqlTypes/Default.cs b/src/Rinsen.DatabaseInstaller/SqlTypes/Default.cs index d0caee9..b6c9aee 100644 --- a/src/Rinsen.DatabaseInstaller/SqlTypes/Default.cs +++ b/src/Rinsen.DatabaseInstaller/SqlTypes/Default.cs @@ -2,6 +2,6 @@ { public class DefaultValue { - public string DefaultString { get; set; } + public string DefaultString { get; set; } = string.Empty; } -} \ No newline at end of file +} diff --git a/src/Rinsen.DatabaseInstaller/SqlTypes/RawSql.cs b/src/Rinsen.DatabaseInstaller/SqlTypes/RawSql.cs index dea155b..542f41f 100644 --- a/src/Rinsen.DatabaseInstaller/SqlTypes/RawSql.cs +++ b/src/Rinsen.DatabaseInstaller/SqlTypes/RawSql.cs @@ -4,9 +4,9 @@ namespace Rinsen.DatabaseInstaller.SqlTypes { public class RawSql : IDbChange { - public List UpScripts { get; set; } + public List UpScripts { get; set; } = new List(); - public List DownScripts { get; set; } + public List DownScripts { get; set; } = new List(); public IReadOnlyList GetDownScript(InstallerOptions installerOptions) { diff --git a/src/Rinsen.DatabaseInstaller/Table.cs b/src/Rinsen.DatabaseInstaller/Table.cs index 070b373..b9c68ae 100644 --- a/src/Rinsen.DatabaseInstaller/Table.cs +++ b/src/Rinsen.DatabaseInstaller/Table.cs @@ -329,6 +329,11 @@ public ColumnBuilder AddColumn(string name, IDbType columnType) var columnBuilder = new ColumnBuilder(this, name, columnType); + if (columnBuilder.Column == null) + { + throw new InvalidOperationException("Failed to create column. ColumnBuilder did not initialize the column properly."); + } + ColumnsToAdd.Add(columnBuilder.Column); return columnBuilder; diff --git a/src/Rinsen.DatabaseInstaller/TableAlteration.cs b/src/Rinsen.DatabaseInstaller/TableAlteration.cs index 877e0f6..b7ed901 100644 --- a/src/Rinsen.DatabaseInstaller/TableAlteration.cs +++ b/src/Rinsen.DatabaseInstaller/TableAlteration.cs @@ -66,6 +66,11 @@ private ColumnBuilder AlterColumn(string name, IDbType dbType) var columnBuilder = new ColumnBuilder(this, name, dbType); + if (columnBuilder.Column == null) + { + throw new InvalidOperationException("Failed to create column. ColumnBuilder did not initialize the column properly."); + } + ColumnsToAlter.Add(columnBuilder.Column); return columnBuilder; @@ -94,6 +99,11 @@ public ColumnBuilder AlterColumn(string name, IDbType dbType) var columnBuilder = new ColumnBuilder(this, name, dbType); + if (columnBuilder.Column == null) + { + throw new InvalidOperationException("Failed to create column. ColumnBuilder did not initialize the column properly."); + } + ColumnsToAlter.Add(columnBuilder.Column); return columnBuilder; diff --git a/src/Rinsen.DatabaseInstaller/VersionHandler.cs b/src/Rinsen.DatabaseInstaller/VersionHandler.cs index 2d3983a..2cebdd7 100644 --- a/src/Rinsen.DatabaseInstaller/VersionHandler.cs +++ b/src/Rinsen.DatabaseInstaller/VersionHandler.cs @@ -22,9 +22,9 @@ internal async Task GetInstalledVersionAsync(string throw new InvalidOperationException("Installer is not installed"); } - InstallationNameAndVersion installedNameAndVersion = await _versionStorage.GetAsync(name, connection, transaction); + var installedNameAndVersion = await _versionStorage.GetAsync(name, connection, transaction); - if (installedNameAndVersion == default(InstallationNameAndVersion)) + if (installedNameAndVersion == default) { installedNameAndVersion = new InstallationNameAndVersion { @@ -48,7 +48,7 @@ internal async Task> GetInstalledVersion } else { - return Enumerable.Empty(); + return []; } } diff --git a/test/Rinsen.DatabaseInstaller.Tests/TestHelper.cs b/test/Rinsen.DatabaseInstaller.Tests/TestHelper.cs index b3586b9..22aa04b 100644 --- a/test/Rinsen.DatabaseInstaller.Tests/TestHelper.cs +++ b/test/Rinsen.DatabaseInstaller.Tests/TestHelper.cs @@ -8,12 +8,7 @@ public static class TestHelper { public static InstallerOptions GetInstallerOptions() { - return new InstallerOptions - { - ConnectionString = string.Empty, - DatabaseName = "TestDb", - Schema = "dbo" - }; + return new InstallerOptions("TestDb", "dbo", string.Empty, string.Empty); } } }