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

OFFI-92: Adding defaults for hosting-related configuration #277

Merged
merged 18 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Lombiq.HelpfulLibraries.AspNetCore/Docs/Extensions.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace Microsoft.Extensions.Configuration;

/// <summary>
/// Shortcuts for <see cref="IConfigurationSection"/> operations.
/// </summary>
public static class ConfigurationSectionExtensions
{
/// <summary>
/// Adds a value to a configuration section if the key doesn't exist yet.
/// </summary>
/// <param name="key">The key of the configuration.</param>
/// <param name="value">The value of the configuration.</param>
public static IConfigurationSection AddValueIfKeyNotExists(
this IConfigurationSection configurationSection,
string key,
string value)
{
configurationSection[key] ??= value;
return configurationSection;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
namespace Microsoft.Extensions.Configuration;

public static class ConfigurationExtensions
public static class AzureConfigurationExtensions
{
public const string IsAzureHostingKey = "IsAzureHosting";

/// <summary>
/// Retrieves a value indicating whether the <c>OrchardCore:IsAzureHosting</c> configuration key is set to <see
/// langword="true"/>.
/// </summary>
public static bool IsAzureHosting(
this IConfiguration configuration) =>
configuration.GetValue<bool>("OrchardCore:IsAzureHosting");
configuration.GetValue<bool>("OrchardCore:" + IsAzureHostingKey);
}
1 change: 1 addition & 0 deletions Lombiq.HelpfulLibraries.OrchardCore/Docs/Environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
## Extensions

- `FeatureInfoEnumerableExtensions`: Shortcuts for `IEnumerable<IFeatureInfo>`, 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()`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

namespace Microsoft.Extensions.DependencyInjection;

public static class HostingDefaultsOrchardCoreBuilderExtensions
{
/// <summary>
/// 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.
/// </summary>
/// <param name="webApplicationBuilder">The <see cref="WebApplicationBuilder"/> instance of the app.</param>
/// <param name="hostingConfiguration">Configuration for the hosting defaults.</param>
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", "[email protected]");

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", "[email protected]");

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

/// <summary>
/// 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.
/// </summary>
/// <param name="webApplicationBuilder">The <see cref="WebApplicationBuilder"/> instance of the app.</param>
/// <param name="hostingConfiguration">Configuration for the hosting defaults.</param>
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
{
/// <summary>
/// Gets or sets a value indicating whether to always enable <c>OrchardCore.HealthChecks</c> and its dependencies in
/// the Production environment, for all tenants, without the ability to turn them off.
/// </summary>
public bool AlwaysEnableHealthChecksInProduction { get; set; } = true;
}

public class AzureHostingConfiguration : HostingConfiguration
{
/// <summary>
/// Gets or sets a value indicating whether to always enable <c>OrchardCore.Media.Azure.Storage</c> and its
/// dependencies when hosted in Azure, for all tenants, without the ability to turn them off.
/// </summary>
public bool AlwaysEnableAzureMediaStorage { get; set; } = true;
}