diff --git a/src/OrchardCore.Cms.Web/appsettings.json b/src/OrchardCore.Cms.Web/appsettings.json index b56253f4727..47a7711e971 100644 --- a/src/OrchardCore.Cms.Web/appsettings.json +++ b/src/OrchardCore.Cms.Web/appsettings.json @@ -15,9 +15,16 @@ //"OrchardCore_ContentLocalization_CulturePickerOptions": { // "CookieLifeTime": 14 // Set the culture picker cookie life time (in days). //}, + // See https://docs.orchardcore.net/en/latest/docs/reference/core/Data/#sqlite. //"OrchardCore_Data_Sqlite": { // "UseConnectionPooling": false //}, + // See https://docs.orchardcore.net/en/latest/docs/reference/core/Data/#database-table to configure database table presets used before a given tenant is setup. + //"OrchardCore_Data_TableOptions": { + // "DefaultDocumentTable": "Document", // Document table name, defaults to 'Document'. + // "DefaultTableNameSeparator": "_", // Table name separator, one or multiple '_', "NULL" means no separator, defaults to '_'. + // "DefaultIdentityColumnSize": "Int64" // Identity column size, 'Int32' or 'Int64', defaults to 'Int64'. + //}, // See https://docs.orchardcore.net/en/latest/docs/reference/modules/DataProtection.Azure/#configuration to configure data protection key storage in Azure Blob Storage. //"OrchardCore_DataProtection_Azure": { // "ConnectionString": "", // Set to your Azure Storage account connection string. @@ -145,6 +152,7 @@ // "DatabaseProvider": "Sqlite", // "DatabaseConnectionString": "", // "DatabaseTablePrefix": "", + // "DatabaseSchema": "", // "RecipeName": "SaaS" // }, // { @@ -157,6 +165,7 @@ // "DatabaseProvider": "Sqlite", // "DatabaseConnectionString": "", // "DatabaseTablePrefix": "tenant", + // "DatabaseSchema": "", // "RecipeName": "Agency", // "RequestUrlHost": "", // "RequestUrlPrefix": "tenant" diff --git a/src/OrchardCore.Modules/OrchardCore.AutoSetup/AutoSetupMiddleware.cs b/src/OrchardCore.Modules/OrchardCore.AutoSetup/AutoSetupMiddleware.cs index 71403d6a494..c0760b2e297 100644 --- a/src/OrchardCore.Modules/OrchardCore.AutoSetup/AutoSetupMiddleware.cs +++ b/src/OrchardCore.Modules/OrchardCore.AutoSetup/AutoSetupMiddleware.cs @@ -214,6 +214,7 @@ public async Task CreateTenantSettingsAsync(TenantSetupOptions se shellSettings["ConnectionString"] = setupOptions.DatabaseConnectionString; shellSettings["TablePrefix"] = setupOptions.DatabaseTablePrefix; + shellSettings["Schema"] = setupOptions.DatabaseSchema; shellSettings["DatabaseProvider"] = setupOptions.DatabaseProvider; shellSettings["Secret"] = Guid.NewGuid().ToString(); shellSettings["RecipeName"] = setupOptions.RecipeName; @@ -250,6 +251,7 @@ private static async Task GetSetupContextAsync(TenantSetupOptions setupContext.Properties[SetupConstants.DatabaseConnectionString] = options.DatabaseConnectionString; setupContext.Properties[SetupConstants.DatabaseProvider] = options.DatabaseProvider; setupContext.Properties[SetupConstants.DatabaseTablePrefix] = options.DatabaseTablePrefix; + setupContext.Properties[SetupConstants.DatabaseSchema] = options.DatabaseSchema; setupContext.Properties[SetupConstants.SiteName] = options.SiteName; setupContext.Properties[SetupConstants.SiteTimeZone] = options.SiteTimeZone; diff --git a/src/OrchardCore.Modules/OrchardCore.AutoSetup/Options/TenantSetupOptions.cs b/src/OrchardCore.Modules/OrchardCore.AutoSetup/Options/TenantSetupOptions.cs index a62091a05b8..d62f01f0c66 100644 --- a/src/OrchardCore.Modules/OrchardCore.AutoSetup/Options/TenantSetupOptions.cs +++ b/src/OrchardCore.Modules/OrchardCore.AutoSetup/Options/TenantSetupOptions.cs @@ -55,6 +55,11 @@ public class TenantSetupOptions /// public string DatabaseTablePrefix { get; set; } + /// + /// Gets or sets the database's schema. + /// + public string DatabaseSchema { get; set; } + /// /// Gets or sets the recipe name. /// diff --git a/src/OrchardCore.Modules/OrchardCore.AutoSetup/OrchardCore.AutoSetup.csproj b/src/OrchardCore.Modules/OrchardCore.AutoSetup/OrchardCore.AutoSetup.csproj index 0c9bf915802..d7d0e22517b 100644 --- a/src/OrchardCore.Modules/OrchardCore.AutoSetup/OrchardCore.AutoSetup.csproj +++ b/src/OrchardCore.Modules/OrchardCore.AutoSetup/OrchardCore.AutoSetup.csproj @@ -14,7 +14,6 @@ - diff --git a/src/OrchardCore.Modules/OrchardCore.Setup/Controllers/SetupController.cs b/src/OrchardCore.Modules/OrchardCore.Setup/Controllers/SetupController.cs index 63c70f30dd4..ef7dee229b9 100644 --- a/src/OrchardCore.Modules/OrchardCore.Setup/Controllers/SetupController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Setup/Controllers/SetupController.cs @@ -69,7 +69,7 @@ public async Task Index(string token) DatabaseProviders = _databaseProviders, Recipes = recipes, RecipeName = defaultRecipe?.Name, - Secret = token + Secret = token, }; CopyShellSettingsValues(model); @@ -80,6 +80,12 @@ public async Task Index(string token) model.TablePrefix = _shellSettings["TablePrefix"]; } + if (!String.IsNullOrEmpty(_shellSettings["Schema"])) + { + model.DatabaseConfigurationPreset = true; + model.Schema = _shellSettings["Schema"]; + } + return View(model); } @@ -157,12 +163,14 @@ public async Task IndexPOST(SetupViewModel model) setupContext.Properties[SetupConstants.DatabaseProvider] = _shellSettings["DatabaseProvider"]; setupContext.Properties[SetupConstants.DatabaseConnectionString] = _shellSettings["ConnectionString"]; setupContext.Properties[SetupConstants.DatabaseTablePrefix] = _shellSettings["TablePrefix"]; + setupContext.Properties[SetupConstants.DatabaseSchema] = _shellSettings["Schema"]; } else { setupContext.Properties[SetupConstants.DatabaseProvider] = model.DatabaseProvider; setupContext.Properties[SetupConstants.DatabaseConnectionString] = model.ConnectionString; setupContext.Properties[SetupConstants.DatabaseTablePrefix] = model.TablePrefix; + setupContext.Properties[SetupConstants.DatabaseSchema] = model.Schema; } var executionId = await _setupService.SetupAsync(setupContext); @@ -204,11 +212,6 @@ private void CopyShellSettingsValues(SetupViewModel model) { model.DatabaseProvider = model.DatabaseProviders.FirstOrDefault(p => p.IsDefault)?.Value; } - - if (!String.IsNullOrEmpty(_shellSettings["Description"])) - { - model.Description = _shellSettings["Description"]; - } } private async Task ShouldProceedWithTokenAsync(string token) @@ -228,10 +231,9 @@ private async Task ShouldProceedWithTokenAsync(string token) private async Task IsTokenValid(string token) { + var result = false; try { - var result = false; - var shellScope = await _shellHost.GetScopeAsync(ShellHelper.DefaultShellName); await shellScope.UsingAsync(scope => @@ -251,15 +253,13 @@ await shellScope.UsingAsync(scope => return Task.CompletedTask; }); - - return result; } catch (Exception ex) { _logger.LogError(ex, "Error in decrypting the token"); } - return false; + return result; } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Setup/ViewModels/SetupViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Setup/ViewModels/SetupViewModel.cs index 69c0dcd6359..a7a82d7c5d3 100644 --- a/src/OrchardCore.Modules/OrchardCore.Setup/ViewModels/SetupViewModel.cs +++ b/src/OrchardCore.Modules/OrchardCore.Setup/ViewModels/SetupViewModel.cs @@ -22,6 +22,8 @@ public class SetupViewModel public string TablePrefix { get; set; } + public string Schema { get; set; } + /// /// True if the database configuration is preset and can't be changed or displayed on the Setup screen. /// diff --git a/src/OrchardCore.Modules/OrchardCore.Setup/Views/Setup/Index.cshtml b/src/OrchardCore.Modules/OrchardCore.Setup/Views/Setup/Index.cshtml index 3450338a417..c60702bdd2a 100644 --- a/src/OrchardCore.Modules/OrchardCore.Setup/Views/Setup/Index.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Setup/Views/Setup/Index.cshtml @@ -168,7 +168,7 @@ @@ -194,6 +194,15 @@ + +
+
+ + + +
@T["When left blank, the default value on the server will be used."] @T["For example, '{0}' for SQL Server.", "dbo"]
+
+
}
@T["Super User"] @@ -240,7 +249,7 @@ diff --git a/src/OrchardCore.Modules/OrchardCore.Tenants/Views/Items/SetupTenantTask.Fields.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Tenants/Views/Items/SetupTenantTask.Fields.Edit.cshtml index d27131fb200..13d8b95e5a0 100644 --- a/src/OrchardCore.Modules/OrchardCore.Tenants/Views/Items/SetupTenantTask.Fields.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Tenants/Views/Items/SetupTenantTask.Fields.Edit.cshtml @@ -41,6 +41,12 @@ +
+ + +
@T["When left blank, the default value on the server will be used."] @T["For example, '{0}' for SQL Server.", "dbo"]
+
+
@@ -56,60 +62,66 @@ diff --git a/src/OrchardCore.Modules/OrchardCore.Tenants/Views/Shared/_TenantDatabaseInfo.cshtml b/src/OrchardCore.Modules/OrchardCore.Tenants/Views/Shared/_TenantDatabaseInfo.cshtml new file mode 100644 index 00000000000..77a4df2ae3b --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Tenants/Views/Shared/_TenantDatabaseInfo.cshtml @@ -0,0 +1,49 @@ +@model EditTenantViewModel +@inject IEnumerable DatabaseProviders + +
+ @T["Database Presets"] + @T["Optionally specify which presets should be used for the Setup experience."] +
+
+ @if (!Model.DatabaseConfigurationPreset || Model.CanEditDatabasePresets) + { +
+ + +
+ } + +
+ + + +
+
+ +@if (!Model.DatabaseConfigurationPreset || Model.CanEditDatabasePresets) +{ +
+
+ + + + +
+
+ +
+
+ + + +
@T["When left blank, the default value on the server will be used."] @T["For example, '{0}' for SQL Server.", "dbo"]
+
+
+} diff --git a/src/OrchardCore.Modules/OrchardCore.Tenants/Views/Shared/_TenantFeatureProfile.cshtml b/src/OrchardCore.Modules/OrchardCore.Tenants/Views/Shared/_TenantFeatureProfile.cshtml new file mode 100644 index 00000000000..d9dc39135ab --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Tenants/Views/Shared/_TenantFeatureProfile.cshtml @@ -0,0 +1,14 @@ +@model EditTenantViewModel + +@if (Model.FeatureProfiles.Any()) +{ +
+ @T["Feature profile"] + @T["Optionally specify which feature profile should be applied to this tenant."] +
+
+
+ +
+
+} diff --git a/src/OrchardCore.Modules/OrchardCore.Tenants/Views/Shared/_TenantInfo.cshtml b/src/OrchardCore.Modules/OrchardCore.Tenants/Views/Shared/_TenantInfo.cshtml new file mode 100644 index 00000000000..d3641d6ad0a --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Tenants/Views/Shared/_TenantInfo.cshtml @@ -0,0 +1,29 @@ +@model EditTenantViewModel + +
+ + + @T["The category of the tenant."] +
+ +
+ + + + @T["(Optional) Example: If prefix is \"site1\", the tenant URL prefix is \"https://orchardcore.net/site1\"."] +
+ +
+ + + + @T["The description of the tenant."] +
+ +
+ + + + @T["Example: If host is \"orchardproject.net\", the tenant site URL is \"https://orchardcore.net/\"."] + @T["You may define multiple domains using the comma (,) as a separator. Use the '*.' prefix to map all subdomains."] +
diff --git a/src/OrchardCore.Modules/OrchardCore.Tenants/Views/Shared/_TenantRecipes.cshtml b/src/OrchardCore.Modules/OrchardCore.Tenants/Views/Shared/_TenantRecipes.cshtml new file mode 100644 index 00000000000..07bc33f13bd --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Tenants/Views/Shared/_TenantRecipes.cshtml @@ -0,0 +1,20 @@ +@model EditTenantViewModel + +
+ @T["Recipe"] + @T["Optionally specify which presets should be used for the Setup experience."] +
+
+
+ +
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/Activities/CreateTenantTask.cs b/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/Activities/CreateTenantTask.cs index 8540436a990..9c2db53258d 100644 --- a/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/Activities/CreateTenantTask.cs +++ b/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/Activities/CreateTenantTask.cs @@ -30,12 +30,6 @@ public CreateTenantTask( public override LocalizedString DisplayText => S["Create Tenant Task"]; - public string ContentType - { - get => GetProperty(); - set => SetProperty(value); - } - public WorkflowExpression Description { get => GetProperty(() => new WorkflowExpression()); @@ -72,6 +66,12 @@ public WorkflowExpression TablePrefix set => SetProperty(value); } + public WorkflowExpression Schema + { + get => GetProperty(() => new WorkflowExpression()); + set => SetProperty(value); + } + public WorkflowExpression RecipeName { get => GetProperty(() => new WorkflowExpression()); @@ -98,62 +98,74 @@ public async override Task ExecuteAsync(WorkflowExecuti var tenantName = (await ExpressionEvaluator.EvaluateAsync(TenantName, workflowContext, null))?.Trim(); - if (string.IsNullOrEmpty(tenantName)) + if (String.IsNullOrEmpty(tenantName)) { return Outcomes("Failed"); } - if (ShellHost.TryGetSettings(tenantName, out var shellSettings)) + if (ShellHost.TryGetSettings(tenantName, out _)) { return Outcomes("Failed"); } + var description = (await ExpressionEvaluator.EvaluateAsync(Description, workflowContext, null))?.Trim(); var requestUrlPrefix = (await ExpressionEvaluator.EvaluateAsync(RequestUrlPrefix, workflowContext, null))?.Trim(); var requestUrlHost = (await ExpressionEvaluator.EvaluateAsync(RequestUrlHost, workflowContext, null))?.Trim(); var databaseProvider = (await ExpressionEvaluator.EvaluateAsync(DatabaseProvider, workflowContext, null))?.Trim(); var connectionString = (await ExpressionEvaluator.EvaluateAsync(ConnectionString, workflowContext, null))?.Trim(); var tablePrefix = (await ExpressionEvaluator.EvaluateAsync(TablePrefix, workflowContext, null))?.Trim(); + var schema = (await ExpressionEvaluator.EvaluateAsync(Schema, workflowContext, null))?.Trim(); var recipeName = (await ExpressionEvaluator.EvaluateAsync(RecipeName, workflowContext, null))?.Trim(); var featureProfile = (await ExpressionEvaluator.EvaluateAsync(FeatureProfile, workflowContext, null))?.Trim(); // Creates a default shell settings based on the configuration. - shellSettings = ShellSettingsManager.CreateDefaultSettings(); + var shellSettings = ShellSettingsManager.CreateDefaultSettings(); shellSettings.Name = tenantName; - if (!string.IsNullOrEmpty(requestUrlHost)) + if (!String.IsNullOrEmpty(description)) + { + shellSettings["Description"] = description; + } + + if (!String.IsNullOrEmpty(requestUrlHost)) { shellSettings.RequestUrlHost = requestUrlHost; } - if (!string.IsNullOrEmpty(requestUrlPrefix)) + if (!String.IsNullOrEmpty(requestUrlPrefix)) { shellSettings.RequestUrlPrefix = requestUrlPrefix; } shellSettings.State = TenantState.Uninitialized; - if (!string.IsNullOrEmpty(connectionString)) + if (!String.IsNullOrEmpty(connectionString)) { shellSettings["ConnectionString"] = connectionString; } - if (!string.IsNullOrEmpty(tablePrefix)) + if (!String.IsNullOrEmpty(tablePrefix)) { shellSettings["TablePrefix"] = tablePrefix; } - if (!string.IsNullOrEmpty(databaseProvider)) + if (!String.IsNullOrEmpty(schema)) + { + shellSettings["Schema"] = schema; + } + + if (!String.IsNullOrEmpty(databaseProvider)) { shellSettings["DatabaseProvider"] = databaseProvider; } - if (!string.IsNullOrEmpty(recipeName)) + if (!String.IsNullOrEmpty(recipeName)) { shellSettings["RecipeName"] = recipeName; } - if (!string.IsNullOrEmpty(featureProfile)) + if (!String.IsNullOrEmpty(featureProfile)) { shellSettings["FeatureProfile"] = featureProfile; } diff --git a/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/Activities/SetupTenantTask.cs b/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/Activities/SetupTenantTask.cs index d832570dc81..6f4ad292e4e 100644 --- a/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/Activities/SetupTenantTask.cs +++ b/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/Activities/SetupTenantTask.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -96,6 +97,12 @@ public WorkflowExpression DatabaseTablePrefix set => SetProperty(value); } + public WorkflowExpression DatabaseSchema + { + get => GetProperty(() => new WorkflowExpression()); + set => SetProperty(value); + } + public WorkflowExpression RecipeName { get => GetProperty(() => new WorkflowExpression()); @@ -116,7 +123,7 @@ public override async Task ExecuteAsync(WorkflowExecuti var tenantName = (await ExpressionEvaluator.EvaluateAsync(TenantName, workflowContext, null))?.Trim(); - if (string.IsNullOrWhiteSpace(tenantName)) + if (String.IsNullOrWhiteSpace(tenantName)) { return Outcomes("Failed"); } @@ -140,12 +147,12 @@ public override async Task ExecuteAsync(WorkflowExecuti var adminUsername = (await ExpressionEvaluator.EvaluateAsync(AdminUsername, workflowContext, null))?.Trim(); var adminEmail = (await ExpressionEvaluator.EvaluateAsync(AdminEmail, workflowContext, null))?.Trim(); - if (string.IsNullOrEmpty(adminUsername) || adminUsername.Any(c => !_identityOptions.User.AllowedUserNameCharacters.Contains(c))) + if (String.IsNullOrEmpty(adminUsername) || adminUsername.Any(c => !_identityOptions.User.AllowedUserNameCharacters.Contains(c))) { return Outcomes("Failed"); } - if (string.IsNullOrEmpty(adminEmail) || !_emailAddressValidator.Validate(adminEmail)) + if (String.IsNullOrEmpty(adminEmail) || !_emailAddressValidator.Validate(adminEmail)) { return Outcomes("Failed"); } @@ -155,24 +162,30 @@ public override async Task ExecuteAsync(WorkflowExecuti var databaseProvider = (await ExpressionEvaluator.EvaluateAsync(DatabaseProvider, workflowContext, null))?.Trim(); var databaseConnectionString = (await ExpressionEvaluator.EvaluateAsync(DatabaseConnectionString, workflowContext, null))?.Trim(); var databaseTablePrefix = (await ExpressionEvaluator.EvaluateAsync(DatabaseTablePrefix, workflowContext, null))?.Trim(); + var databaseSchema = (await ExpressionEvaluator.EvaluateAsync(DatabaseSchema, workflowContext, null))?.Trim(); var recipeName = (await ExpressionEvaluator.EvaluateAsync(RecipeName, workflowContext, null))?.Trim(); - if (string.IsNullOrEmpty(databaseProvider)) + if (String.IsNullOrEmpty(databaseProvider)) { databaseProvider = shellSettings["DatabaseProvider"]; } - if (string.IsNullOrEmpty(databaseConnectionString)) + if (String.IsNullOrEmpty(databaseConnectionString)) { databaseConnectionString = shellSettings["ConnectionString"]; } - if (string.IsNullOrEmpty(databaseTablePrefix)) + if (String.IsNullOrEmpty(databaseTablePrefix)) { databaseTablePrefix = shellSettings["TablePrefix"]; } - if (string.IsNullOrEmpty(recipeName)) + if (String.IsNullOrWhiteSpace(databaseSchema)) + { + databaseSchema = shellSettings["Schema"]; + } + + if (String.IsNullOrEmpty(recipeName)) { recipeName = shellSettings["RecipeName"]; } @@ -196,6 +209,7 @@ public override async Task ExecuteAsync(WorkflowExecuti { SetupConstants.DatabaseProvider, databaseProvider }, { SetupConstants.DatabaseConnectionString, databaseConnectionString }, { SetupConstants.DatabaseTablePrefix, databaseTablePrefix }, + { SetupConstants.DatabaseSchema, databaseSchema }, } }; diff --git a/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/Drivers/CreateTenantTaskDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/Drivers/CreateTenantTaskDisplayDriver.cs index a966eb37238..c64323dd43e 100644 --- a/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/Drivers/CreateTenantTaskDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/Drivers/CreateTenantTaskDisplayDriver.cs @@ -9,23 +9,27 @@ public class CreateTenantTaskDisplayDriver : TenantTaskDisplayDriver(model.TenantNameExpression); + activity.TenantName = new WorkflowExpression(model.DescriptionExpression); + activity.Description = new WorkflowExpression(model.TenantNameExpression); activity.RequestUrlPrefix = new WorkflowExpression(model.RequestUrlPrefixExpression); activity.RequestUrlHost = new WorkflowExpression(model.RequestUrlHostExpression); activity.DatabaseProvider = new WorkflowExpression(model.DatabaseProviderExpression); activity.ConnectionString = new WorkflowExpression(model.ConnectionStringExpression); activity.TablePrefix = new WorkflowExpression(model.TablePrefixExpression); + activity.Schema = new WorkflowExpression(model.SchemaExpression); activity.RecipeName = new WorkflowExpression(model.RecipeNameExpression); activity.FeatureProfile = new WorkflowExpression(model.FeatureProfileExpression); } diff --git a/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/Drivers/SetupTenantTaskDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/Drivers/SetupTenantTaskDisplayDriver.cs index aa1fed1ec28..64a1ceecb90 100644 --- a/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/Drivers/SetupTenantTaskDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/Drivers/SetupTenantTaskDisplayDriver.cs @@ -16,6 +16,7 @@ protected override void EditActivity(SetupTenantTask activity, SetupTenantTaskVi model.DatabaseProviderExpression = activity.DatabaseProvider.Expression; model.DatabaseConnectionStringExpression = activity.DatabaseConnectionString.Expression; model.DatabaseTablePrefixExpression = activity.DatabaseTablePrefix.Expression; + model.DatabaseSchemaExpression = activity.DatabaseSchema.Expression; model.RecipeNameExpression = activity.RecipeName.Expression; } @@ -29,6 +30,7 @@ protected override void UpdateActivity(SetupTenantTaskViewModel model, SetupTena activity.DatabaseProvider = new WorkflowExpression(model.DatabaseProviderExpression); activity.DatabaseConnectionString = new WorkflowExpression(model.DatabaseConnectionStringExpression); activity.DatabaseTablePrefix = new WorkflowExpression(model.DatabaseTablePrefixExpression); + activity.DatabaseSchema = new WorkflowExpression(model.DatabaseSchemaExpression); activity.RecipeName = new WorkflowExpression(model.RecipeNameExpression); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/ViewModels/CreateTenantTaskViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/ViewModels/CreateTenantTaskViewModel.cs index f23ae6a8d00..33329658463 100644 --- a/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/ViewModels/CreateTenantTaskViewModel.cs +++ b/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/ViewModels/CreateTenantTaskViewModel.cs @@ -16,7 +16,10 @@ public class CreateTenantTaskViewModel : TenantTaskViewModel public string TablePrefixExpression { get; set; } + public string SchemaExpression { get; set; } + public string RecipeNameExpression { get; set; } + public string FeatureProfileExpression { get; set; } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/ViewModels/SetupTenantTaskViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/ViewModels/SetupTenantTaskViewModel.cs index bce019f7618..59c6c6f4b70 100644 --- a/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/ViewModels/SetupTenantTaskViewModel.cs +++ b/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/ViewModels/SetupTenantTaskViewModel.cs @@ -5,12 +5,21 @@ namespace OrchardCore.Tenants.Workflows.ViewModels public class SetupTenantTaskViewModel : TenantTaskViewModel { public string SiteNameExpression { get; set; } + public string AdminUsernameExpression { get; set; } + public string AdminEmailExpression { get; set; } + public string AdminPasswordExpression { get; set; } + public string DatabaseProviderExpression { get; set; } + public string DatabaseConnectionStringExpression { get; set; } + public string DatabaseTablePrefixExpression { get; set; } + + public string DatabaseSchemaExpression { get; set; } + public string RecipeNameExpression { get; set; } } } diff --git a/src/OrchardCore/OrchardCore.Abstractions/Setup/SetupConstants.cs b/src/OrchardCore/OrchardCore.Abstractions/Setup/SetupConstants.cs index 1484c07bea3..c00c9a80f69 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Setup/SetupConstants.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Setup/SetupConstants.cs @@ -10,6 +10,8 @@ public static class SetupConstants public const string DatabaseProvider = "DatabaseProvider"; public const string DatabaseConnectionString = "DatabaseConnectionString"; public const string DatabaseTablePrefix = "DatabaseTablePrefix"; + public const string DatabaseSchema = "DatabaseSchema"; public const string SiteTimeZone = "SiteTimeZone"; + public const string FeatureProfile = "FeatureProfile"; } } diff --git a/src/OrchardCore/OrchardCore.Abstractions/Shell/ShellSettings.cs b/src/OrchardCore/OrchardCore.Abstractions/Shell/ShellSettings.cs index e01c5cfcc54..b257d411f46 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Shell/ShellSettings.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Shell/ShellSettings.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Newtonsoft.Json; @@ -14,6 +15,8 @@ namespace OrchardCore.Environment.Shell /// public class ShellSettings { + private static readonly char[] _hostSeparators = new[] { ',', ' ' }; + private readonly ShellConfiguration _settings; private readonly ShellConfiguration _configuration; @@ -50,6 +53,11 @@ public string RequestUrlHost set => _settings["RequestUrlHost"] = value; } + [JsonIgnore] + public string[] RequestUrlHosts => _settings["RequestUrlHost"] + ?.Split(_hostSeparators, StringSplitOptions.RemoveEmptyEntries) + ?? Array.Empty(); + public string RequestUrlPrefix { get => _settings["RequestUrlPrefix"]?.Trim(' ', '/'); diff --git a/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/ContentItemsFieldType.cs b/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/ContentItemsFieldType.cs index 8eae0a1b024..17aca956fd6 100644 --- a/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/ContentItemsFieldType.cs +++ b/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/ContentItemsFieldType.cs @@ -15,7 +15,6 @@ using OrchardCore.ContentManagement.GraphQL.Queries.Predicates; using OrchardCore.ContentManagement.GraphQL.Queries.Types; using OrchardCore.ContentManagement.Records; -using OrchardCore.Environment.Shell; using YesSql; using Expression = OrchardCore.ContentManagement.GraphQL.Queries.Predicates.Expression; @@ -125,8 +124,7 @@ private IQuery FilterWhereArguments( string defaultTableAlias = query.GetTypeAlias(typeof(ContentItemIndex)); IPredicateQuery predicateQuery = new PredicateQuery( - dialect: session.Store.Configuration.SqlDialect, - shellSettings: fieldContext.RequestServices.GetService(), + configuration: session.Store.Configuration, propertyProviders: fieldContext.RequestServices.GetServices()); // Create the default table alias diff --git a/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/Predicates/PredicateQuery.cs b/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/Predicates/PredicateQuery.cs index a37d494fbc1..3aa4c4f4efd 100644 --- a/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/Predicates/PredicateQuery.cs +++ b/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/Predicates/PredicateQuery.cs @@ -1,24 +1,25 @@ using System; using System.Collections.Generic; using System.Linq; -using OrchardCore.Environment.Shell; using YesSql; namespace OrchardCore.ContentManagement.GraphQL.Queries.Predicates { public class PredicateQuery : IPredicateQuery { + private readonly IConfiguration _configuration; private readonly IEnumerable _propertyProviders; - private readonly HashSet _usedAliases = new HashSet(); - private readonly Dictionary _aliases = new Dictionary(); - private readonly Dictionary _tableAliases = new Dictionary(); + + private readonly HashSet _usedAliases = new(); + private readonly Dictionary _aliases = new(); + private readonly Dictionary _tableAliases = new(); public PredicateQuery( - ISqlDialect dialect, - ShellSettings shellSettings, + IConfiguration configuration, IEnumerable propertyProviders) { - Dialect = dialect; + Dialect = configuration.SqlDialect; + _configuration = configuration; _propertyProviders = propertyProviders; } @@ -39,15 +40,29 @@ public string NewQueryParameter(object value) public void CreateAlias(string path, string alias) { - if (path == null) throw new ArgumentNullException(nameof(path)); - if (alias == null) throw new ArgumentNullException(nameof(alias)); + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + if (alias == null) + { + throw new ArgumentNullException(nameof(alias)); + } _aliases[path] = alias; } public void CreateTableAlias(string path, string tableAlias) { - if (path == null) throw new ArgumentNullException(nameof(path)); - if (tableAlias == null) throw new ArgumentNullException(nameof(tableAlias)); + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + if (tableAlias == null) + { + throw new ArgumentNullException(nameof(tableAlias)); + } _tableAliases[path] = tableAlias; } @@ -55,7 +70,10 @@ public void CreateTableAlias(string path, string tableAlias) public void SearchUsedAlias(string propertyPath) { - if (propertyPath == null) throw new ArgumentNullException(nameof(propertyPath)); + if (propertyPath == null) + { + throw new ArgumentNullException(nameof(propertyPath)); + } // Check if there's an alias for the full path // aliasPart.Alias -> AliasFieldIndex.Alias @@ -68,7 +86,7 @@ public void SearchUsedAlias(string propertyPath) var values = propertyPath.Split('.', 2); // if empty prefix, use default (empty alias) - var aliasPath = values.Length == 1 ? string.Empty : values[0]; + var aliasPath = values.Length == 1 ? String.Empty : values[0]; // get the actual index from the alias if (_aliases.TryGetValue(aliasPath, out alias)) @@ -97,11 +115,14 @@ public void SearchUsedAlias(string propertyPath) public string GetColumnName(string propertyPath) { - if (propertyPath == null) throw new ArgumentNullException(nameof(propertyPath)); + if (propertyPath == null) + { + throw new ArgumentNullException(nameof(propertyPath)); + } // Check if there's an alias for the full path // aliasPart.Alias -> AliasFieldIndex.Alias - if (_aliases.TryGetValue(propertyPath, out string alias)) + if (_aliases.TryGetValue(propertyPath, out var alias)) { return Dialect.QuoteForColumnName(alias); } @@ -109,12 +130,12 @@ public string GetColumnName(string propertyPath) var values = propertyPath.Split('.', 2); // if empty prefix, use default (empty alias) - var aliasPath = values.Length == 1 ? string.Empty : values[0]; + var aliasPath = values.Length == 1 ? String.Empty : values[0]; // get the actual index from the alias if (_aliases.TryGetValue(aliasPath, out alias)) { - string tableAlias = _tableAliases[alias]; + var tableAlias = _tableAliases[alias]; // get the index property provider fore the alias var propertyProvider = _propertyProviders.FirstOrDefault(x => x.IndexName.Equals(alias, StringComparison.OrdinalIgnoreCase)); @@ -124,7 +145,7 @@ public string GetColumnName(string propertyPath) { // Switch the given alias in the path with the mapped alias. // aliasPart.alias -> AliasPartIndex.Alias - return Dialect.QuoteForTableName($"{tableAlias}", schema: null) + "." + Dialect.QuoteForColumnName(columnName); + return Dialect.QuoteForTableName($"{tableAlias}", _configuration.Schema) + "." + Dialect.QuoteForColumnName(columnName); } } else @@ -132,7 +153,7 @@ public string GetColumnName(string propertyPath) // no property provider exists; hope sql is case-insensitive (will break postgres; property providers must be supplied for postgres) // Switch the given alias in the path with the mapped alias. // aliasPart.Alias -> AliasPartIndex.alias - return Dialect.QuoteForTableName($"{tableAlias}", schema: null) + "." + Dialect.QuoteForColumnName(values[1]); + return Dialect.QuoteForTableName($"{tableAlias}", _configuration.Schema) + "." + Dialect.QuoteForColumnName(values[1]); } } diff --git a/src/OrchardCore/OrchardCore.Data.Abstractions/DatabaseTableOptions.cs b/src/OrchardCore/OrchardCore.Data.Abstractions/DatabaseTableOptions.cs new file mode 100644 index 00000000000..d0cd2a5f862 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Data.Abstractions/DatabaseTableOptions.cs @@ -0,0 +1,10 @@ +namespace OrchardCore.Data; + +public class DatabaseTableOptions +{ + public string DocumentTable { get; set; } + + public string TableNameSeparator { get; set; } + + public string IdentityColumnSize { get; set; } +} diff --git a/src/OrchardCore/OrchardCore.Data.Abstractions/DbConnectionValidatorContext.cs b/src/OrchardCore/OrchardCore.Data.Abstractions/DbConnectionValidatorContext.cs new file mode 100644 index 00000000000..cc0a32fdb7c --- /dev/null +++ b/src/OrchardCore/OrchardCore.Data.Abstractions/DbConnectionValidatorContext.cs @@ -0,0 +1,40 @@ +using OrchardCore.Environment.Shell; + +namespace OrchardCore.Data; + +public class DbConnectionValidatorContext : IDbConnectionInfo +{ + public DbConnectionValidatorContext(ShellSettings shellSettings) + { + ShellName = shellSettings.Name; + DatabaseProvider = shellSettings["DatabaseProvider"]; + ConnectionString = shellSettings["ConnectionString"]; + TablePrefix = shellSettings["TablePrefix"]; + Schema = shellSettings["Schema"]; + + TableOptions = shellSettings.GetDatabaseTableOptions(); + } + + public DbConnectionValidatorContext(ShellSettings shellSettings, IDbConnectionInfo dbConnectionInfo) + { + ShellName = shellSettings.Name; + DatabaseProvider = dbConnectionInfo.DatabaseProvider; + ConnectionString = dbConnectionInfo.ConnectionString; + TablePrefix = dbConnectionInfo.TablePrefix; + Schema = dbConnectionInfo.Schema; + + TableOptions = shellSettings.GetDatabaseTableOptions(); + } + + public string ShellName { get; } + + public string DatabaseProvider { get; } + + public string ConnectionString { get; } + + public string TablePrefix { get; } + + public string Schema { get; } + + public DatabaseTableOptions TableOptions { get; } +} diff --git a/src/OrchardCore/OrchardCore.Data.Abstractions/IDbConnectionInfo.cs b/src/OrchardCore/OrchardCore.Data.Abstractions/IDbConnectionInfo.cs new file mode 100644 index 00000000000..f4abc6885af --- /dev/null +++ b/src/OrchardCore/OrchardCore.Data.Abstractions/IDbConnectionInfo.cs @@ -0,0 +1,12 @@ +namespace OrchardCore.Data; + +public interface IDbConnectionInfo +{ + string DatabaseProvider { get; } + + string ConnectionString { get; } + + string TablePrefix { get; } + + string Schema { get; } +} diff --git a/src/OrchardCore/OrchardCore.Data.Abstractions/IDbConnectionValidator.cs b/src/OrchardCore/OrchardCore.Data.Abstractions/IDbConnectionValidator.cs index b2c64ee6248..38391b71dd9 100644 --- a/src/OrchardCore/OrchardCore.Data.Abstractions/IDbConnectionValidator.cs +++ b/src/OrchardCore/OrchardCore.Data.Abstractions/IDbConnectionValidator.cs @@ -4,5 +4,5 @@ namespace OrchardCore.Data; public interface IDbConnectionValidator { - Task ValidateAsync(string databaseProvider, string connectionString, string tablePrefix, string shellName); + Task ValidateAsync(DbConnectionValidatorContext context); } diff --git a/src/OrchardCore/OrchardCore.Data.Abstractions/OrchardCore.Data.Abstractions.csproj b/src/OrchardCore/OrchardCore.Data.Abstractions/OrchardCore.Data.Abstractions.csproj index 02f940f5d6b..39235668293 100644 --- a/src/OrchardCore/OrchardCore.Data.Abstractions/OrchardCore.Data.Abstractions.csproj +++ b/src/OrchardCore/OrchardCore.Data.Abstractions/OrchardCore.Data.Abstractions.csproj @@ -15,6 +15,7 @@ + diff --git a/src/OrchardCore/OrchardCore.Data.Abstractions/ShellSettingsExtensions.cs b/src/OrchardCore/OrchardCore.Data.Abstractions/ShellSettingsExtensions.cs new file mode 100644 index 00000000000..bd91bff819f --- /dev/null +++ b/src/OrchardCore/OrchardCore.Data.Abstractions/ShellSettingsExtensions.cs @@ -0,0 +1,97 @@ +using System; +using System.Linq; +using OrchardCore.Environment.Shell; +using OrchardCore.Environment.Shell.Models; + +namespace OrchardCore.Data; + +public static class ShellSettingsExtensions +{ + private const string _databaseTableOptions = "OrchardCore_Data_TableOptions"; + private const string _defaultDocumentTable = $"{_databaseTableOptions}:DefaultDocumentTable"; + private const string _defaultTableNameSeparator = $"{_databaseTableOptions}:DefaultTableNameSeparator"; + private const string _defaultIdentityColumnSize = $"{_databaseTableOptions}:DefaultIdentityColumnSize"; + + private readonly static string[] _identityColumnSizes = new[] { nameof(Int64), nameof(Int32) }; + + public static DatabaseTableOptions GetDatabaseTableOptions(this ShellSettings shellSettings) => + new() + { + DocumentTable = shellSettings.GetDocumentTable(), + TableNameSeparator = shellSettings.GetTableNameSeparator(), + IdentityColumnSize = shellSettings.GetIdentityColumnSize(), + }; + + public static ShellSettings ConfigureDatabaseTableOptions(this ShellSettings shellSettings) + { + if (!shellSettings.IsInitialized()) + { + shellSettings["DocumentTable"] = shellSettings.GetDocumentTable(); + shellSettings["TableNameSeparator"] = shellSettings.GetTableNameSeparator(); + shellSettings["IdentityColumnSize"] = shellSettings.GetIdentityColumnSize(); + } + + return shellSettings; + } + + public static string GetDocumentTable(this ShellSettings shellSettings) + { + var documentTable = (!shellSettings.IsInitialized() + ? shellSettings[_defaultDocumentTable] + : shellSettings["DocumentTable"]) + ?.Trim(); + + if (String.IsNullOrEmpty(documentTable)) + { + documentTable = "Document"; + } + + return documentTable; + } + + public static string GetTableNameSeparator(this ShellSettings shellSettings) + { + var tableNameSeparator = (!shellSettings.IsInitialized() + ? shellSettings[_defaultTableNameSeparator] + : shellSettings["TableNameSeparator"]) + ?.Trim(); + + if (String.IsNullOrEmpty(tableNameSeparator)) + { + tableNameSeparator = "_"; + } + else if (tableNameSeparator == "NULL") + { + tableNameSeparator = String.Empty; + } + else if (tableNameSeparator.Any(c => c != '_')) + { + throw new InvalidOperationException($"The configured table name separator '{tableNameSeparator}' is invalid."); + } + + return tableNameSeparator; + } + + public static string GetIdentityColumnSize(this ShellSettings shellSettings) + { + var identityColumnSize = (!shellSettings.IsInitialized() + ? shellSettings[_defaultIdentityColumnSize] + : shellSettings["IdentityColumnSize"]) + ?.Trim(); + + if (String.IsNullOrEmpty(identityColumnSize)) + { + identityColumnSize = !shellSettings.IsInitialized() ? nameof(Int64) : nameof(Int32); + } + else if (!_identityColumnSizes.Contains(identityColumnSize)) + { + throw new InvalidOperationException($"The configured identity column size '{identityColumnSize}' is invalid."); + } + + return identityColumnSize; + } + + public static bool IsInitialized(this ShellSettings shellSettings) => + shellSettings.State == TenantState.Running || + shellSettings.State == TenantState.Disabled; +} diff --git a/src/OrchardCore/OrchardCore.Data.YesSql.Abstractions/DefaultTableNameConvention.cs b/src/OrchardCore/OrchardCore.Data.YesSql.Abstractions/DefaultTableNameConvention.cs new file mode 100644 index 00000000000..eadc36728fe --- /dev/null +++ b/src/OrchardCore/OrchardCore.Data.YesSql.Abstractions/DefaultTableNameConvention.cs @@ -0,0 +1,31 @@ +using System; +using YesSql; + +namespace OrchardCore.Data; + +public class DefaultTableNameConvention : ITableNameConvention +{ + private readonly DatabaseTableOptions _options; + + public DefaultTableNameConvention(DatabaseTableOptions options) => _options = options; + + public string GetIndexTable(Type type, string collection = null) + { + if (String.IsNullOrEmpty(collection)) + { + return type.Name; + } + + return $"{collection}{_options.TableNameSeparator}{type.Name}"; + } + + public string GetDocumentTable(string collection = null) + { + if (String.IsNullOrEmpty(collection)) + { + return _options.DocumentTable; + } + + return $"{collection}{_options.TableNameSeparator}{_options.DocumentTable}"; + } +} diff --git a/src/OrchardCore/OrchardCore.Data.YesSql.Abstractions/ITableNameConventionFactory.cs b/src/OrchardCore/OrchardCore.Data.YesSql.Abstractions/ITableNameConventionFactory.cs new file mode 100644 index 00000000000..37b63c324df --- /dev/null +++ b/src/OrchardCore/OrchardCore.Data.YesSql.Abstractions/ITableNameConventionFactory.cs @@ -0,0 +1,8 @@ +using YesSql; + +namespace OrchardCore.Data.YesSql; + +public interface ITableNameConventionFactory +{ + ITableNameConvention Create(DatabaseTableOptions options); +} diff --git a/src/OrchardCore/OrchardCore.Data.YesSql.Abstractions/YesSqlOptions.cs b/src/OrchardCore/OrchardCore.Data.YesSql.Abstractions/YesSqlOptions.cs index d875b159126..a0b92c24ae8 100644 --- a/src/OrchardCore/OrchardCore.Data.YesSql.Abstractions/YesSqlOptions.cs +++ b/src/OrchardCore/OrchardCore.Data.YesSql.Abstractions/YesSqlOptions.cs @@ -10,13 +10,9 @@ public class YesSqlOptions public IIdGenerator IdGenerator { get; set; } - public ITableNameConvention TableNameConvention { get; set; } - public IAccessorFactory IdentifierAccessorFactory { get; set; } public IAccessorFactory VersionAccessorFactory { get; set; } public IContentSerializer ContentSerializer { get; set; } - - public string TablePrefixSeparator { get; set; } = "_"; } diff --git a/src/OrchardCore/OrchardCore.Data.YesSql/DbConnectionValidator.cs b/src/OrchardCore/OrchardCore.Data.YesSql/DbConnectionValidator.cs index d6283772fe1..8f583ebd323 100644 --- a/src/OrchardCore/OrchardCore.Data.YesSql/DbConnectionValidator.cs +++ b/src/OrchardCore/OrchardCore.Data.YesSql/DbConnectionValidator.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Microsoft.Data.SqlClient; using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using MySqlConnector; using Npgsql; @@ -26,35 +27,44 @@ public class DbConnectionValidator : IDbConnectionValidator private static readonly string _shellDescriptorTypeColumnValue = new TypeService()[typeof(ShellDescriptor)]; private readonly IEnumerable _databaseProviders; - private readonly YesSqlOptions _yesSqlOptions; + private readonly ITableNameConventionFactory _tableNameConventionFactory; + private readonly ILogger _logger; private readonly SqliteOptions _sqliteOptions; private readonly ShellOptions _shellOptions; public DbConnectionValidator( IEnumerable databaseProviders, - IOptions yesSqlOptions, IOptions sqliteOptions, - IOptions shellOptions) + IOptions shellOptions, + ITableNameConventionFactory tableNameConventionFactory, + ILogger logger) { _databaseProviders = databaseProviders; - _yesSqlOptions = yesSqlOptions.Value; + _tableNameConventionFactory = tableNameConventionFactory; + _logger = logger; _sqliteOptions = sqliteOptions.Value; _shellOptions = shellOptions.Value; } - public async Task ValidateAsync(string databaseProvider, string connectionString, string tablePrefix, string shellName) + public async Task ValidateAsync(DbConnectionValidatorContext context) { - if (String.IsNullOrWhiteSpace(databaseProvider)) + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (String.IsNullOrWhiteSpace(context.DatabaseProvider)) { return DbConnectionValidatorResult.NoProvider; } - var provider = _databaseProviders.FirstOrDefault(x => x.Value == databaseProvider); + var provider = _databaseProviders.FirstOrDefault(provider => provider.Value == context.DatabaseProvider); if (provider == null) { return DbConnectionValidatorResult.UnsupportedProvider; } + var connectionString = context.ConnectionString; if (!provider.HasConnectionString) { if (provider.Value != DatabaseProviderValue.Sqlite) @@ -62,7 +72,7 @@ public async Task ValidateAsync(string databaseProv return DbConnectionValidatorResult.DocumentTableNotFound; } - connectionString = SqliteHelper.GetConnectionString(_sqliteOptions, _shellOptions, shellName); + connectionString = SqliteHelper.GetConnectionString(_sqliteOptions, _shellOptions, context.ShellName); } if (String.IsNullOrWhiteSpace(connectionString)) @@ -70,7 +80,7 @@ public async Task ValidateAsync(string databaseProv return DbConnectionValidatorResult.InvalidConnection; } - var factory = GetFactory(databaseProvider, connectionString); + var factory = GetFactory(context.DatabaseProvider, connectionString); using var connection = factory.CreateConnection(); @@ -78,24 +88,31 @@ public async Task ValidateAsync(string databaseProv { await connection.OpenAsync(); } - catch + catch (Exception ex) { if (provider.Value != DatabaseProviderValue.Sqlite) { + _logger.LogWarning(ex, "Unable to validate connection string."); + return DbConnectionValidatorResult.InvalidConnection; } return DbConnectionValidatorResult.DocumentTableNotFound; } - var selectBuilder = GetSelectBuilderForDocumentTable(tablePrefix, databaseProvider); + var tableNameConvention = _tableNameConventionFactory.Create(context.TableOptions); + var documentName = tableNameConvention.GetDocumentTable(); + + var sqlDialect = GetSqlDialect(context.DatabaseProvider); + var sqlBuilder = GetSqlBuilder(sqlDialect, context.TablePrefix, context.TableOptions.TableNameSeparator); + try { var selectCommand = connection.CreateCommand(); - selectCommand.CommandText = selectBuilder.ToSqlString(); + selectCommand.CommandText = GetSelectBuilderForDocumentTable(sqlBuilder, documentName, context.Schema).ToSqlString(); using var result = await selectCommand.ExecuteReaderAsync(); - if (shellName != ShellHelper.DefaultShellName) + if (context.ShellName != ShellHelper.DefaultShellName) { // The 'Document' table exists. return DbConnectionValidatorResult.DocumentTableFound; @@ -118,11 +135,10 @@ public async Task ValidateAsync(string databaseProv return DbConnectionValidatorResult.DocumentTableNotFound; } - selectBuilder = GetSelectBuilderForShellDescriptorDocument(tablePrefix, databaseProvider); try { var selectCommand = connection.CreateCommand(); - selectCommand.CommandText = selectBuilder.ToSqlString(); + selectCommand.CommandText = GetSelectBuilderForShellDescriptorDocument(sqlBuilder, documentName, context.Schema).ToSqlString(); using var result = await selectCommand.ExecuteReaderAsync(); if (!result.HasRows) @@ -139,29 +155,25 @@ public async Task ValidateAsync(string databaseProv return DbConnectionValidatorResult.DocumentTableFound; } - private ISqlBuilder GetSelectBuilderForDocumentTable(string tablePrefix, string databaseProvider) + private static ISqlBuilder GetSelectBuilderForDocumentTable(ISqlBuilder sqlBuilder, string documentTable, string schema) { - var selectBuilder = GetSqlBuilder(databaseProvider, tablePrefix); - - selectBuilder.Select(); - selectBuilder.Selector("*"); - selectBuilder.Table(_yesSqlOptions.TableNameConvention.GetDocumentTable(), alias: null, schema: null); - selectBuilder.Take("1"); + sqlBuilder.Select(); + sqlBuilder.Selector("*"); + sqlBuilder.Table(documentTable, alias: null, schema); + sqlBuilder.Take("1"); - return selectBuilder; + return sqlBuilder; } - private ISqlBuilder GetSelectBuilderForShellDescriptorDocument(string tablePrefix, string databaseProvider) + private static ISqlBuilder GetSelectBuilderForShellDescriptorDocument(ISqlBuilder sqlBuilder, string documentTable, string schema) { - var selectBuilder = GetSqlBuilder(databaseProvider, tablePrefix); - - selectBuilder.Select(); - selectBuilder.Selector("*"); - selectBuilder.Table(_yesSqlOptions.TableNameConvention.GetDocumentTable(), alias: null, schema: null); - selectBuilder.WhereAnd($"Type = '{_shellDescriptorTypeColumnValue}'"); - selectBuilder.Take("1"); + sqlBuilder.Select(); + sqlBuilder.Selector("*"); + sqlBuilder.Table(documentTable, alias: null, schema); + sqlBuilder.WhereAnd($"Type = '{_shellDescriptorTypeColumnValue}'"); + sqlBuilder.Take("1"); - return selectBuilder; + return sqlBuilder; } private static IConnectionFactory GetFactory(string databaseProvider, string connectionString) @@ -176,9 +188,9 @@ private static IConnectionFactory GetFactory(string databaseProvider, string con }; } - private ISqlBuilder GetSqlBuilder(string databaseProvider, string tablePrefix) + private static ISqlDialect GetSqlDialect(string databaseProvider) { - ISqlDialect dialect = databaseProvider switch + return databaseProvider switch { DatabaseProviderValue.SqlConnection => new SqlServerDialect(), DatabaseProviderValue.MySql => new MySqlDialect(), @@ -186,13 +198,16 @@ private ISqlBuilder GetSqlBuilder(string databaseProvider, string tablePrefix) DatabaseProviderValue.Postgres => new PostgreSqlDialect(), _ => throw new ArgumentOutOfRangeException(nameof(databaseProvider), "Unsupported database provider"), }; + } + private static ISqlBuilder GetSqlBuilder(ISqlDialect sqlDialect, string tablePrefix, string tableNameSeparator) + { var prefix = String.Empty; if (!String.IsNullOrWhiteSpace(tablePrefix)) { - prefix = tablePrefix.Trim() + _yesSqlOptions.TablePrefixSeparator; + prefix = tablePrefix.Trim() + tableNameSeparator; } - return new SqlBuilder(prefix, dialect); + return new SqlBuilder(prefix, sqlDialect); } } diff --git a/src/OrchardCore/OrchardCore.Data.YesSql/OrchardCoreBuilderExtensions.cs b/src/OrchardCore/OrchardCore.Data.YesSql/OrchardCoreBuilderExtensions.cs index 209fa47ca03..e67dffbc176 100644 --- a/src/OrchardCore/OrchardCore.Data.YesSql/OrchardCoreBuilderExtensions.cs +++ b/src/OrchardCore/OrchardCore.Data.YesSql/OrchardCoreBuilderExtensions.cs @@ -3,7 +3,6 @@ using System.Data; using System.IO; using System.Linq; -using Microsoft.Data.Sqlite; using Microsoft.Extensions.Options; using OrchardCore.Data; using OrchardCore.Data.Documents; @@ -39,6 +38,7 @@ public static OrchardCoreBuilder AddDataAccess(this OrchardCoreBuilder builder) services.AddScoped(); services.AddScoped(); + services.AddTransient(); services.AddTransient, SqliteOptionsConfiguration>(); // Adding supported databases @@ -47,9 +47,6 @@ public static OrchardCoreBuilder AddDataAccess(this OrchardCoreBuilder builder) services.TryAddDataProvider(name: "MySql", value: DatabaseProviderValue.MySql, hasConnectionString: true, sampleConnectionString: "Server=localhost;Database=Orchard;Uid=username;Pwd=password", hasTablePrefix: true, isDefault: false); services.TryAddDataProvider(name: "Postgres", value: DatabaseProviderValue.Postgres, hasConnectionString: true, sampleConnectionString: "Server=localhost;Port=5432;Database=Orchard;User Id=username;Password=password", hasTablePrefix: true, isDefault: false); - // Ensure a non null 'TableNameConvention' to be always used for `YesSql.Configuration` and then in sync with it. - services.PostConfigure(o => o.TableNameConvention ??= new YesSql.Configuration().TableNameConvention); - // Configuring data access services.AddSingleton(sp => { @@ -62,13 +59,14 @@ public static OrchardCoreBuilder AddDataAccess(this OrchardCoreBuilder builder) } var yesSqlOptions = sp.GetService>().Value; - var storeConfiguration = GetStoreConfiguration(sp, yesSqlOptions); + var databaseTableOptions = shellSettings.GetDatabaseTableOptions(); + var storeConfiguration = GetStoreConfiguration(sp, yesSqlOptions, databaseTableOptions); switch (shellSettings["DatabaseProvider"]) { case DatabaseProviderValue.SqlConnection: storeConfiguration - .UseSqlServer(shellSettings["ConnectionString"], IsolationLevel.ReadUncommitted) + .UseSqlServer(shellSettings["ConnectionString"], IsolationLevel.ReadUncommitted, shellSettings["Schema"]) .UseBlockIdGenerator(); break; case DatabaseProviderValue.Sqlite: @@ -85,12 +83,12 @@ public static OrchardCoreBuilder AddDataAccess(this OrchardCoreBuilder builder) break; case DatabaseProviderValue.MySql: storeConfiguration - .UseMySql(shellSettings["ConnectionString"], IsolationLevel.ReadUncommitted) + .UseMySql(shellSettings["ConnectionString"], IsolationLevel.ReadUncommitted, shellSettings["Schema"]) .UseBlockIdGenerator(); break; case DatabaseProviderValue.Postgres: storeConfiguration - .UsePostgreSql(shellSettings["ConnectionString"], IsolationLevel.ReadUncommitted) + .UsePostgreSql(shellSettings["ConnectionString"], IsolationLevel.ReadUncommitted, shellSettings["Schema"]) .UseBlockIdGenerator(); break; default: @@ -99,7 +97,8 @@ public static OrchardCoreBuilder AddDataAccess(this OrchardCoreBuilder builder) if (!String.IsNullOrWhiteSpace(shellSettings["TablePrefix"])) { - var tablePrefix = shellSettings["TablePrefix"].Trim() + yesSqlOptions.TablePrefixSeparator; + var tablePrefix = shellSettings["TablePrefix"].Trim() + databaseTableOptions.TableNameSeparator; + storeConfiguration = storeConfiguration.SetTablePrefix(tablePrefix); } @@ -151,21 +150,23 @@ public static OrchardCoreBuilder AddDataAccess(this OrchardCoreBuilder builder) services.AddScoped(); services.AddSingleton(); - services.AddTransient(); }); return builder; } - private static IConfiguration GetStoreConfiguration(IServiceProvider sp, YesSqlOptions yesSqlOptions) + private static IConfiguration GetStoreConfiguration(IServiceProvider sp, YesSqlOptions yesSqlOptions, DatabaseTableOptions databaseTableOptions) { + var tableNameFactory = sp.GetRequiredService(); + var storeConfiguration = new YesSql.Configuration { CommandsPageSize = yesSqlOptions.CommandsPageSize, QueryGatingEnabled = yesSqlOptions.QueryGatingEnabled, ContentSerializer = new PoolingJsonContentSerializer(sp.GetService>()), - TableNameConvention = yesSqlOptions.TableNameConvention + TableNameConvention = tableNameFactory.Create(databaseTableOptions), + IdentityColumnSize = Enum.Parse(databaseTableOptions.IdentityColumnSize), }; if (yesSqlOptions.IdGenerator != null) diff --git a/src/OrchardCore/OrchardCore.Data.YesSql/TableNameConventionFactory.cs b/src/OrchardCore/OrchardCore.Data.YesSql/TableNameConventionFactory.cs new file mode 100644 index 00000000000..3bf4698bcd3 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Data.YesSql/TableNameConventionFactory.cs @@ -0,0 +1,12 @@ +using OrchardCore.Data.YesSql; +using YesSql; + +namespace OrchardCore.Data; + +public class TableNameConventionFactory : ITableNameConventionFactory +{ + public ITableNameConvention Create(DatabaseTableOptions options) + { + return new DefaultTableNameConvention(options); + } +} diff --git a/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Configuration/DatabaseShellsStorageOptions.cs b/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Configuration/DatabaseShellsStorageOptions.cs index f97718d2ac6..695349aa124 100644 --- a/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Configuration/DatabaseShellsStorageOptions.cs +++ b/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Configuration/DatabaseShellsStorageOptions.cs @@ -2,11 +2,12 @@ namespace OrchardCore.Shells.Database.Configuration { - public class DatabaseShellsStorageOptions + public class DatabaseShellsStorageOptions : IDbConnectionInfo { public bool MigrateFromFiles { get; set; } public string DatabaseProvider { get; set; } public string ConnectionString { get; set; } public string TablePrefix { get; set; } + public string Schema { get; set; } } } diff --git a/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Extensions/DatabaseShellContextFactoryExtensions.cs b/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Extensions/DatabaseShellContextFactoryExtensions.cs index 78ff11d4a32..9dfb437dc2e 100644 --- a/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Extensions/DatabaseShellContextFactoryExtensions.cs +++ b/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Extensions/DatabaseShellContextFactoryExtensions.cs @@ -27,6 +27,7 @@ internal static Task GetDatabaseContextAsync(this IShellContextFac settings["DatabaseProvider"] = options.DatabaseProvider; settings["ConnectionString"] = options.ConnectionString; settings["TablePrefix"] = options.TablePrefix; + settings["Schema"] = options.Schema; return shellContextFactory.CreateDescribedContextAsync(settings, new ShellDescriptor()); } diff --git a/src/OrchardCore/OrchardCore.Setup.Core/SetupService.cs b/src/OrchardCore/OrchardCore.Setup.Core/SetupService.cs index d0d1566d521..1c1d119a6ca 100644 --- a/src/OrchardCore/OrchardCore.Setup.Core/SetupService.cs +++ b/src/OrchardCore/OrchardCore.Setup.Core/SetupService.cs @@ -155,16 +155,17 @@ private async Task SetupInternalAsync(SetupContext context) _httpContextAccessor.HttpContext.Features.Set(recipeEnvironmentFeature); - var shellSettings = new ShellSettings(context.ShellSettings); - + var shellSettings = new ShellSettings(context.ShellSettings).ConfigureDatabaseTableOptions(); if (String.IsNullOrWhiteSpace(shellSettings["DatabaseProvider"])) { shellSettings["DatabaseProvider"] = context.Properties.TryGetValue(SetupConstants.DatabaseProvider, out var databaseProvider) ? databaseProvider?.ToString() : String.Empty; shellSettings["ConnectionString"] = context.Properties.TryGetValue(SetupConstants.DatabaseConnectionString, out var databaseConnectionString) ? databaseConnectionString?.ToString() : String.Empty; shellSettings["TablePrefix"] = context.Properties.TryGetValue(SetupConstants.DatabaseTablePrefix, out var databaseTablePrefix) ? databaseTablePrefix?.ToString() : String.Empty; + shellSettings["Schema"] = context.Properties.TryGetValue(SetupConstants.DatabaseSchema, out var schema) ? schema?.ToString() : null; } - switch (await _dbConnectionValidator.ValidateAsync(shellSettings["DatabaseProvider"], shellSettings["ConnectionString"], shellSettings["TablePrefix"], shellSettings.Name)) + var validationContext = new DbConnectionValidatorContext(shellSettings); + switch (await _dbConnectionValidator.ValidateAsync(validationContext)) { case DbConnectionValidatorResult.NoProvider: context.Errors.Add(String.Empty, S["DatabaseProvider setting is required."]); @@ -176,7 +177,7 @@ private async Task SetupInternalAsync(SetupContext context) context.Errors.Add(String.Empty, S["The provided connection string is invalid or server is unreachable."]); break; case DbConnectionValidatorResult.DocumentTableFound: - context.Errors.Add(String.Empty, S["The provided database and table prefix are already in use."]); + context.Errors.Add(String.Empty, S["The provided database, table prefix and schema are already in use."]); break; } diff --git a/src/docs/reference/core/Data/README.md b/src/docs/reference/core/Data/README.md index 982cad6e1ac..01a4edf08be 100644 --- a/src/docs/reference/core/Data/README.md +++ b/src/docs/reference/core/Data/README.md @@ -26,27 +26,46 @@ See the [`Microsoft.Data.Sqlite` documentation](https://docs.microsoft.com/en-us ## Configuring YesSql -OrchardCore uses the `YesSql` library to interact with the configured database provider. `YesSql` is shipped with configuration that is suitable for most use cases. However, you can change these settings by configuring `YesSqlOptions`. `YesSqlOptions` provides the following configurable options +OrchardCore uses the `YesSql` library to interact with the configured database provider. `YesSql` is shipped with configuration that is suitable for most use cases. However, you can change these settings by configuring `YesSqlOptions`. `YesSqlOptions` provides the following configurable options. | Setting | Description | | --- | --- | | `CommandsPageSize` | Gets or sets the command page size. If you have to many queries in one command, `YesSql` will split the large command into multiple commands. | -| `ContentTypeOptions` | Gets or sets the `QueryGatingEnabled` option in `YesSql`. | -| `TableNameConvention` | You can provide your own implementation for generating ids. | +| `QueryGatingEnabled` | Gets or sets the `QueryGatingEnabled` option in `YesSql`. | +| `IdGenerator` | You can provide your own implementation for generating ids. | | `IdentifierAccessorFactory` | You can provide your own value accessor factory. | | `VersionAccessorFactory` | You can provide your own version accessor factory. | | `ContentSerializer` | You can provide your own content serializer. | -| `TablePrefixSeparator` | Gets or sets the prefix used to seperate database prefix and the table name. | -For example, you can change the default table prefix seperator from `_` to `__` by adding the following code to your startup code. +For example, you can change the default command-page-size from `500` to `1000` by adding the following code to your startup code. ```C# services.Configure(options => { - options.TablePrefixSeparator = "__"; + options.CommandsPageSize = 1000; }); ``` +## Database table + +The following database table settings, only used as presets before a given tenant is setup, can be provided from any configuration source. + +| Setting | Description | +| --- | --- | +| `DefaultDocumentTable` | Document table name, defaults to 'Document'. | +| `DefaultTableNameSeparator` | Table name separator, one or multiple '_', "NULL" means no separator, defaults to '_'. | +| `DefaultIdentityColumnSize` | Identity column size, 'Int32' or 'Int64', defaults to 'Int64'. | + +##### `appsettings.json` + +```json + "OrchardCore_Data_TableOptions": { + "DefaultDocumentTable": "Document", + "DefaultTableNameSeparator": "_", + "DefaultIdentityColumnSize": "Int64" +} +``` + ## Running SQL queries ### Creating a `DbConnection` instance diff --git a/test/OrchardCore.Tests/Apis/Context/SiteContext.cs b/test/OrchardCore.Tests/Apis/Context/SiteContext.cs index cf6348e2b83..674dbf4abdf 100644 --- a/test/OrchardCore.Tests/Apis/Context/SiteContext.cs +++ b/test/OrchardCore.Tests/Apis/Context/SiteContext.cs @@ -10,7 +10,6 @@ using OrchardCore.BackgroundTasks; using OrchardCore.ContentManagement; using OrchardCore.Environment.Shell; -using OrchardCore.Environment.Shell.Builders; using OrchardCore.Environment.Shell.Scope; using OrchardCore.Recipes.Services; using OrchardCore.Search.Lucene; @@ -22,6 +21,7 @@ public class SiteContext : IDisposable private static readonly TablePrefixGenerator TablePrefixGenerator = new TablePrefixGenerator(); public static OrchardTestFixture Site { get; } public static IShellHost ShellHost { get; private set; } + public static IShellSettingsManager ShellSettingsManager { get; private set; } public static IHttpContextAccessor HttpContextAccessor { get; } public static HttpClient DefaultTenantClient { get; } @@ -38,6 +38,7 @@ static SiteContext() { Site = new OrchardTestFixture(); ShellHost = Site.Services.GetRequiredService(); + ShellSettingsManager = Site.Services.GetRequiredService(); HttpContextAccessor = Site.Services.GetRequiredService(); DefaultTenantClient = Site.CreateDefaultClient(); } @@ -54,7 +55,8 @@ public virtual async Task InitializeAsync() ConnectionString = ConnectionString, RecipeName = RecipeName, Name = tenantName, - RequestUrlPrefix = tenantName + RequestUrlPrefix = tenantName, + Schema = null, }; var createResult = await DefaultTenantClient.PostAsJsonAsync("api/tenants/create", createModel); @@ -75,7 +77,7 @@ public virtual async Task InitializeAsync() UserName = "admin", Password = "Password01_", Name = tenantName, - Email = "Nick@Orchard" + Email = "Nick@Orchard", }; var setupResult = await DefaultTenantClient.PostAsJsonAsync("api/tenants/setup", setupModel); diff --git a/test/OrchardCore.Tests/Modules/OrchardCore.Tenants/Services/TenantValidatorTests.cs b/test/OrchardCore.Tests/Modules/OrchardCore.Tenants/Services/TenantValidatorTests.cs index cd317154016..25a286044d5 100644 --- a/test/OrchardCore.Tests/Modules/OrchardCore.Tenants/Services/TenantValidatorTests.cs +++ b/test/OrchardCore.Tests/Modules/OrchardCore.Tenants/Services/TenantValidatorTests.cs @@ -27,18 +27,18 @@ static TenantValidatorTests() [InlineData("Tenant5", "tenant3", "", "Feature Profile", new[] { "A tenant with the same host and prefix already exists." })] [InlineData("Tenant5", "tenant3", null, "Feature Profile", new[] { "A tenant with the same host and prefix already exists." })] [InlineData("Tenant5", "", "example2.com", "Feature Profile", new[] { "A tenant with the same host and prefix already exists." })] + [InlineData("Tenant5", null, "example2.com", "Feature Profile", new[] { "A tenant with the same host and prefix already exists." })] [InlineData("Tenant6", "tenant4", "example4.com", "Feature Profile", new[] { "A tenant with the same host and prefix already exists." })] [InlineData("", "tenant7", "example1.com", "Feature Profile", new[] { "The tenant name is mandatory." })] [InlineData("Tenant7", "tenant7", "", "Feature", new[] { "The feature profile does not exist." })] [InlineData("@Invalid Tenant", "tenant7", "example1.com", "Feature Profile", new[] { "Invalid tenant name. Must contain characters only and no spaces." })] - [InlineData("Tenant7", null, " ", "Feature Profile", new[] { "Host and url prefix can not be empty at the same time." })] + [InlineData("Tenant7", null, " ", "Feature Profile", new[] { "Host and url prefix can not be empty at the same time.", "A tenant with the same host and prefix already exists." })] [InlineData("Tenant7", "/tenant7", "", "Feature Profile", new[] { "The url prefix can not contain more than one segment." })] [InlineData("@Invalid Tenant", "/tenant7", "", "Feature Profile", new[] { "Invalid tenant name. Must contain characters only and no spaces.", "The url prefix can not contain more than one segment." })] [InlineData("Tenant8", "tenant4", "example6.com,example4.com, example5.com", "Feature Profile", new[] { "A tenant with the same host and prefix already exists." })] [InlineData("Tenant9", "tenant9", "", "Feature Profile", new string[] { })] [InlineData("Tenant9", "", "example6.com", "Feature Profile", new string[] { })] [InlineData("Tenant9", "tenant9", "example6.com", "Feature Profile", new string[] { })] - [InlineData("Tenant9", null, "example2.com", "Feature Profile", new string[] { })] public async Task TenantValidationFailsIfInvalidConfigurationsWasProvided(string name, string urlPrefix, string hostName, string featureProfile, string[] errorMessages) { // Arrange @@ -92,8 +92,17 @@ public async Task DuplicateTenantHostOrPrefixShouldFailValidation(bool isNewTena var errors = await tenantValidator.ValidateAsync(viewModel); // Assert - Assert.Single(errors); - Assert.Equal("A tenant with the same host and prefix already exists.", errors.Single().Message); + if (isNewTenant) + { + Assert.Single(errors); + Assert.Equal("A tenant with the same host and prefix already exists.", errors.Single().Message); + } + else + { + Assert.Equal(2, errors.Count()); + Assert.Equal("A tenant with the same host and prefix already exists.", errors.ElementAt(0).Message); + Assert.Equal("The existing tenant to be validated was not found.", errors.ElementAt(1).Message); + } } private static TenantValidator CreateTenantValidator(bool defaultTenant = true) @@ -117,24 +126,26 @@ private static TenantValidator CreateTenantValidator(bool defaultTenant = true) ? ShellHost.GetSettings(ShellHelper.DefaultShellName) : new ShellSettings(); - var connectionFactory = new Mock(); - connectionFactory.Setup(l => l.ValidateAsync(shellSettings["ProviderName"], shellSettings["ConnectionName"], shellSettings["TablePrefix"], shellSettings.Name)); + var dbConnectionValidatorMock = new Mock(); + var validationContext = new DbConnectionValidatorContext(shellSettings); + + dbConnectionValidatorMock.Setup(v => v.ValidateAsync(validationContext)); return new TenantValidator( ShellHost, + ShellSettingsManager, featureProfilesServiceMock.Object, - connectionFactory.Object, + dbConnectionValidatorMock.Object, stringLocalizerMock.Object ); } private static async Task SeedTenantsAsync() { - await ShellHost.GetOrCreateShellContextAsync(new ShellSettings { Name = "Tenant1", State = TenantState.Uninitialized }); - await ShellHost.GetOrCreateShellContextAsync(new ShellSettings { Name = "Tenant2", State = TenantState.Uninitialized, RequestUrlPrefix = String.Empty, RequestUrlHost = "example2.com" }); + await ShellHost.GetOrCreateShellContextAsync(new ShellSettings { Name = "Tenant1", State = TenantState.Uninitialized, RequestUrlPrefix = "tenant1" }); await ShellHost.GetOrCreateShellContextAsync(new ShellSettings { Name = "Tenant2", State = TenantState.Uninitialized, RequestUrlPrefix = String.Empty, RequestUrlHost = "example2.com" }); await ShellHost.GetOrCreateShellContextAsync(new ShellSettings { Name = "Tenant3", State = TenantState.Uninitialized, RequestUrlPrefix = "tenant3", RequestUrlHost = String.Empty }); - await ShellHost.GetOrCreateShellContextAsync(new ShellSettings { Name = "Tenant4", State = TenantState.Uninitialized, RequestUrlPrefix = "tenant4", RequestUrlHost = "example4.com,example5.com" }); + await ShellHost.GetOrCreateShellContextAsync(new ShellSettings { Name = "Tenant4", State = TenantState.Uninitialized, RequestUrlPrefix = "tenant4", RequestUrlHost = "example4.com, example5.com" }); } } }