Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix DbConnectionValidator when Shell Settings from database #12342

Merged
merged 15 commits into from
Sep 9, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public async Task<IEnumerable<ModelError>> ValidateAsync(TenantViewModel model)
}
}

await AssertConnectionValidityAndApplyErrorsAsync(model.DatabaseProvider, model.ConnectionString, model.TablePrefix, errors);
await AssertConnectionValidityAndApplyErrorsAsync(model.DatabaseProvider, model.ConnectionString, model.TablePrefix, errors, shellSettings?.IsDefaultShell() == true);
}
else
{
Expand All @@ -100,25 +100,25 @@ public async Task<IEnumerable<ModelError>> ValidateAsync(TenantViewModel model)
// While the tenant is in Uninitialized state, we still are able to change the database settings.
// Let's validate the database for assurance.

await AssertConnectionValidityAndApplyErrorsAsync(model.DatabaseProvider, model.ConnectionString, model.TablePrefix, errors);
await AssertConnectionValidityAndApplyErrorsAsync(model.DatabaseProvider, model.ConnectionString, model.TablePrefix, errors, shellSettings?.IsDefaultShell() == true);
}
}

return errors;
}

private async Task AssertConnectionValidityAndApplyErrorsAsync(string databaseProvider, string connectionString, string tablePrefix, List<ModelError> errors)
private async Task AssertConnectionValidityAndApplyErrorsAsync(string databaseProvider, string connectionString, string tablePrefix, List<ModelError> errors, bool isDefaultShell)
{
switch (await _dbConnectionValidator.ValidateAsync(databaseProvider, connectionString, tablePrefix))
switch (await _dbConnectionValidator.ValidateAsync(databaseProvider, connectionString, tablePrefix, isDefaultShell))
{
case DbConnectionValidatorResult.UnsupportedProvider:
errors.Add(new ModelError(nameof(TenantViewModel.DatabaseProvider), S["The provided database provider is not supported."]));
break;
case DbConnectionValidatorResult.InvalidConnection:
errors.Add(new ModelError(nameof(TenantViewModel.ConnectionString), S["The provided connection string is invalid or server is unreachable."]));
break;
case DbConnectionValidatorResult.DocumentFound:
errors.Add(new ModelError(nameof(TenantViewModel.TablePrefix), S["The provided table prefix already exists."]));
case DbConnectionValidatorResult.DocumentTableFound:
errors.Add(new ModelError(nameof(TenantViewModel.TablePrefix), S["The provided database and table prefix are already in use."]));
break;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,42 @@
namespace OrchardCore.Data;

/// <summary>
/// The result from validating a database connection using <see cref="IDbConnectionValidator"/>.
/// </summary>
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved
public enum DbConnectionValidatorResult
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved
{
// Unknown indicates that the connection string status is unknown or was not yet validated
/// <summary>
/// The connection string status is unknown or was not validated.
/// </summary>
Unknown,

// NoProvider indicated that the provider is missing
/// <summary>
/// The database provider is missing.
/// </summary>
NoProvider,

// DocumentNotFound indicates that the connection string was valid, yet the Document table does not exist
DocumentNotFound,
/// <summary>
/// The connection string is valid and the 'Document' table does not exists.
/// </summary>
DocumentTableNotFound,

// DocumentFound indicates that the connection string was valid, yet the Document table exist
DocumentFound,
/// <summary>
/// The connection string is valid and the 'Document' table exists.
/// </summary>
DocumentTableFound,

// InvalidConnection unable to open a connection to the given connection string
/// <summary>
/// The 'Document' table exists with no 'ShellDescriptor' document.
/// </summary>
ShellDescriptorDocumentNotFound,

/// <summary>
/// Unable to open a connection with the given database connection string.
/// </summary>
hishamco marked this conversation as resolved.
Show resolved Hide resolved
InvalidConnection,

// UnsupportedProvider indicated invalid or unsupported database provider
/// <summary>
/// Unsupported database provider.
/// </summary>
UnsupportedProvider
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ namespace OrchardCore.Data;

public interface IDbConnectionValidator
{
Task<DbConnectionValidatorResult> ValidateAsync(string databaseProvider, string connectionString, string tablePrefix);
Task<DbConnectionValidatorResult> ValidateAsync(string databaseProvider, string connectionString, string tablePrefix, bool isDefaultShell);
}
63 changes: 52 additions & 11 deletions src/OrchardCore/OrchardCore.Data.YesSql/DbConnectionValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using MySqlConnector;
using Npgsql;
using OrchardCore.Data.YesSql.Abstractions;
using OrchardCore.Environment.Shell.Descriptor.Models;
using YesSql;
using YesSql.Provider.MySql;
using YesSql.Provider.PostgreSql;
Expand All @@ -34,7 +35,7 @@ IOptions<YesSqlOptions> yesSqlOptions
_yesSqlOptions = yesSqlOptions.Value;
}

public async Task<DbConnectionValidatorResult> ValidateAsync(string databaseProvider, string connectionString, string tablePrefix)
public async Task<DbConnectionValidatorResult> ValidateAsync(string databaseProvider, string connectionString, string tablePrefix, bool isDefaultShell)
{
if (String.IsNullOrWhiteSpace(databaseProvider))
{
Expand All @@ -50,7 +51,7 @@ public async Task<DbConnectionValidatorResult> ValidateAsync(string databaseProv

if (provider != null && !provider.HasConnectionString)
{
return DbConnectionValidatorResult.DocumentNotFound;
return DbConnectionValidatorResult.DocumentTableNotFound;
}

if (String.IsNullOrWhiteSpace(connectionString))
Expand All @@ -72,32 +73,72 @@ public async Task<DbConnectionValidatorResult> ValidateAsync(string databaseProv
}

var selectBuilder = GetSelectBuilderForDocumentTable(tablePrefix, providerName);

try
{
var selectCommand = connection.CreateCommand();
selectCommand.CommandText = selectBuilder.ToSqlString();

using var result = await selectCommand.ExecuteReaderAsync();

// at this point the query succeeded and the table exists
return DbConnectionValidatorResult.DocumentFound;
if (!isDefaultShell)
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved
{
// The 'Document' table exists.
return DbConnectionValidatorResult.DocumentTableFound;
}

var columns = Enumerable.Range(0, result.FieldCount).Select(result.GetName);
if (!columns.Any(c => c == "Type") || !columns.Any(c => c == "Content") || !columns.Any(c => c == "Version"))
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved
{
// The 'Document' table exists with another schema.
return DbConnectionValidatorResult.DocumentTableFound;
}
}
catch
{
// at this point we know that the document table does not exist
// The 'Document' table does not exist.
return DbConnectionValidatorResult.DocumentTableNotFound;
}

selectBuilder = GetSelectBuilderForShellDescriptorDocument(tablePrefix, providerName);
try
{
var selectCommand = connection.CreateCommand();
selectCommand.CommandText = selectBuilder.ToSqlString();

return DbConnectionValidatorResult.DocumentNotFound;
using var result = await selectCommand.ExecuteReaderAsync();
if (!result.HasRows)
{
// The 'Document' table exists with no 'ShellDescriptor' document.
return DbConnectionValidatorResult.ShellDescriptorDocumentNotFound;
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved
}
}
catch
{
}

// The 'Document' table exists.
return DbConnectionValidatorResult.DocumentTableFound;
}

private ISqlBuilder GetSelectBuilderForDocumentTable(string tablePrefix, DatabaseProviderName providerName)
{
var selectBuilder = GetSqlBuilder(providerName, tablePrefix);

selectBuilder.Select();
selectBuilder.AddSelector("*");
selectBuilder.Selector("*");
selectBuilder.Table(_tableNameConvention.GetDocumentTable());
selectBuilder.Take("1");

return selectBuilder;
}

private ISqlBuilder GetSelectBuilderForShellDescriptorDocument(string tablePrefix, DatabaseProviderName providerName)
{
var selectBuilder = GetSqlBuilder(providerName, tablePrefix);

selectBuilder.Select();
selectBuilder.Selector("*");
selectBuilder.Table(_tableNameConvention.GetDocumentTable());
selectBuilder.WhereAnd($"Type = '{typeof(ShellDescriptor).FullName}, {typeof(ShellDescriptor).Assembly.GetName().Name}'");
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved
selectBuilder.Take("1");

return selectBuilder;
Expand All @@ -111,7 +152,7 @@ private static IConnectionFactory GetFactory(DatabaseProviderName providerName,
DatabaseProviderName.MySql => new DbConnectionFactory<MySqlConnection>(connectionString),
DatabaseProviderName.Sqlite => new DbConnectionFactory<SqliteConnection>(connectionString),
DatabaseProviderName.Postgres => new DbConnectionFactory<NpgsqlConnection>(connectionString),
_ => throw new ArgumentOutOfRangeException("Unsupported Database Provider"),
_ => throw new ArgumentOutOfRangeException(nameof(providerName), "Unsupported database provider"),
};
}

Expand All @@ -123,7 +164,7 @@ private ISqlBuilder GetSqlBuilder(DatabaseProviderName providerName, string tabl
DatabaseProviderName.MySql => new MySqlDialect(),
DatabaseProviderName.Sqlite => new SqliteDialect(),
DatabaseProviderName.Postgres => new PostgreSqlDialect(),
_ => throw new ArgumentOutOfRangeException("Unsupported Database Provider"),
_ => throw new ArgumentOutOfRangeException(nameof(providerName), "Unsupported database provider"),
};

var prefix = String.Empty;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using OrchardCore.Data;

namespace OrchardCore.Shells.Database.Configuration
{
public class DatabaseShellsStorageOptions
{
public bool MigrateFromFiles { get; set; }
public string DatabaseProvider { get; set; }
public DatabaseProviderName DatabaseProvider { get; set; }
public string ConnectionString { get; set; }
public string TablePrefix { get; set; }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using OrchardCore.Data;
using OrchardCore.Environment.Shell;
using OrchardCore.Environment.Shell.Builders;
using OrchardCore.Environment.Shell.Descriptor.Models;
Expand All @@ -12,7 +13,7 @@ public static class DatabaseShellContextFactoryExtensions
{
internal static Task<ShellContext> GetDatabaseContextAsync(this IShellContextFactory shellContextFactory, DatabaseShellsStorageOptions options)
{
if (options.DatabaseProvider == null)
if (options.DatabaseProvider == DatabaseProviderName.None)
{
throw new ArgumentNullException(nameof(options.DatabaseProvider),
"The 'OrchardCore.Shells.Database' configuration section should define a 'DatabaseProvider'");
Expand All @@ -24,7 +25,7 @@ internal static Task<ShellContext> GetDatabaseContextAsync(this IShellContextFac
State = TenantState.Running
};

settings["DatabaseProvider"] = options.DatabaseProvider;
settings["DatabaseProvider"] = options.DatabaseProvider.ToString();
settings["ConnectionString"] = options.ConnectionString;
settings["TablePrefix"] = options.TablePrefix;

Expand Down
6 changes: 3 additions & 3 deletions src/OrchardCore/OrchardCore.Setup.Core/SetupService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ private async Task<string> SetupInternalAsync(SetupContext context)
shellSettings["TablePrefix"] = context.Properties.TryGetValue(SetupConstants.DatabaseTablePrefix, out var databaseTablePrefix) ? databaseTablePrefix?.ToString() : String.Empty;
}

switch (await _dbConnectionValidator.ValidateAsync(shellSettings["DatabaseProvider"], shellSettings["ConnectionString"], shellSettings["TablePrefix"]))
switch (await _dbConnectionValidator.ValidateAsync(shellSettings["DatabaseProvider"], shellSettings["ConnectionString"], shellSettings["TablePrefix"], shellSettings.IsDefaultShell()))
{
case DbConnectionValidatorResult.NoProvider:
context.Errors.Add(String.Empty, S["DatabaseProvider setting is required."]);
Expand All @@ -181,8 +181,8 @@ private async Task<string> SetupInternalAsync(SetupContext context)
case DbConnectionValidatorResult.InvalidConnection:
context.Errors.Add(String.Empty, S["The provided connection string is invalid or server is unreachable."]);
break;
case DbConnectionValidatorResult.DocumentFound:
context.Errors.Add(String.Empty, S["The provided database table is already in use."]);
case DbConnectionValidatorResult.DocumentTableFound:
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved
context.Errors.Add(String.Empty, S["The provided database and table prefix are already in use."]);
break;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ private static TenantValidator CreateTenantValidator(bool defaultTenant = true)
: new ShellSettings();

var connectionFactory = new Mock<IDbConnectionValidator>();
connectionFactory.Setup(l => l.ValidateAsync(shellSettings["ProviderName"], shellSettings["ConnectionName"], shellSettings["TablePrefix"]));
connectionFactory.Setup(l => l.ValidateAsync(shellSettings["ProviderName"], shellSettings["ConnectionName"], shellSettings["TablePrefix"], shellSettings.IsDefaultShell()));

return new TenantValidator(
ShellHost,
Expand Down