diff --git a/Directory.Packages.props b/Directory.Packages.props index de7acc506..027cfe16b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,6 +5,7 @@ + diff --git a/src/Core/Grand.Infrastructure/Configuration/FeatureFlagsConfig.cs b/src/Core/Grand.Infrastructure/Configuration/FeatureFlagsConfig.cs deleted file mode 100644 index 30d1b55b7..000000000 --- a/src/Core/Grand.Infrastructure/Configuration/FeatureFlagsConfig.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Grand.Infrastructure.Configuration; - -public class FeatureFlagsConfig -{ - public Dictionary Modules { get; set; } = new Dictionary(); -} diff --git a/src/Core/Grand.Infrastructure/Grand.Infrastructure.csproj b/src/Core/Grand.Infrastructure/Grand.Infrastructure.csproj index 99d6fd79d..072467ff0 100644 --- a/src/Core/Grand.Infrastructure/Grand.Infrastructure.csproj +++ b/src/Core/Grand.Infrastructure/Grand.Infrastructure.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Core/Grand.Infrastructure/Modules/ModuleLoader.cs b/src/Core/Grand.Infrastructure/Modules/ModuleLoader.cs index 0296c0583..8d5e00730 100644 --- a/src/Core/Grand.Infrastructure/Modules/ModuleLoader.cs +++ b/src/Core/Grand.Infrastructure/Modules/ModuleLoader.cs @@ -11,31 +11,43 @@ namespace Grand.Infrastructure.Modules; public static class ModuleLoader { - private const string MODULE_SECTION_PATH = "FeatureFlags:Modules"; + private const string MODULE_SECTION_PATH = "FeatureManagement"; private const string MODULES_DIRECTORY = "Modules"; private const string LOGGER_NAME = "ModuleManager"; + private static List<(string FeatureName, bool IsEnabled)> GetFeatureList(IConfiguration configuration) + { + var featureSection = configuration.GetSection(MODULE_SECTION_PATH); + if (!featureSection.Exists()) + { + return new List<(string, bool)>(); + } + + return featureSection.GetChildren() + .Select(feature => ( + FeatureName: feature.Key, + IsEnabled: bool.TryParse(feature.Value, out var isEnabled) && isEnabled + )) + .ToList(); + } + [MethodImpl(MethodImplOptions.NoInlining)] public static void LoadModules(IMvcCoreBuilder mvcCoreBuilder, IConfiguration configuration, IWebHostEnvironment hostingEnvironment) { - var modulesSection = configuration.GetSection(MODULE_SECTION_PATH); - if (!modulesSection.Exists()) + var modules = GetFeatureList(configuration); + if (modules.Count == 0) { return; } - var logger = CreateLogger(mvcCoreBuilder.Services); - var modules = modulesSection.Get>(); - - foreach (var (moduleName, isEnabled) in modules) + foreach (var module in modules) { - if (!isEnabled) + if (!module.IsEnabled) { - logger.LogInformation("Module '{ModuleName}' is disabled.", moduleName); + logger.LogInformation("Module '{ModuleName}' is disabled.", module.FeatureName); continue; } - - LoadModule(moduleName, hostingEnvironment, mvcCoreBuilder, logger); + LoadModule(module.FeatureName, hostingEnvironment, mvcCoreBuilder, logger); } } diff --git a/src/Core/Grand.Infrastructure/StartupBase.cs b/src/Core/Grand.Infrastructure/StartupBase.cs index 6e04b72f6..70e0d350a 100644 --- a/src/Core/Grand.Infrastructure/StartupBase.cs +++ b/src/Core/Grand.Infrastructure/StartupBase.cs @@ -20,6 +20,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.FeatureManagement; namespace Grand.Infrastructure; @@ -253,7 +254,6 @@ private static void RegisterConfigurations(IServiceCollection services, IConfigu services.StartupConfig(configuration.GetSection("BackendAPI")); services.StartupConfig(configuration.GetSection("FrontendAPI")); services.StartupConfig(configuration.GetSection("Database")); - services.StartupConfig(configuration.GetSection("FeatureFlags")); services.StartupConfig(configuration.GetSection("Amazon")); services.StartupConfig(configuration.GetSection("Azure")); services.StartupConfig(configuration.GetSection("ApplicationInsights")); @@ -270,6 +270,8 @@ private static void RegisterConfigurations(IServiceCollection services, IConfigu /// Configuration root of the application public static void ConfigureServices(IServiceCollection services, IConfiguration configuration) { + services.AddFeatureManagement(); + //find startup configurations provided by other assemblies var typeSearcher = new TypeSearcher(); services.AddSingleton(typeSearcher); diff --git a/src/Modules/Grand.Module.Migration/Startup/StartupApplication.cs b/src/Modules/Grand.Module.Migration/Startup/StartupApplication.cs index 194a89e5b..37eb4218a 100644 --- a/src/Modules/Grand.Module.Migration/Startup/StartupApplication.cs +++ b/src/Modules/Grand.Module.Migration/Startup/StartupApplication.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.FeatureManagement; namespace Grand.Module.Migration.Startup; @@ -21,9 +22,8 @@ public void Configure(WebApplication application, IWebHostEnvironment webHostEnv { if (!DataSettingsManager.DatabaseIsInstalled()) return; - - var featureFlagsConfig = application.Services.GetRequiredService(); - if (featureFlagsConfig.Modules.TryGetValue("Grand.Module.Migration", out var value) && value) + var featureManager = application.Services.GetRequiredService(); + if (featureManager.IsEnabledAsync("Grand.Module.Migration").Result) { var migrationProcess = application.Services.GetRequiredService(); migrationProcess.RunMigrationProcess(); diff --git a/src/Web/Grand.Web.Common/Infrastructure/ApplicationBuilderExtensions.cs b/src/Web/Grand.Web.Common/Infrastructure/ApplicationBuilderExtensions.cs index afddf93f5..5cf7f616b 100644 --- a/src/Web/Grand.Web.Common/Infrastructure/ApplicationBuilderExtensions.cs +++ b/src/Web/Grand.Web.Common/Infrastructure/ApplicationBuilderExtensions.cs @@ -16,6 +16,7 @@ using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.FeatureManagement; using Microsoft.Net.Http.Headers; namespace Grand.Web.Common.Infrastructure; @@ -274,7 +275,7 @@ public static void UseDefaultSecurityHeaders(this WebApplication application) /// Builder for configuring an application's request pipeline public static void UseInstallUrl(this WebApplication application) { - application.UseMiddleware(); + application.UseMiddlewareForFeature("Grand.Module.Installer"); } /// diff --git a/src/Web/Grand.Web.Common/Middleware/InstallUrlMiddleware.cs b/src/Web/Grand.Web.Common/Middleware/InstallUrlMiddleware.cs index 64c7f88b8..16fc3d7db 100644 --- a/src/Web/Grand.Web.Common/Middleware/InstallUrlMiddleware.cs +++ b/src/Web/Grand.Web.Common/Middleware/InstallUrlMiddleware.cs @@ -1,8 +1,8 @@ using Grand.Data; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.FeatureManagement; namespace Grand.Web.Common.Middleware; @@ -35,8 +35,8 @@ public async Task InvokeAsync(HttpContext context) //whether database is installed if (!DataSettingsManager.DatabaseIsInstalled()) { - var configuration = context.RequestServices.GetService(); - var isInstallerModuleEnabled = configuration.GetValue("FeatureFlags:Modules:Grand.Module.Installer"); + var featureManager = context.RequestServices.GetRequiredService(); + var isInstallerModuleEnabled = await featureManager.IsEnabledAsync("Grand.Module.Installer"); if (!isInstallerModuleEnabled) { // Return a response indicating the installer module is not enabled diff --git a/src/Web/Grand.Web.Common/Startup/GrandCommonStartup.cs b/src/Web/Grand.Web.Common/Startup/GrandCommonStartup.cs index 63e74597d..a799c2714 100644 --- a/src/Web/Grand.Web.Common/Startup/GrandCommonStartup.cs +++ b/src/Web/Grand.Web.Common/Startup/GrandCommonStartup.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.FeatureManagement; namespace Grand.Web.Common.Startup; @@ -71,7 +72,7 @@ public void Configure(WebApplication application, IWebHostEnvironment webHostEnv var appConfig = application.Services.GetRequiredService(); var performanceConfig = application.Services.GetRequiredService(); var securityConfig = application.Services.GetRequiredService(); - var featureFlagsConfig = application.Services.GetRequiredService(); + var featureManager = application.Services.GetRequiredService(); //add HealthChecks application.UseGrandHealthChecks(); @@ -92,9 +93,8 @@ public void Configure(WebApplication application, IWebHostEnvironment webHostEnv //use static files feature application.UseGrandStaticFiles(appConfig); - //check whether database is installed - if (featureFlagsConfig.Modules.TryGetValue("Grand.Module.Installer", out var value) && value) - application.UseInstallUrl(); + //install middleware + application.UseInstallUrl(); //use HTTP session application.UseSession(); diff --git a/src/Web/Grand.Web/App_Data/Resources/Installation/installation.en.xml b/src/Web/Grand.Web/App_Data/Resources/Installation/installation.en.xml index dc1b2a074..a3602daf6 100644 --- a/src/Web/Grand.Web/App_Data/Resources/Installation/installation.en.xml +++ b/src/Web/Grand.Web/App_Data/Resources/Installation/installation.en.xml @@ -159,7 +159,7 @@ Use integrated Windows authentication - Application has been installed successfully, please restart application. + Application has been installed successfully. Please restart the application. We recommend disabling the feature flag in appsettings "Grand.Module.Installer" by setting it to false. This database already exists and has GrandNode installed on it. Choose different database. diff --git a/src/Web/Grand.Web/App_Data/appsettings.json b/src/Web/Grand.Web/App_Data/appsettings.json index 316459742..f130456a8 100644 --- a/src/Web/Grand.Web/App_Data/appsettings.json +++ b/src/Web/Grand.Web/App_Data/appsettings.json @@ -1,32 +1,26 @@ { "Application": { - //Enable if you want to see the full error in production environment. It's ignored (always enabled) in development environment "DisplayFullErrorStack": true, - //Value of "Cache-Control" header value for static content "StaticFilesCacheControl": "public,max-age=31536000", - //Enable the session-based TempData provider "UseSessionStateTempDataProvider": false, - //A value indicating whether SEO friendly URLs with multiple languages are enabled "SeoFriendlyUrlsForLanguagesEnabled": false, "SeoFriendlyUrlsDefaultCode": "en", - //Note: While use of custom response header encoding may be needed in some cases, //we discourage the use of non-ASCII encodings to avoid compatibility issues //with other HTTP clients. "AllowNonAsciiCharInHeaders": false, - //Gets or sets the maximum allowed size of any request body in bytes //the default value is 30MB - "MaxRequestBodySize": null, //max 2147483648 + "MaxRequestBodySize": null, + //max 2147483648 //Gets or sets the value to enable a middleware for logging additional information about CurrentCustomer and CurrentStore "EnableContextLoggingMiddleware": true }, - "Database": { //This setting is required to use LiteDB database "UseLiteDb": false, @@ -38,18 +32,13 @@ "ConnectionString": "", "DbProvider": 0 }, - "Security": { - //Use a reverse proxy server - more information you can find at: https://docs.microsoft.com/en-US/aspnet/core/host-and-deploy/linux-nginx "UseForwardedHeaders": false, - //In some cases, it might not be possible to add forwarded headers to the requests proxied to the app. "ForceUseHTTPS": false, - //AllowedHosts, is used for host filtering to bind your app to specific hostnames "AllowedHosts": "*", - //This settings adds the following headers to all responses that pass //X-Content-Type-Options: nosniff //Strict-Transport-Security: max-age=31536000; includeSubDomains @@ -58,41 +47,31 @@ //Referrer-Policy: strict-origin-when-cross-origin //Content-Security-Policy: object-src 'none'; form-action 'self'; frame-ancestors 'none' "UseDefaultSecurityHeaders": false, - //HTTP Strict Transport Security Protocol "UseHsts": false, - //When enabled, allow Razor files to be updated if they're edited. "EnableRuntimeCompilation": false, - //We recommend all ASP.NET Core web apps call HTTPS Redirection Middleware to redirect all HTTP requests to HTTPS "UseHttpsRedirection": false, "HttpsRedirectionRedirect": 308, "HttpsRedirectionHttpsPort": 443, - //Key persistence location you can point to a directory on the local machine, or it can point to a folder on a network share. //if is null it will use the default directory path - ApplicationPath\App_Data\DataProtectionKeys "KeyPersistenceLocation": "/App_Data/DataProtectionKeys", - //Gets or sets a value indicating for cookie auth expires in hours - default 24 * 365 = 8760 "CookieAuthExpires": 8760, - //Gets or sets a value for cookie prefix - any changes will log out all of the customers "CookiePrefix": ".Grand.", - //Gets or sets a value for cookie claim issuer - any changes will log out all of the customers "CookieClaimsIssuer": "grandnode", - //CookieSecurePolicy.Always always sets the Secure flag //Always setting the Secure flag is the most restrictive and most secure option. //This is the one you should be targeting if your production environment fully runs on HTTPS "CookieSecurePolicyAlways": false, - //Controls whether or not a cookie is sent with cross-site requests, providing some protection against cross-site request forgery attacks //Available values Unspecified, None, Lax, Strict "CookieSameSite": "Lax", "CookieSameSiteExternalAuth": "None", - //Enabling this setting allows for verification of access to a specific controller and action in the admin panel using menu configuration. "AuthorizeAdminMenu": false }, @@ -101,53 +80,40 @@ "DefaultCacheTimeMinutes": 60 }, "Extensions": { - //For developers - more info you can find at https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/ //https://github.com/dotnet/roslyn/wiki/Roslyn-Overview "UseRoslynScripts": false, - //Disable upload extensions plugins "DisableUploadExtensions": false, - //A list of plugins to be ignored during start application - pattern "PluginSkipLoadingPattern": "", - //Enable if you want to clear /Plugins/bin directory on application startup "ClearPluginShadowDirectoryOnStartup": true, - //For develop you should enable if you want to copy plugin dll files to directory /Plugins/bin on application startup "PluginShadowCopy": true, - //separator comma "InstalledPlugins": "" }, "AccessControl": { /// A value indicating whether to ignore ACL rules "IgnoreAcl": false, - //A value indicating whether to ignore "limit per store" rules "IgnoreStoreLimitations": false }, "Performance": { - //Indicates whether to compress response (gzip by default) //You may want to disable it, for example, If you have an active IIS Dynamic Compression Module configured at the server level "UseResponseCompression": false, - //Indicates whether to ignore DbVersionCheckMiddleware "IgnoreDbVersionCheckMiddleware": false, - //Indicates whether to ignore UsePoweredByMiddleware "IgnoreUsePoweredByMiddleware": false - }, - "FeatureFlags": { - "Modules": { - "Grand.Module.Installer": true, - "Grand.Module.Migration": true, - "Grand.Module.ScheduledTasks": true, - "Grand.Module.Api": false - } + "FeatureManagement": { + "Grand.Module.Installer": false, + "Grand.Module.Migration": true, + "Grand.Module.ScheduledTasks": true, + "Grand.Module.Api": false }, "Redis": { ///Enable the Publish/Subscribe messaging with redis to manage memory cache on every server @@ -157,7 +123,6 @@ "PersistKeysToRedis": false, "PersistKeysToRedisUrl": "127.0.0.1:6379,allowAdmin=true,defaultDatabase=1" }, - "Rabbit": { //Enable RabbitMq "RabbitEnabled": false, @@ -183,29 +148,31 @@ "AzureBlobStorageConnectionString": "", "AzureBlobStorageContainerName": "", "AzureBlobStorageEndPoint": "", - //DataProtection - Azure Key Vault - you can use only one of method PersistKeysToAzureKeyVault or PersistKeysToAzureBlobStorage - "PersistKeysAzureBlobStorageConnectionString": "", //required + "PersistKeysAzureBlobStorageConnectionString": "", + //required "PersistKeysToAzureKeyVault": false, "PersistKeysToAzureBlobStorage": false, - "DataProtectionContainerName": "", //required - "DataProtectionBlobName": "keys.xml", //required - "KeyIdentifier": "", //required when use PersistKeysToAzureKeyVault + "DataProtectionContainerName": "", + //required + "DataProtectionBlobName": "keys.xml", + //required + "KeyIdentifier": "", + //required when use PersistKeysToAzureKeyVault //Azure App Configuration "AppConfiguration": "", "AppKeyPrefix": "" }, "Amazon": { - //Amazon Blob storage // "AmazonAwsAccessKeyId": "", "AmazonAwsSecretAccessKey": "", "AmazonBucketName": "", "AmazonRegion": "", - "AmazonDistributionDomainName": "" //Domain name for cloudfront distribution - + "AmazonDistributionDomainName": "" + //Domain name for cloudfront distribution }, "FacebookSettings": { //Facebook-assigned App ID @@ -217,11 +184,11 @@ "ClientId": "", "ClientSecret": "" }, - //access to the api to web controllers "FrontendAPI": { "Enabled": true, - "JsonContentType": false, //when is enabled, use ContentType = application/json to read json from body, default is form-data + "JsonContentType": false, + //when is enabled, use ContentType = application/json to read json from body, default is form-data "SecretKey": "your private secret key to use api", "ValidateIssuer": false, "ValidIssuer": "", @@ -247,7 +214,6 @@ "SystemModel": true }, "UseSwagger": true, - "Logging": { "LogLevel": { "Default": "Information",