diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Docs/Extensions.md b/Lombiq.HelpfulLibraries.AspNetCore/Docs/Extensions.md index 86bdcb4d..0dccc3ab 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Docs/Extensions.md +++ b/Lombiq.HelpfulLibraries.AspNetCore/Docs/Extensions.md @@ -1,5 +1,6 @@ # Lombiq Helpful Libraries - ASP.NET Core Libraries - Extensions +- `ConfigurationSectionExtensions`: Provides shortcuts for `IConfigurationSection` operations. - `CookieHttpContextExtensions`: Provides shortcuts for some cookie-related operations. - `DateTimeHttpContextExtensions`: Makes it possible to set or get IANA time-zone IDs in the HTTP context. - `EnvironmentHttpContextExtensions`: Provides shortcuts to determine information about the current hosting environment, like whether the app is running in Development mode. diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Extensions/ConfigurationSectionExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Extensions/ConfigurationSectionExtensions.cs new file mode 100644 index 00000000..dc972ff5 --- /dev/null +++ b/Lombiq.HelpfulLibraries.AspNetCore/Extensions/ConfigurationSectionExtensions.cs @@ -0,0 +1,21 @@ +namespace Microsoft.Extensions.Configuration; + +/// +/// Shortcuts for operations. +/// +public static class ConfigurationSectionExtensions +{ + /// + /// Adds a value to a configuration section if the key doesn't exist yet. + /// + /// The key of the configuration. + /// The value of the configuration. + public static IConfigurationSection AddValueIfKeyNotExists( + this IConfigurationSection configurationSection, + string key, + string value) + { + configurationSection[key] ??= value; + return configurationSection; + } +} diff --git a/Lombiq.HelpfulLibraries.Common/Extensions/ConfigurationExtensions.cs b/Lombiq.HelpfulLibraries.Common/Extensions/AzureConfigurationExtensions.cs similarity index 62% rename from Lombiq.HelpfulLibraries.Common/Extensions/ConfigurationExtensions.cs rename to Lombiq.HelpfulLibraries.Common/Extensions/AzureConfigurationExtensions.cs index 0ab3cddf..9a3fe00f 100644 --- a/Lombiq.HelpfulLibraries.Common/Extensions/ConfigurationExtensions.cs +++ b/Lombiq.HelpfulLibraries.Common/Extensions/AzureConfigurationExtensions.cs @@ -1,12 +1,14 @@ namespace Microsoft.Extensions.Configuration; -public static class ConfigurationExtensions +public static class AzureConfigurationExtensions { + public const string IsAzureHostingKey = "IsAzureHosting"; + /// /// Retrieves a value indicating whether the OrchardCore:IsAzureHosting configuration key is set to . /// public static bool IsAzureHosting( this IConfiguration configuration) => - configuration.GetValue("OrchardCore:IsAzureHosting"); + configuration.GetValue("OrchardCore:" + IsAzureHostingKey); } diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Docs/Environment.md b/Lombiq.HelpfulLibraries.OrchardCore/Docs/Environment.md index 5f008f48..94ce8989 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Docs/Environment.md +++ b/Lombiq.HelpfulLibraries.OrchardCore/Docs/Environment.md @@ -3,5 +3,6 @@ ## Extensions - `FeatureInfoEnumerableExtensions`: Shortcuts for `IEnumerable`, like `Any(featureId)`. +- `HostingDefaultsOrchardCoreBuilderExtensions`: Lombiq-recommended opinionated default configuration for features of a standard Orchard Core application, including one hosted in Azure. It substitutes much of what you'd write as configuration in a `Program` class or _appsettings.json_ files. - `OrchardCoreBuilderExtensions`: Shortcuts that can be used when initializing Orchard with `OrchardCoreBuilder`, e.g. `AddOrchardCms()`. - `ShellFeaturesManagerExtensions`: Shortcuts for `IShellFeaturesManager`, like `IsFeatureEnabledAsync()`. diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Environment/HostingDefaultsOrchardCoreBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Environment/HostingDefaultsOrchardCoreBuilderExtensions.cs new file mode 100644 index 00000000..3431ee7b --- /dev/null +++ b/Lombiq.HelpfulLibraries.OrchardCore/Environment/HostingDefaultsOrchardCoreBuilderExtensions.cs @@ -0,0 +1,195 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class HostingDefaultsOrchardCoreBuilderExtensions +{ + /// + /// Lombiq-recommended opinionated default configuration for features of a standard Orchard Core application. If + /// any of the configuration values exist, they won't be overridden, so e.g. appsettings.json configuration will + /// take precedence. + /// + /// The instance of the app. + /// Configuration for the hosting defaults. + public static OrchardCoreBuilder ConfigureHostingDefaults( + this OrchardCoreBuilder builder, + WebApplicationBuilder webApplicationBuilder, + HostingConfiguration hostingConfiguration = null) + { + hostingConfiguration ??= new HostingConfiguration(); + + // Not using static type references for the names here because those practically never change, but we'd need to + // add project/package references to all the affected projects. + + var ocSection = webApplicationBuilder.Configuration.GetSection("OrchardCore"); + + ocSection.GetSection("OrchardCore_Tenants").AddValueIfKeyNotExists("TenantRemovalAllowed", "true"); + + ocSection.GetSection("OrchardCore_Localization_CultureOptions").AddValueIfKeyNotExists("IgnoreSystemSettings", "true"); + + var shellsDatabaseSection = ocSection.GetSection("OrchardCore_Shells_Database"); + + shellsDatabaseSection.AddValueIfKeyNotExists("DatabaseProvider", "SqlConnection"); + shellsDatabaseSection.AddValueIfKeyNotExists("TablePrefix", "Shells"); + + ocSection.GetSection("OrchardCore_Tenants").AddValueIfKeyNotExists("TenantRemovalAllowed", "true"); + + var logLevelSection = webApplicationBuilder.Configuration.GetSection("Logging:LogLevel"); + var elasticSearchSection = ocSection.GetSection("OrchardCore_Elasticsearch"); + + if (webApplicationBuilder.Environment.IsDevelopment()) + { + logLevelSection + .AddValueIfKeyNotExists("Default", "Debug") + .AddValueIfKeyNotExists("System", "Information") + .AddValueIfKeyNotExists("Microsoft", "Information"); + + // Orchard Core 1.8 and prior, this can be removed after an Orchard Core upgrade to 2.0. + // OrchardCore_Email_Smtp below is 2.0+. + var oc18SmtpSection = ocSection.GetSection("SmtpSettings"); + + if (oc18SmtpSection["Host"] == null) + { + oc18SmtpSection["Host"] = "127.0.0.1"; + oc18SmtpSection["RequireCredentials"] = "false"; + oc18SmtpSection["Port"] = "25"; + } + + oc18SmtpSection.AddValueIfKeyNotExists("DefaultSender", "sender@example.com"); + + var smtpSection = ocSection.GetSection("OrchardCore_Email_Smtp"); + + if (smtpSection["Host"] == null) + { + smtpSection["Host"] = "127.0.0.1"; + smtpSection["RequireCredentials"] = "false"; + smtpSection["Port"] = "25"; + } + + smtpSection.AddValueIfKeyNotExists("DefaultSender", "sender@example.com"); + + if (elasticSearchSection["Url"] == null) + { + elasticSearchSection["ConnectionType"] = "SingleNodeConnectionPool"; + elasticSearchSection["Url"] = "http://localhost"; + elasticSearchSection["Ports:0"] = "9200"; + elasticSearchSection["Username"] = "admin"; + elasticSearchSection["Password"] = "admin"; + } + } + else + { + logLevelSection + .AddValueIfKeyNotExists("Default", "Warning") + .AddValueIfKeyNotExists("Microsoft.AspNetCore", "Warning"); + + ocSection.AddValueIfKeyNotExists("DatabaseProvider", "SqlConnection"); + + // Elastic Cloud configuration if none is provided. The Url and Password are still needed. + if (elasticSearchSection["ConnectionType"] == null && + elasticSearchSection["Ports"] == null && + elasticSearchSection["Username"] == null) + { + elasticSearchSection["ConnectionType"] = "CloudConnectionPool"; + elasticSearchSection["Ports:0"] = "9243"; + elasticSearchSection["Username"] = "elastic"; + } + } + + if (hostingConfiguration.AlwaysEnableHealthChecksInProduction && webApplicationBuilder.Environment.IsProduction()) + { + builder.AddTenantFeatures("OrchardCore.HealthChecks"); + } + + builder + .AddDatabaseShellsConfigurationIfAvailable(webApplicationBuilder.Configuration) + .ConfigureSmtpSettings(overrideAdminSettings: false) + .ConfigureSecurityDefaultsWithStaticFiles(allowInlineStyle: true); + + return builder; + } + + /// + /// Lombiq-recommended opinionated default configuration for features of an Orchard Core application hosted in + /// Azure. If any of the configuration values exist, they won't be overridden, so e.g. appsettings.json + /// configuration will take precedence. + /// + /// The instance of the app. + /// Configuration for the hosting defaults. + public static OrchardCoreBuilder ConfigureAzureHostingDefaults( + this OrchardCoreBuilder builder, + WebApplicationBuilder webApplicationBuilder, + AzureHostingConfiguration hostingConfiguration = null) + { + hostingConfiguration ??= new AzureHostingConfiguration(); + + builder.ConfigureHostingDefaults(webApplicationBuilder, hostingConfiguration); + + var ocSection = webApplicationBuilder.Configuration.GetSection("OrchardCore"); + + if (!webApplicationBuilder.Environment.IsDevelopment()) + { + ocSection.AddValueIfKeyNotExists(AzureConfigurationExtensions.IsAzureHostingKey, "true"); + } + + if (webApplicationBuilder.Configuration.IsAzureHosting()) + { + builder + .AddTenantFeatures( + "OrchardCore.DataProtection.Azure", + "Lombiq.Hosting.BuildVersionDisplay") + .DisableResourceDebugMode(); + + if (hostingConfiguration.AlwaysEnableAzureMediaStorage) + { + // Azure Media Storage and its dependencies. Keep this updated with Orchard upgrades. + builder.AddTenantFeatures( + "OrchardCore.Contents", + "OrchardCore.ContentTypes", + "OrchardCore.Liquid", + "OrchardCore.Media", + "OrchardCore.Media.Azure.Storage", + "OrchardCore.Media.Cache", + "OrchardCore.Settings"); + } + } + + var mediaSection = ocSection.GetSection("OrchardCore_Media_Azure"); + + mediaSection.AddValueIfKeyNotExists("ContainerName", "media"); + mediaSection.AddValueIfKeyNotExists("BasePath", "{{ ShellSettings.Name }}"); + + if (webApplicationBuilder.Environment.IsDevelopment()) + { + var dataProtectionSection = ocSection.GetSection("OrchardCore_DataProtection_Azure"); + + dataProtectionSection.AddValueIfKeyNotExists("CreateContainer", "true"); + dataProtectionSection.AddValueIfKeyNotExists("ConnectionString", "UseDevelopmentStorage=true"); + + mediaSection.AddValueIfKeyNotExists("CreateContainer", "true"); + mediaSection.AddValueIfKeyNotExists("ConnectionString", "UseDevelopmentStorage=true"); + } + + return builder; + } +} + +public class HostingConfiguration +{ + /// + /// Gets or sets a value indicating whether to always enable OrchardCore.HealthChecks and its dependencies in + /// the Production environment, for all tenants, without the ability to turn them off. + /// + public bool AlwaysEnableHealthChecksInProduction { get; set; } = true; +} + +public class AzureHostingConfiguration : HostingConfiguration +{ + /// + /// Gets or sets a value indicating whether to always enable OrchardCore.Media.Azure.Storage and its + /// dependencies when hosted in Azure, for all tenants, without the ability to turn them off. + /// + public bool AlwaysEnableAzureMediaStorage { get; set; } = true; +}