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;
+}