From 95bbd6d7853f2a7e8713a9c327ffff9c004dc440 Mon Sep 17 00:00:00 2001 From: Georg von Kries Date: Fri, 24 May 2024 18:27:07 +0200 Subject: [PATCH] Corrects the mapping of types to features in ITypeFeatureProvider (#15793) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --------- Co-authored-by: Zoltán Lehóczky Co-authored-by: Mike Alhayek Co-authored-by: Sébastien Ros --- .../Indexing/SQL/Migrations.cs | 2 - .../Indexing/SQL/UserPickerMigrations.cs | 2 - .../AdminMenu.cs | 2 - ...portContentToDeploymentTargetMigrations.cs | 2 - .../FacebookLoginConfiguration.cs | 2 - .../Widgets/Services/LiquidShapes.cs | 2 - .../Widgets/WidgetMigrations.cs | 2 - .../AdminMenuGitHubLogin.cs | 2 - .../GoogleAuthenticationAdminMenu.cs | 4 - .../SecureMediaPermissions.cs | 2 - .../AdminMenuMicrosoftAccount.cs | 3 - .../OpenIdClientConfiguration.cs | 2 - .../OpenIdServerConfiguration.cs | 2 - .../OpenIdValidationConfiguration.cs | 4 +- .../Controllers/AdminController.cs | 13 ++- .../OrchardCore.Roles/Services/RoleUpdater.cs | 8 +- .../LuceneContentPickerShapeProvider.cs | 2 - .../TenantFeatureProfileShapeTableProvider.cs | 2 - .../OrchardCore.Twitter/AdminMenuSignin.cs | 3 - .../OrchardCore.Users/AdminMenu.cs | 4 - .../ChangeEmailSettingsDisplayDriver.cs | 2 - .../RegistrationSettingsDisplayDriver.cs | 2 - .../ResetPasswordSettingsDisplayDriver.cs | 2 - .../Extensions/Features/FeatureEntry.cs | 22 ----- .../Features/ITypeFeatureProvider.cs | 31 +++++++ .../Extensions/IExtensionManager.cs | 6 +- .../Shell/Builders/Models/ShellBlueprint.cs | 2 +- .../Migration/DataMigrationManager.cs | 2 +- .../Shell/ShellDescriptorManager.cs | 2 +- .../ModularApplicationModelProvider.cs | 29 +++---- .../Services/RecipeMigrator.cs | 4 +- .../ElasticContentPickerShapeProvider.cs | 2 - .../Extensions/ExtensionManager.cs | 86 ++++++------------- .../Features/TypeFeatureProvider.cs | 39 +++++++-- .../Shell/Builders/CompositionStrategy.cs | 18 +++- .../Shell/Builders/ShellContainerFactory.cs | 82 ++++++++++++++---- ...onfiguredFeaturesShellDescriptorManager.cs | 2 +- .../SetFeaturesShellDescriptorManager.cs | 2 +- src/docs/releases/2.0.0.md | 4 + .../ShapeDescriptorIndexBenchmark.cs | 1 + .../DefaultShapeTableManagerTests.cs | 17 ++-- .../Extensions/ExtensionManagerTests.cs | 3 - .../Shell/ShellContainerFactoryTests.cs | 44 +++++++++- .../Stubs/StubExtensionManager.cs | 11 ++- 44 files changed, 279 insertions(+), 201 deletions(-) delete mode 100644 src/OrchardCore/OrchardCore.Abstractions/Extensions/Features/FeatureEntry.cs diff --git a/src/OrchardCore.Modules/OrchardCore.ContentFields/Indexing/SQL/Migrations.cs b/src/OrchardCore.Modules/OrchardCore.ContentFields/Indexing/SQL/Migrations.cs index 19ae0d9a242..66f13a1d57d 100644 --- a/src/OrchardCore.Modules/OrchardCore.ContentFields/Indexing/SQL/Migrations.cs +++ b/src/OrchardCore.Modules/OrchardCore.ContentFields/Indexing/SQL/Migrations.cs @@ -3,12 +3,10 @@ using Microsoft.Extensions.Logging; using OrchardCore.ContentManagement.Records; using OrchardCore.Data.Migration; -using OrchardCore.Modules; using YesSql.Sql; namespace OrchardCore.ContentFields.Indexing.SQL { - [Feature("OrchardCore.ContentFields.Indexing.SQL")] public class Migrations : DataMigration { private readonly ILogger _logger; diff --git a/src/OrchardCore.Modules/OrchardCore.ContentFields/Indexing/SQL/UserPickerMigrations.cs b/src/OrchardCore.Modules/OrchardCore.ContentFields/Indexing/SQL/UserPickerMigrations.cs index 06fd0dac97d..1bc6ec0e99a 100644 --- a/src/OrchardCore.Modules/OrchardCore.ContentFields/Indexing/SQL/UserPickerMigrations.cs +++ b/src/OrchardCore.Modules/OrchardCore.ContentFields/Indexing/SQL/UserPickerMigrations.cs @@ -1,12 +1,10 @@ using System.Threading.Tasks; using OrchardCore.ContentManagement.Records; using OrchardCore.Data.Migration; -using OrchardCore.Modules; using YesSql.Sql; namespace OrchardCore.ContentFields.Indexing.SQL { - [Feature("OrchardCore.ContentFields.Indexing.SQL.UserPicker")] public class UserPickerMigrations : DataMigration { public async Task CreateAsync() diff --git a/src/OrchardCore.Modules/OrchardCore.ContentLocalization/AdminMenu.cs b/src/OrchardCore.Modules/OrchardCore.ContentLocalization/AdminMenu.cs index 0b8726ff7db..3c89f9fecfa 100644 --- a/src/OrchardCore.Modules/OrchardCore.ContentLocalization/AdminMenu.cs +++ b/src/OrchardCore.Modules/OrchardCore.ContentLocalization/AdminMenu.cs @@ -2,12 +2,10 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Localization; using OrchardCore.ContentLocalization.Drivers; -using OrchardCore.Modules; using OrchardCore.Navigation; namespace OrchardCore.ContentLocalization { - [Feature("OrchardCore.ContentLocalization.ContentCulturePicker")] public class AdminMenu : INavigationProvider { private static readonly RouteValueDictionary _providersRouteValues = new() diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Deployment/ExportContentToDeploymentTarget/ExportContentToDeploymentTargetMigrations.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Deployment/ExportContentToDeploymentTarget/ExportContentToDeploymentTargetMigrations.cs index 997b545e2d9..d49520fe82c 100644 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Deployment/ExportContentToDeploymentTarget/ExportContentToDeploymentTargetMigrations.cs +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Deployment/ExportContentToDeploymentTarget/ExportContentToDeploymentTargetMigrations.cs @@ -3,14 +3,12 @@ using OrchardCore.Data.Migration; using OrchardCore.Deployment; using OrchardCore.Entities; -using OrchardCore.Modules; using OrchardCore.Recipes; using OrchardCore.Recipes.Services; using OrchardCore.Settings; namespace OrchardCore.Contents.Deployment.ExportContentToDeploymentTarget { - [Feature("OrchardCore.Contents.Deployment.ExportContentToDeploymentTarget")] public class ExportContentToDeploymentTargetMigrations : DataMigration { private readonly IRecipeMigrator _recipeMigrator; diff --git a/src/OrchardCore.Modules/OrchardCore.Facebook/Login/Configuration/FacebookLoginConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.Facebook/Login/Configuration/FacebookLoginConfiguration.cs index d400a12d7c6..b70d3048814 100644 --- a/src/OrchardCore.Modules/OrchardCore.Facebook/Login/Configuration/FacebookLoginConfiguration.cs +++ b/src/OrchardCore.Modules/OrchardCore.Facebook/Login/Configuration/FacebookLoginConfiguration.cs @@ -11,11 +11,9 @@ using OrchardCore.Facebook.Login.Services; using OrchardCore.Facebook.Login.Settings; using OrchardCore.Facebook.Settings; -using OrchardCore.Modules; namespace OrchardCore.Facebook.Login.Configuration { - [Feature(FacebookConstants.Features.Login)] public class FacebookLoginConfiguration : IConfigureOptions, IConfigureNamedOptions diff --git a/src/OrchardCore.Modules/OrchardCore.Facebook/Widgets/Services/LiquidShapes.cs b/src/OrchardCore.Modules/OrchardCore.Facebook/Widgets/Services/LiquidShapes.cs index acb857d1ed1..ff496ece983 100644 --- a/src/OrchardCore.Modules/OrchardCore.Facebook/Widgets/Services/LiquidShapes.cs +++ b/src/OrchardCore.Modules/OrchardCore.Facebook/Widgets/Services/LiquidShapes.cs @@ -7,11 +7,9 @@ using OrchardCore.DisplayManagement.Implementation; using OrchardCore.Facebook.Widgets.ViewModels; using OrchardCore.Liquid; -using OrchardCore.Modules; namespace OrchardCore.Facebook.Widgets.Services; -[Feature(FacebookConstants.Features.Widgets)] public class LiquidShapes(HtmlEncoder htmlEncoder) : ShapeTableProvider { private readonly HtmlEncoder _htmlEncoder = htmlEncoder; diff --git a/src/OrchardCore.Modules/OrchardCore.Facebook/Widgets/WidgetMigrations.cs b/src/OrchardCore.Modules/OrchardCore.Facebook/Widgets/WidgetMigrations.cs index f36c0845fac..5ffac237891 100644 --- a/src/OrchardCore.Modules/OrchardCore.Facebook/Widgets/WidgetMigrations.cs +++ b/src/OrchardCore.Modules/OrchardCore.Facebook/Widgets/WidgetMigrations.cs @@ -3,13 +3,11 @@ using OrchardCore.ContentManagement.Metadata.Settings; using OrchardCore.Data.Migration; using OrchardCore.Facebook.Widgets.Models; -using OrchardCore.Modules; using OrchardCore.Recipes; using OrchardCore.Recipes.Services; namespace OrchardCore.Facebook.Widgets { - [Feature(FacebookConstants.Features.Widgets)] public class WidgetMigrations : DataMigration { private readonly IRecipeMigrator _recipeMigrator; diff --git a/src/OrchardCore.Modules/OrchardCore.GitHub/AdminMenuGitHubLogin.cs b/src/OrchardCore.Modules/OrchardCore.GitHub/AdminMenuGitHubLogin.cs index 3bf0dbd3176..ec2420226e2 100644 --- a/src/OrchardCore.Modules/OrchardCore.GitHub/AdminMenuGitHubLogin.cs +++ b/src/OrchardCore.Modules/OrchardCore.GitHub/AdminMenuGitHubLogin.cs @@ -1,12 +1,10 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Localization; -using OrchardCore.Modules; using OrchardCore.Navigation; namespace OrchardCore.GitHub { - [Feature(GitHubConstants.Features.GitHubAuthentication)] public class AdminMenuGitHubLogin : INavigationProvider { private static readonly RouteValueDictionary _routeValues = new() diff --git a/src/OrchardCore.Modules/OrchardCore.Google/GoogleAuthenticationAdminMenu.cs b/src/OrchardCore.Modules/OrchardCore.Google/GoogleAuthenticationAdminMenu.cs index 425dfc883e9..6c91ce2dfe7 100644 --- a/src/OrchardCore.Modules/OrchardCore.Google/GoogleAuthenticationAdminMenu.cs +++ b/src/OrchardCore.Modules/OrchardCore.Google/GoogleAuthenticationAdminMenu.cs @@ -1,12 +1,10 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Localization; -using OrchardCore.Modules; using OrchardCore.Navigation; namespace OrchardCore.Google { - [Feature(GoogleConstants.Features.GoogleAuthentication)] public class GoogleAuthenticationAdminMenu : INavigationProvider { private static readonly RouteValueDictionary _routeValues = new() @@ -46,7 +44,6 @@ public Task BuildNavigationAsync(string name, NavigationBuilder builder) } } - [Feature(GoogleConstants.Features.GoogleAnalytics)] public class GoogleAnalyticsAdminMenu : INavigationProvider { private static readonly RouteValueDictionary _routeValues = new() @@ -85,7 +82,6 @@ public Task BuildNavigationAsync(string name, NavigationBuilder builder) } } - [Feature(GoogleConstants.Features.GoogleTagManager)] public class GoogleTagManagerAdminMenu : INavigationProvider { private static readonly RouteValueDictionary _routeValues = new() diff --git a/src/OrchardCore.Modules/OrchardCore.Media/SecureMediaPermissions.cs b/src/OrchardCore.Modules/OrchardCore.Media/SecureMediaPermissions.cs index 6e958017d46..6c14167c504 100644 --- a/src/OrchardCore.Modules/OrchardCore.Media/SecureMediaPermissions.cs +++ b/src/OrchardCore.Modules/OrchardCore.Media/SecureMediaPermissions.cs @@ -7,12 +7,10 @@ using Microsoft.Extensions.Options; using OrchardCore.Environment.Cache; using OrchardCore.Media.Services; -using OrchardCore.Modules; using OrchardCore.Security.Permissions; namespace OrchardCore.Media { - [Feature("OrchardCore.Media.Security")] public class SecureMediaPermissions : IPermissionProvider { // Note: The ManageMediaFolder permission grants all access, so viewing must be implied by it too. diff --git a/src/OrchardCore.Modules/OrchardCore.Microsoft.Authentication/AdminMenuMicrosoftAccount.cs b/src/OrchardCore.Modules/OrchardCore.Microsoft.Authentication/AdminMenuMicrosoftAccount.cs index 3fcd818deb8..4f6d3ab1450 100644 --- a/src/OrchardCore.Modules/OrchardCore.Microsoft.Authentication/AdminMenuMicrosoftAccount.cs +++ b/src/OrchardCore.Modules/OrchardCore.Microsoft.Authentication/AdminMenuMicrosoftAccount.cs @@ -1,12 +1,10 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Localization; -using OrchardCore.Modules; using OrchardCore.Navigation; namespace OrchardCore.Microsoft.Authentication { - [Feature(MicrosoftAuthenticationConstants.Features.MicrosoftAccount)] public class AdminMenuMicrosoftAccount : INavigationProvider { private static readonly RouteValueDictionary _routeValues = new() @@ -46,7 +44,6 @@ public Task BuildNavigationAsync(string name, NavigationBuilder builder) } } - [Feature(MicrosoftAuthenticationConstants.Features.AAD)] public class AdminMenuAAD : INavigationProvider { private static readonly RouteValueDictionary _routeValues = new() diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/Configuration/OpenIdClientConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.OpenId/Configuration/OpenIdClientConfiguration.cs index a70f67751eb..bc00032bf3b 100644 --- a/src/OrchardCore.Modules/OrchardCore.OpenId/Configuration/OpenIdClientConfiguration.cs +++ b/src/OrchardCore.Modules/OrchardCore.OpenId/Configuration/OpenIdClientConfiguration.cs @@ -9,13 +9,11 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using OrchardCore.Environment.Shell; -using OrchardCore.Modules; using OrchardCore.OpenId.Services; using OrchardCore.OpenId.Settings; namespace OrchardCore.OpenId.Configuration { - [Feature(OpenIdConstants.Features.Client)] public class OpenIdClientConfiguration : IConfigureOptions, IConfigureNamedOptions diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/Configuration/OpenIdServerConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.OpenId/Configuration/OpenIdServerConfiguration.cs index cd31fcafa01..c2d8ca2d955 100644 --- a/src/OrchardCore.Modules/OrchardCore.OpenId/Configuration/OpenIdServerConfiguration.cs +++ b/src/OrchardCore.Modules/OrchardCore.OpenId/Configuration/OpenIdServerConfiguration.cs @@ -11,14 +11,12 @@ using OpenIddict.Server.AspNetCore; using OpenIddict.Server.DataProtection; using OrchardCore.Environment.Shell; -using OrchardCore.Modules; using OrchardCore.OpenId.Services; using OrchardCore.OpenId.Settings; using static OpenIddict.Abstractions.OpenIddictConstants; namespace OrchardCore.OpenId.Configuration { - [Feature(OpenIdConstants.Features.Server)] public class OpenIdServerConfiguration : IConfigureOptions, IConfigureOptions, IConfigureOptions, diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/Configuration/OpenIdValidationConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.OpenId/Configuration/OpenIdValidationConfiguration.cs index 20ebb006db4..73757417f56 100644 --- a/src/OrchardCore.Modules/OrchardCore.OpenId/Configuration/OpenIdValidationConfiguration.cs +++ b/src/OrchardCore.Modules/OrchardCore.OpenId/Configuration/OpenIdValidationConfiguration.cs @@ -16,16 +16,14 @@ using OpenIddict.Validation.DataProtection; using OrchardCore.Environment.Shell; using OrchardCore.Environment.Shell.Scope; -using OrchardCore.Modules; using OrchardCore.OpenId.Services; using OrchardCore.OpenId.Settings; using OrchardCore.Security; -using SystemEnvironment = System.Environment; using static OpenIddict.Abstractions.OpenIddictConstants; +using SystemEnvironment = System.Environment; namespace OrchardCore.OpenId.Configuration { - [Feature(OpenIdConstants.Features.Validation)] public class OpenIdValidationConfiguration : IConfigureOptions, IConfigureOptions, IConfigureOptions, diff --git a/src/OrchardCore.Modules/OrchardCore.Roles/Controllers/AdminController.cs b/src/OrchardCore.Modules/OrchardCore.Roles/Controllers/AdminController.cs index 41b1bbb1a87..1fff5e39075 100644 --- a/src/OrchardCore.Modules/OrchardCore.Roles/Controllers/AdminController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Roles/Controllers/AdminController.cs @@ -13,6 +13,7 @@ using OrchardCore.DisplayManagement.Notify; using OrchardCore.Environment.Extensions; using OrchardCore.Environment.Extensions.Features; +using OrchardCore.Environment.Shell; using OrchardCore.Roles.ViewModels; using OrchardCore.Security; using OrchardCore.Security.Permissions; @@ -28,8 +29,10 @@ public class AdminController : Controller private readonly IAuthorizationService _authorizationService; private readonly IEnumerable _permissionProviders; private readonly ITypeFeatureProvider _typeFeatureProvider; + private readonly IShellFeaturesManager _shellFeaturesManager; private readonly IRoleService _roleService; private readonly INotifier _notifier; + protected readonly IStringLocalizer S; protected readonly IHtmlLocalizer H; @@ -39,6 +42,7 @@ public AdminController( IAuthorizationService authorizationService, IEnumerable permissionProviders, ITypeFeatureProvider typeFeatureProvider, + IShellFeaturesManager shellFeaturesManager, IRoleService roleService, INotifier notifier, IStringLocalizer stringLocalizer, @@ -49,6 +53,7 @@ public AdminController( _authorizationService = authorizationService; _permissionProviders = permissionProviders; _typeFeatureProvider = typeFeatureProvider; + _shellFeaturesManager = shellFeaturesManager; _roleService = roleService; _notifier = notifier; S = stringLocalizer; @@ -241,9 +246,15 @@ private RoleEntry BuildRoleEntry(IRole role) private async Task>> GetInstalledPermissionsAsync() { var installedPermissions = new Dictionary>(); + var enabledFeatures = await _shellFeaturesManager.GetEnabledFeaturesAsync(); + foreach (var permissionProvider in _permissionProviders) { - var feature = _typeFeatureProvider.GetFeatureForDependency(permissionProvider.GetType()); + // Two features could use the same permission. + var feature = _typeFeatureProvider + .GetFeaturesForDependency(permissionProvider.GetType()) + .LastOrDefault(feature => enabledFeatures.Any(enabledFeature => feature.Id == enabledFeature.Id)); + var permissions = await permissionProvider.GetPermissionsAsync(); foreach (var permission in permissions) diff --git a/src/OrchardCore.Modules/OrchardCore.Roles/Services/RoleUpdater.cs b/src/OrchardCore.Modules/OrchardCore.Roles/Services/RoleUpdater.cs index 3651293739c..c4440bbfb87 100644 --- a/src/OrchardCore.Modules/OrchardCore.Roles/Services/RoleUpdater.cs +++ b/src/OrchardCore.Modules/OrchardCore.Roles/Services/RoleUpdater.cs @@ -54,7 +54,7 @@ private async Task UpdateRolesForInstalledFeatureAsync(IFeatureInfo feature) _installedFeatures.Add(feature.Id); var providers = _permissionProviders - .Where(provider => _typeFeatureProvider.GetFeatureForDependency(provider.GetType()).Id == feature.Id); + .Where(provider => _typeFeatureProvider.GetFeaturesForDependency(provider.GetType()).Any(p => p.Id == feature.Id)); if (!providers.Any()) { @@ -98,7 +98,7 @@ private async Task UpdateRolesForEnabledFeatureAsync(IFeatureInfo feature) } var providers = _permissionProviders - .Where(provider => _typeFeatureProvider.GetFeatureForDependency(provider.GetType()).Id == feature.Id); + .Where(provider => _typeFeatureProvider.GetFeaturesForDependency(provider.GetType()).Any(p => p.Id == feature.Id)); if (!providers.Any()) { @@ -144,8 +144,8 @@ private async Task UpdateRoleForInstalledFeaturesAsync(string roleName) // And defining at least one 'IPermissionProvider'. rolesDocument.MissingFeaturesByRole[roleName] = (await _extensionManager.LoadFeaturesAsync(missingFeatures)) - .Where(entry => entry.ExportedTypes.Any(type => type.IsAssignableTo(typeof(IPermissionProvider)))) - .Select(entry => entry.FeatureInfo.Id) + .Where(entry => _typeFeatureProvider.GetTypesForFeature(entry).Any(type => type.IsAssignableTo(typeof(IPermissionProvider)))) + .Select(entry => entry.Id) .ToList(); await _documentManager.UpdateAsync(rolesDocument); diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Shapes/LuceneContentPickerShapeProvider.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Shapes/LuceneContentPickerShapeProvider.cs index ebc8472f4bf..7c2335d0278 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Shapes/LuceneContentPickerShapeProvider.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Shapes/LuceneContentPickerShapeProvider.cs @@ -3,11 +3,9 @@ using OrchardCore.DisplayManagement; using OrchardCore.DisplayManagement.Descriptors; using OrchardCore.DisplayManagement.Shapes; -using OrchardCore.Modules; namespace OrchardCore.Search.Lucene { - [Feature("OrchardCore.Search.Lucene.ContentPicker")] public class LuceneContentPickerShapeProvider : IShapeAttributeProvider { protected readonly IStringLocalizer S; diff --git a/src/OrchardCore.Modules/OrchardCore.Tenants/Services/TenantFeatureProfileShapeTableProvider.cs b/src/OrchardCore.Modules/OrchardCore.Tenants/Services/TenantFeatureProfileShapeTableProvider.cs index 26a8bec69ee..3287a2ef7af 100644 --- a/src/OrchardCore.Modules/OrchardCore.Tenants/Services/TenantFeatureProfileShapeTableProvider.cs +++ b/src/OrchardCore.Modules/OrchardCore.Tenants/Services/TenantFeatureProfileShapeTableProvider.cs @@ -2,12 +2,10 @@ using OrchardCore.DisplayManagement; using OrchardCore.DisplayManagement.Descriptors; using OrchardCore.DisplayManagement.Views; -using OrchardCore.Modules; using OrchardCore.Tenants.ViewModels; namespace OrchardCore.Tenants.Services; -[Feature("OrchardCore.Tenants.FeatureProfiles")] public class TenantFeatureProfileShapeTableProvider : ShapeTableProvider { public override ValueTask DiscoverAsync(ShapeTableBuilder builder) diff --git a/src/OrchardCore.Modules/OrchardCore.Twitter/AdminMenuSignin.cs b/src/OrchardCore.Modules/OrchardCore.Twitter/AdminMenuSignin.cs index 16106ab6107..c94861b43d2 100644 --- a/src/OrchardCore.Modules/OrchardCore.Twitter/AdminMenuSignin.cs +++ b/src/OrchardCore.Modules/OrchardCore.Twitter/AdminMenuSignin.cs @@ -1,12 +1,10 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Localization; -using OrchardCore.Modules; using OrchardCore.Navigation; namespace OrchardCore.Twitter { - [Feature(TwitterConstants.Features.Signin)] public class AdminMenuSignin : INavigationProvider { private static readonly RouteValueDictionary _routeValues = new() @@ -45,7 +43,6 @@ public Task BuildNavigationAsync(string name, NavigationBuilder builder) } } - [Feature(TwitterConstants.Features.Twitter)] public class AdminMenu : INavigationProvider { private static readonly RouteValueDictionary _routeValues = new() diff --git a/src/OrchardCore.Modules/OrchardCore.Users/AdminMenu.cs b/src/OrchardCore.Modules/OrchardCore.Users/AdminMenu.cs index 03d6e4a9ce5..a129fcee209 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/AdminMenu.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/AdminMenu.cs @@ -1,7 +1,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Localization; -using OrchardCore.Modules; using OrchardCore.Navigation; using OrchardCore.Users.Drivers; using OrchardCore.Users.Models; @@ -55,7 +54,6 @@ public Task BuildNavigationAsync(string name, NavigationBuilder builder) } } - [Feature("OrchardCore.Users.ChangeEmail")] public class ChangeEmailAdminMenu : INavigationProvider { private static readonly RouteValueDictionary _routeValues = new() @@ -93,7 +91,6 @@ public Task BuildNavigationAsync(string name, NavigationBuilder builder) } } - [Feature(UserConstants.Features.UserRegistration)] public class RegistrationAdminMenu : INavigationProvider { private static readonly RouteValueDictionary _routeValues = new() @@ -131,7 +128,6 @@ public Task BuildNavigationAsync(string name, NavigationBuilder builder) } } - [Feature(UserConstants.Features.ResetPassword)] public class ResetPasswordAdminMenu : INavigationProvider { private static readonly RouteValueDictionary _routeValues = new() diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ChangeEmailSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ChangeEmailSettingsDisplayDriver.cs index 4f44c0e58be..5bba6fbc7bf 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ChangeEmailSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ChangeEmailSettingsDisplayDriver.cs @@ -5,13 +5,11 @@ using OrchardCore.DisplayManagement.Entities; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; -using OrchardCore.Modules; using OrchardCore.Settings; using OrchardCore.Users.Models; namespace OrchardCore.Users.Drivers { - [Feature("OrchardCore.Users.ChangeEmail")] public class ChangeEmailSettingsDisplayDriver : SectionDisplayDriver { public const string GroupId = "userChangeEmail"; diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegistrationSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegistrationSettingsDisplayDriver.cs index 64c71abfd92..2bc38975813 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegistrationSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegistrationSettingsDisplayDriver.cs @@ -5,13 +5,11 @@ using OrchardCore.DisplayManagement.Entities; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; -using OrchardCore.Modules; using OrchardCore.Settings; using OrchardCore.Users.Models; namespace OrchardCore.Users.Drivers { - [Feature(UserConstants.Features.UserRegistration)] public class RegistrationSettingsDisplayDriver : SectionDisplayDriver { public const string GroupId = "userRegistration"; diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ResetPasswordSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ResetPasswordSettingsDisplayDriver.cs index 59e7e19e664..7e21dbbc9a8 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ResetPasswordSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ResetPasswordSettingsDisplayDriver.cs @@ -5,13 +5,11 @@ using OrchardCore.DisplayManagement.Entities; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; -using OrchardCore.Modules; using OrchardCore.Settings; using OrchardCore.Users.Models; namespace OrchardCore.Users.Drivers { - [Feature(UserConstants.Features.ResetPassword)] public class ResetPasswordSettingsDisplayDriver : SectionDisplayDriver { public const string GroupId = "userResetPassword"; diff --git a/src/OrchardCore/OrchardCore.Abstractions/Extensions/Features/FeatureEntry.cs b/src/OrchardCore/OrchardCore.Abstractions/Extensions/Features/FeatureEntry.cs deleted file mode 100644 index 342961aa9ef..00000000000 --- a/src/OrchardCore/OrchardCore.Abstractions/Extensions/Features/FeatureEntry.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace OrchardCore.Environment.Extensions.Features -{ - public class FeatureEntry - { - public FeatureEntry(IFeatureInfo featureInfo) - { - FeatureInfo = featureInfo; - } - - public FeatureEntry(IFeatureInfo featureInfo, IEnumerable exportedTypes) - { - FeatureInfo = featureInfo; - ExportedTypes = exportedTypes; - } - - public IFeatureInfo FeatureInfo { get; } - public IEnumerable ExportedTypes { get; } = []; - } -} diff --git a/src/OrchardCore/OrchardCore.Abstractions/Extensions/Features/ITypeFeatureProvider.cs b/src/OrchardCore/OrchardCore.Abstractions/Extensions/Features/ITypeFeatureProvider.cs index ce9aa247803..850de1357f1 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Extensions/Features/ITypeFeatureProvider.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Extensions/Features/ITypeFeatureProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using OrchardCore.Environment.Extensions.Features; namespace OrchardCore.Environment.Extensions @@ -9,7 +10,37 @@ namespace OrchardCore.Environment.Extensions /// public interface ITypeFeatureProvider { + /// + /// Gets the extension for the specified dependent type. + /// + IExtensionInfo GetExtensionForDependency(Type dependency); + + /// + /// Gets the first feature for the specified dependent type. + /// + /// + /// If a type has been registered for more than one feature, + /// returns the first feature that has + /// the same ID as the parent extension. + /// Use this method when you only need one feature of a module, such + /// as when applying migrations for the entire module as opposed to + /// functionality of individual features. + /// IFeatureInfo GetFeatureForDependency(Type dependency); + + /// + /// Gets all features for the specified dependent type. + /// + IEnumerable GetFeaturesForDependency(Type dependency); + + /// + /// Gets all dependent types for the specified feature. + /// + IEnumerable GetTypesForFeature(IFeatureInfo feature); + + /// + /// Adds a type to the specified feature. + /// void TryAdd(Type type, IFeatureInfo feature); } } diff --git a/src/OrchardCore/OrchardCore.Abstractions/Extensions/IExtensionManager.cs b/src/OrchardCore/OrchardCore.Abstractions/Extensions/IExtensionManager.cs index 985225f384c..11854790320 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Extensions/IExtensionManager.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Extensions/IExtensionManager.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Threading.Tasks; using OrchardCore.Environment.Extensions.Features; @@ -8,13 +9,14 @@ public interface IExtensionManager { IExtensionInfo GetExtension(string extensionId); IEnumerable GetExtensions(); + IEnumerable GetExportedExtensionTypes(IExtensionInfo extensionInfo); Task LoadExtensionAsync(IExtensionInfo extensionInfo); IEnumerable GetFeatures(); IEnumerable GetFeatures(string[] featureIdsToLoad); IEnumerable GetFeatureDependencies(string featureId); IEnumerable GetDependentFeatures(string featureId); - Task> LoadFeaturesAsync(); - Task> LoadFeaturesAsync(string[] featureIdsToLoad); + Task> LoadFeaturesAsync(); + Task> LoadFeaturesAsync(string[] featureIdsToLoad); } } diff --git a/src/OrchardCore/OrchardCore.Abstractions/Shell/Builders/Models/ShellBlueprint.cs b/src/OrchardCore/OrchardCore.Abstractions/Shell/Builders/Models/ShellBlueprint.cs index 8ade2c8f70d..35d19b59b09 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Shell/Builders/Models/ShellBlueprint.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Shell/Builders/Models/ShellBlueprint.cs @@ -15,6 +15,6 @@ public class ShellBlueprint public ShellSettings Settings { get; set; } public ShellDescriptor Descriptor { get; set; } - public IDictionary Dependencies { get; set; } + public IDictionary> Dependencies { get; set; } } } diff --git a/src/OrchardCore/OrchardCore.Data.YesSql/Migration/DataMigrationManager.cs b/src/OrchardCore/OrchardCore.Data.YesSql/Migration/DataMigrationManager.cs index 70c8b92bcb8..a9ce8703cfc 100644 --- a/src/OrchardCore/OrchardCore.Data.YesSql/Migration/DataMigrationManager.cs +++ b/src/OrchardCore/OrchardCore.Data.YesSql/Migration/DataMigrationManager.cs @@ -276,7 +276,7 @@ private static async Task InvokeMethodAsync(MethodInfo method, IDataMigrati private IDataMigration[] GetDataMigrations(string featureId) { var migrations = _dataMigrations - .Where(dm => _typeFeatureProvider.GetFeatureForDependency(dm.GetType()).Id == featureId) + .Where(dm => _typeFeatureProvider.GetFeaturesForDependency(dm.GetType()).Any(feature => feature.Id == featureId)) .ToArray(); return migrations; diff --git a/src/OrchardCore/OrchardCore.Infrastructure/Shell/ShellDescriptorManager.cs b/src/OrchardCore/OrchardCore.Infrastructure/Shell/ShellDescriptorManager.cs index 9c7b6ba54c9..d9fd9c07cd0 100644 --- a/src/OrchardCore/OrchardCore.Infrastructure/Shell/ShellDescriptorManager.cs +++ b/src/OrchardCore/OrchardCore.Infrastructure/Shell/ShellDescriptorManager.cs @@ -83,7 +83,7 @@ public async Task GetShellDescriptorAsync() var featureIds = features.Select(sf => sf.Id).ToArray(); var missingDependencies = (await _extensionManager.LoadFeaturesAsync(featureIds)) - .Select(entry => entry.FeatureInfo.Id) + .Select(entry => entry.Id) .Except(featureIds) .Select(id => new ShellFeature(id)); diff --git a/src/OrchardCore/OrchardCore.Mvc.Core/ModularApplicationModelProvider.cs b/src/OrchardCore/OrchardCore.Mvc.Core/ModularApplicationModelProvider.cs index 4a5946af9b2..a9a52c04be6 100644 --- a/src/OrchardCore/OrchardCore.Mvc.Core/ModularApplicationModelProvider.cs +++ b/src/OrchardCore/OrchardCore.Mvc.Core/ModularApplicationModelProvider.cs @@ -1,3 +1,4 @@ +using System.Linq; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.Extensions.Hosting; using OrchardCore.Environment.Extensions; @@ -42,25 +43,23 @@ public void OnProvidersExecuted(ApplicationModelProviderContext context) foreach (var controller in context.Result.Controllers) { var controllerType = controller.ControllerType.AsType(); - var blueprint = _typeFeatureProvider.GetFeatureForDependency(controllerType); - if (blueprint != null) - { - if (blueprint.Extension.Id == _hostingEnvironment.ApplicationName && !_shellSettings.IsRunning()) - { - // Don't serve any action of the application'module which is enabled during a setup. - foreach (var action in controller.Actions) - { - action.Selectors.Clear(); - } + var extension = _typeFeatureProvider.GetExtensionForDependency(controllerType); - controller.Selectors.Clear(); - } - else + if (extension.Id == _hostingEnvironment.ApplicationName && !_shellSettings.IsRunning()) + { + // Don't serve any action of the application's module which is enabled during a setup. + foreach (var action in controller.Actions) { - // Add an "area" route value equal to the module id. - controller.RouteValues.Add("area", blueprint.Extension.Id); + action.Selectors.Clear(); } + + controller.Selectors.Clear(); + } + else + { + // Add an "area" route value equal to the module id. + controller.RouteValues.Add("area", extension.Id); } } } diff --git a/src/OrchardCore/OrchardCore.Recipes.Core/Services/RecipeMigrator.cs b/src/OrchardCore/OrchardCore.Recipes.Core/Services/RecipeMigrator.cs index ce453ef4086..2ae164a9897 100644 --- a/src/OrchardCore/OrchardCore.Recipes.Core/Services/RecipeMigrator.cs +++ b/src/OrchardCore/OrchardCore.Recipes.Core/Services/RecipeMigrator.cs @@ -39,9 +39,9 @@ public RecipeMigrator( public async Task ExecuteAsync(string recipeFileName, IDataMigration migration) { - var featureInfo = _typeFeatureProvider.GetFeatureForDependency(migration.GetType()); + var extensionInfo = _typeFeatureProvider.GetExtensionForDependency(migration.GetType()); - var recipeBasePath = Path.Combine(featureInfo.Extension.SubPath, "Migrations").Replace('\\', '/'); + var recipeBasePath = Path.Combine(extensionInfo.SubPath, "Migrations").Replace('\\', '/'); var recipeFilePath = Path.Combine(recipeBasePath, recipeFileName).Replace('\\', '/'); var recipeFileInfo = _hostingEnvironment.ContentRootFileProvider.GetFileInfo(recipeFilePath); var recipeDescriptor = await _recipeReader.GetRecipeDescriptorAsync(recipeBasePath, recipeFileInfo, _hostingEnvironment.ContentRootFileProvider); diff --git a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Shapes/ElasticContentPickerShapeProvider.cs b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Shapes/ElasticContentPickerShapeProvider.cs index 4f3f229fcb3..6b95e7a1aef 100644 --- a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Shapes/ElasticContentPickerShapeProvider.cs +++ b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Shapes/ElasticContentPickerShapeProvider.cs @@ -3,11 +3,9 @@ using OrchardCore.DisplayManagement; using OrchardCore.DisplayManagement.Descriptors; using OrchardCore.DisplayManagement.Shapes; -using OrchardCore.Modules; namespace OrchardCore.Search.Elasticsearch { - [Feature("OrchardCore.Search.Elasticsearch.ContentPicker")] public class ElasticContentPickerShapeProvider : IShapeAttributeProvider { protected readonly IStringLocalizer S; diff --git a/src/OrchardCore/OrchardCore/Extensions/ExtensionManager.cs b/src/OrchardCore/OrchardCore/Extensions/ExtensionManager.cs index 394a3dcad9d..d942df73b7c 100644 --- a/src/OrchardCore/OrchardCore/Extensions/ExtensionManager.cs +++ b/src/OrchardCore/OrchardCore/Extensions/ExtensionManager.cs @@ -3,7 +3,7 @@ using System.Collections.Frozen; using System.Collections.Generic; using System.Linq; -using System.Reflection; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using OrchardCore.Environment.Extensions.Features; @@ -24,12 +24,11 @@ public class ExtensionManager : IExtensionManager private readonly IExtensionDependencyStrategy[] _extensionDependencyStrategies; private readonly IExtensionPriorityStrategy[] _extensionPriorityStrategies; - private readonly ITypeFeatureProvider _typeFeatureProvider; private readonly IFeaturesProvider _featuresProvider; private FrozenDictionary _extensions; private List _extensionsInfos; - private Dictionary _features; + private Dictionary _features; private IFeatureInfo[] _featureInfos; private readonly ConcurrentDictionary>> _featureDependencies = new(); @@ -42,14 +41,12 @@ public ExtensionManager( IApplicationContext applicationContext, IEnumerable extensionDependencyStrategies, IEnumerable extensionPriorityStrategies, - ITypeFeatureProvider typeFeatureProvider, IFeaturesProvider featuresProvider, ILogger logger) { _applicationContext = applicationContext; _extensionDependencyStrategies = extensionDependencyStrategies as IExtensionDependencyStrategy[] ?? extensionDependencyStrategies.ToArray(); _extensionPriorityStrategies = extensionPriorityStrategies as IExtensionPriorityStrategy[] ?? extensionPriorityStrategies.ToArray(); - _typeFeatureProvider = typeFeatureProvider; _featuresProvider = featuresProvider; L = logger; } @@ -92,6 +89,18 @@ public IEnumerable GetFeatures(string[] featureIdsToLoad) } } + public IEnumerable GetExportedExtensionTypes(IExtensionInfo extensionInfo) + { + EnsureInitialized(); + + if (_extensions.TryGetValue(extensionInfo.Id, out var extension)) + { + return extension.ExportedTypes; + } + + return []; + } + public Task LoadExtensionAsync(IExtensionInfo extensionInfo) { EnsureInitialized(); @@ -101,22 +110,23 @@ public Task LoadExtensionAsync(IExtensionInfo extensionInfo) return Task.FromResult(extension); } - public Task> LoadFeaturesAsync() + public Task> LoadFeaturesAsync() { EnsureInitialized(); - return Task.FromResult>(_features.Values); + + return Task.FromResult>(_features.Values); } - public Task> LoadFeaturesAsync(string[] featureIdsToLoad) + public Task> LoadFeaturesAsync(string[] featureIdsToLoad) { EnsureInitialized(); var features = new HashSet(GetFeatures(featureIdsToLoad).Select(f => f.Id)); var loadedFeatures = _features.Values - .Where(f => features.Contains(f.FeatureInfo.Id)); + .Where(f => features.Contains(f.Id)); - return Task.FromResult>(loadedFeatures); + return Task.FromResult>(loadedFeatures); } public IEnumerable GetFeatureDependencies(string featureId) @@ -130,9 +140,7 @@ public IEnumerable GetFeatureDependencies(string featureId) return []; } - var feature = entry.FeatureInfo; - - return GetFeatureDependencies(feature, _featureInfos); + return GetFeatureDependencies(entry, _featureInfos); })).Value; } @@ -147,9 +155,7 @@ public IEnumerable GetDependentFeatures(string featureId) return []; } - var feature = entry.FeatureInfo; - - return GetDependentFeatures(feature, _featureInfos); + return GetDependentFeatures(entry, _featureInfos); })).Value; } @@ -260,13 +266,6 @@ private static List GetFeatureDependenciesFunc(IFeatureInfo curren return list; } - private static string GetSourceFeatureNameForType(Type type, string extensionId) - { - var attribute = type.GetCustomAttributes(false).FirstOrDefault(); - - return attribute?.FeatureName ?? extensionId; - } - private void EnsureInitialized() { if (_isInitialized) @@ -308,24 +307,7 @@ private void EnsureInitialized() loadedExtensions.TryAdd(module.Name, entry); }); - var loadedFeatures = new Dictionary(); - - // Get all valid types from any extension - var allTypesByExtension = loadedExtensions.SelectMany(extension => - extension.Value.ExportedTypes.Where(IsComponentType) - .Select(type => new - { - ExtensionEntry = extension.Value, - Type = type - })).ToArray(); - - var typesByFeature = allTypesByExtension - .GroupBy(typeByExtension => GetSourceFeatureNameForType( - typeByExtension.Type, - typeByExtension.ExtensionEntry.ExtensionInfo.Id)) - .ToDictionary( - group => group.Key, - group => group.Select(typesByExtension => typesByExtension.Type).ToArray()); + var loadedFeatures = new Dictionary(); foreach (var loadedExtension in loadedExtensions) { @@ -333,25 +315,12 @@ private void EnsureInitialized() foreach (var feature in extension.ExtensionInfo.Features) { - // Features can have no types - if (typesByFeature.TryGetValue(feature.Id, out var featureTypes)) - { - foreach (var type in featureTypes) - { - _typeFeatureProvider.TryAdd(type, feature); - } - } - else - { - featureTypes = []; - } - - loadedFeatures.Add(feature.Id, new FeatureEntry(feature, featureTypes)); + loadedFeatures.Add(feature.Id, feature); } } // Feature infos and entries are ordered by priority and dependencies. - _featureInfos = Order(loadedFeatures.Values.Select(f => f.FeatureInfo)); + _featureInfos = Order(loadedFeatures.Values); _features = _featureInfos.ToDictionary(f => f.Id, f => loadedFeatures[f.Id]); // Extensions are also ordered according to the weight of their first features. @@ -366,11 +335,6 @@ private void EnsureInitialized() } } - private static bool IsComponentType(Type type) - { - return type.IsClass && !type.IsAbstract && type.IsPublic; - } - private IFeatureInfo[] Order(IEnumerable featuresToOrder) { return featuresToOrder diff --git a/src/OrchardCore/OrchardCore/Extensions/Features/TypeFeatureProvider.cs b/src/OrchardCore/OrchardCore/Extensions/Features/TypeFeatureProvider.cs index 23caf77694c..4acf71f17a1 100644 --- a/src/OrchardCore/OrchardCore/Extensions/Features/TypeFeatureProvider.cs +++ b/src/OrchardCore/OrchardCore/Extensions/Features/TypeFeatureProvider.cs @@ -1,26 +1,55 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; using OrchardCore.Environment.Extensions.Features; namespace OrchardCore.Environment.Extensions { public class TypeFeatureProvider : ITypeFeatureProvider { - private readonly ConcurrentDictionary _features = new(); + private readonly ConcurrentDictionary> _features = new(); + + public IExtensionInfo GetExtensionForDependency(Type dependency) + { + if (_features.TryGetValue(dependency, out var features)) + { + return features.First().Extension; + } + + throw new InvalidOperationException($"Could not resolve extension for type {dependency.Name}."); + } public IFeatureInfo GetFeatureForDependency(Type dependency) { - if (_features.TryGetValue(dependency, out var feature)) + if (_features.TryGetValue(dependency, out var features)) { - return feature; + // Gets the first feature that has the same ID as the extension, if any. + // Otherwise returns the first feature. + return features.FirstOrDefault(feature => feature.Extension.Id == feature.Id) ?? features.First(); } - throw new InvalidOperationException($"Could not resolve feature for type {dependency.Name}"); + throw new InvalidOperationException($"Could not resolve main feature for type {dependency.Name}."); + } + + public IEnumerable GetFeaturesForDependency(Type dependency) + { + if (_features.TryGetValue(dependency, out var features)) + { + return features; + } + + throw new InvalidOperationException($"Could not resolve features for type {dependency.Name}."); + } + + public IEnumerable GetTypesForFeature(IFeatureInfo feature) + { + return _features.Where(kv => kv.Value.Contains(feature)).Select(kv => kv.Key); } public void TryAdd(Type type, IFeatureInfo feature) { - _features.TryAdd(type, feature); + _features.AddOrUpdate(type, (key, value) => [value], (key, features, value) => features.Contains(value) ? features : features.Append(value).ToArray(), feature); } } } diff --git a/src/OrchardCore/OrchardCore/Shell/Builders/CompositionStrategy.cs b/src/OrchardCore/OrchardCore/Shell/Builders/CompositionStrategy.cs index a75445aea2c..aedbfa8eb44 100644 --- a/src/OrchardCore/OrchardCore/Shell/Builders/CompositionStrategy.cs +++ b/src/OrchardCore/OrchardCore/Shell/Builders/CompositionStrategy.cs @@ -14,13 +14,16 @@ namespace OrchardCore.Environment.Shell.Builders public class CompositionStrategy : ICompositionStrategy { private readonly IExtensionManager _extensionManager; + private readonly ITypeFeatureProvider _typeFeatureProvider; private readonly ILogger _logger; public CompositionStrategy( IExtensionManager extensionManager, + ITypeFeatureProvider typeFeatureProvider, ILogger logger) { _extensionManager = extensionManager; + _typeFeatureProvider = typeFeatureProvider; _logger = logger; } @@ -35,17 +38,26 @@ public async Task ComposeAsync(ShellSettings settings, ShellDesc var features = await _extensionManager.LoadFeaturesAsync(featureNames); - var entries = new Dictionary(); + var entries = new Dictionary>(); foreach (var feature in features) { - foreach (var exportedType in feature.ExportedTypes) + foreach (var exportedType in _typeFeatureProvider.GetTypesForFeature(feature)) { var requiredFeatures = RequireFeaturesAttribute.GetRequiredFeatureNamesForType(exportedType); if (requiredFeatures.All(x => featureNames.Contains(x))) { - entries.Add(exportedType, feature); + if (entries.TryGetValue(exportedType, out var featureDependencies)) + { + featureDependencies = featureDependencies.Append(feature).ToArray(); + } + else + { + featureDependencies = [feature]; + } + + entries[exportedType] = featureDependencies; } } } diff --git a/src/OrchardCore/OrchardCore/Shell/Builders/ShellContainerFactory.cs b/src/OrchardCore/OrchardCore/Shell/Builders/ShellContainerFactory.cs index 4ae3c1281de..e5be3cca4e9 100644 --- a/src/OrchardCore/OrchardCore/Shell/Builders/ShellContainerFactory.cs +++ b/src/OrchardCore/OrchardCore/Shell/Builders/ShellContainerFactory.cs @@ -67,8 +67,8 @@ public async Task CreateContainerAsync(ShellSettings settings, continue; } - // Ignore Startup class from main application - if (blueprint.Dependencies.TryGetValue(rawStartup, out var startupFeature) && startupFeature.FeatureInfo.Id == _applicationFeature.Id) + // Ignore Startup class from main application. + if (blueprint.Dependencies.TryGetValue(rawStartup, out var startupFeatures) && startupFeatures.Any(f => f.Id == _applicationFeature.Id)) { continue; } @@ -115,10 +115,10 @@ public async Task CreateContainerAsync(ShellSettings settings, // OrderBy performs a stable sort so order is preserved among equal Order values. startups = startups.OrderBy(s => s.Order); - // Let any module add custom service descriptors to the tenant + // Let any module add custom service descriptors to the tenant. foreach (var startup in startups) { - var feature = blueprint.Dependencies.FirstOrDefault(x => x.Key == startup.GetType()).Value?.FeatureInfo; + var feature = blueprint.Dependencies.FirstOrDefault(x => x.Key == startup.GetType()).Value?.FirstOrDefault(); // If the startup is not coming from an extension, associate it to the application feature. // For instance when Startup classes are registered with Configure() from the application. @@ -132,9 +132,63 @@ public async Task CreateContainerAsync(ShellSettings settings, // Rebuild the service provider from the updated collection. shellServiceProvider = tenantServiceCollection.BuildServiceProvider(true); - // Register all DIed types in ITypeFeatureProvider var typeFeatureProvider = shellServiceProvider.GetRequiredService(); + PopulateTypeFeatureProvider(typeFeatureProvider, featureAwareServiceCollection); + return shellServiceProvider; + } + + private void EnsureApplicationFeature() + { + if (_applicationFeature is null) + { + lock (this) + { + _applicationFeature ??= _extensionManager.GetFeatures() + .FirstOrDefault(f => f.Id == _hostingEnvironment.ApplicationName); + } + } + } + + private void PopulateTypeFeatureProvider(ITypeFeatureProvider typeFeatureProvider, FeatureAwareServiceCollection featureAwareServiceCollection) + { + // Get all types from all extension and add them to the type feature provider. + var extensions = _extensionManager.GetExtensions(); + + var allTypesByExtension = extensions + .SelectMany(extension => + _extensionManager.GetExportedExtensionTypes(extension) + .Where(IsComponentType) + .Select(type => new + { + Extension = extension, + Type = type + })); + + var typesByFeature = allTypesByExtension + .GroupBy(typeByExtension => GetSourceFeatureNameForType( + typeByExtension.Type, + typeByExtension.Extension.Id)) + .ToDictionary( + group => group.Key, + group => group.Select(typesByExtension => typesByExtension.Type)); + + foreach (var extension in extensions) + { + foreach (var feature in extension.Features) + { + // Features can have no types. + if (typesByFeature.TryGetValue(feature.Id, out var featureTypes)) + { + foreach (var type in featureTypes) + { + typeFeatureProvider.TryAdd(type, feature); + } + } + } + } + + // Register all DIed types in ITypeFeatureProvider. foreach (var featureServiceCollection in featureAwareServiceCollection.FeatureCollections) { foreach (var serviceDescriptor in featureServiceCollection.Value) @@ -161,20 +215,18 @@ public async Task CreateContainerAsync(ShellSettings settings, } } } + } - return shellServiceProvider; + private static string GetSourceFeatureNameForType(Type type, string extensionId) + { + var attribute = type.GetCustomAttributes(false).FirstOrDefault(); + + return attribute?.FeatureName ?? extensionId; } - private void EnsureApplicationFeature() + private static bool IsComponentType(Type type) { - if (_applicationFeature is null) - { - lock (this) - { - _applicationFeature ??= _extensionManager.GetFeatures() - .FirstOrDefault(f => f.Id == _hostingEnvironment.ApplicationName); - } - } + return type.IsClass && !type.IsAbstract && type.IsPublic; } } } diff --git a/src/OrchardCore/OrchardCore/Shell/Descriptor/Settings/ConfiguredFeaturesShellDescriptorManager.cs b/src/OrchardCore/OrchardCore/Shell/Descriptor/Settings/ConfiguredFeaturesShellDescriptorManager.cs index 53f2882fcca..d2d88eac1b8 100644 --- a/src/OrchardCore/OrchardCore/Shell/Descriptor/Settings/ConfiguredFeaturesShellDescriptorManager.cs +++ b/src/OrchardCore/OrchardCore/Shell/Descriptor/Settings/ConfiguredFeaturesShellDescriptorManager.cs @@ -43,7 +43,7 @@ public async Task GetShellDescriptorAsync() var featureIds = features.Select(sf => sf.Id).ToArray(); var missingDependencies = (await _extensionManager.LoadFeaturesAsync(featureIds)) - .Select(entry => entry.FeatureInfo.Id) + .Select(entry => entry.Id) .Except(featureIds) .Select(id => new ShellFeature(id)); diff --git a/src/OrchardCore/OrchardCore/Shell/Descriptor/Settings/SetFeaturesShellDescriptorManager.cs b/src/OrchardCore/OrchardCore/Shell/Descriptor/Settings/SetFeaturesShellDescriptorManager.cs index bbbc349b007..010289ecd46 100644 --- a/src/OrchardCore/OrchardCore/Shell/Descriptor/Settings/SetFeaturesShellDescriptorManager.cs +++ b/src/OrchardCore/OrchardCore/Shell/Descriptor/Settings/SetFeaturesShellDescriptorManager.cs @@ -30,7 +30,7 @@ public async Task GetShellDescriptorAsync() var featureIds = _shellFeatures.Distinct().Select(sf => sf.Id).ToArray(); var missingDependencies = (await _extensionManager.LoadFeaturesAsync(featureIds)) - .Select(entry => entry.FeatureInfo.Id) + .Select(entry => entry.Id) .Except(featureIds) .Select(id => new ShellFeature(id)); diff --git a/src/docs/releases/2.0.0.md b/src/docs/releases/2.0.0.md index 5a1f1bfd675..cc00e15fd5c 100644 --- a/src/docs/releases/2.0.0.md +++ b/src/docs/releases/2.0.0.md @@ -495,3 +495,7 @@ public class ReverseProxySettingsDisplayDriver : SectionDisplayDriver foreach (var bindingStrategy in bindingStrategies) { var strategyFeature = typeFeatureProvider.GetFeatureForDependency(bindingStrategy.GetType()); + var builder = new ShapeTableBuilder(strategyFeature, []); await bindingStrategy.DiscoverAsync(builder); BuildDescriptors(bindingStrategy, builder.BuildAlterations()); diff --git a/test/OrchardCore.Tests/DisplayManagement/Decriptors/DefaultShapeTableManagerTests.cs b/test/OrchardCore.Tests/DisplayManagement/Decriptors/DefaultShapeTableManagerTests.cs index 86181a83a04..40e0806515f 100644 --- a/test/OrchardCore.Tests/DisplayManagement/Decriptors/DefaultShapeTableManagerTests.cs +++ b/test/OrchardCore.Tests/DisplayManagement/Decriptors/DefaultShapeTableManagerTests.cs @@ -241,14 +241,14 @@ public Task> LoadExtensionsAsync(IEnumerable LoadFeatureAsync(IFeatureInfo feature) + public Task LoadFeatureAsync(IFeatureInfo feature) { - return Task.FromResult(new FeatureEntry(feature)); + return Task.FromResult(feature); } - public Task> LoadFeaturesAsync(IEnumerable features) + public Task> LoadFeaturesAsync(IEnumerable features) { - return Task.FromResult(features.Select(x => new FeatureEntry(x))); + return Task.FromResult(features); } #pragma warning restore CA1822 // Mark members as static @@ -267,12 +267,17 @@ public IEnumerable GetDependentFeatures(string featureId) throw new NotImplementedException(); } - public Task> LoadFeaturesAsync() + public Task> LoadFeaturesAsync() { throw new NotImplementedException(); } - public Task> LoadFeaturesAsync(string[] featureIdsToLoad) + public Task> LoadFeaturesAsync(string[] featureIdsToLoad) + { + throw new NotImplementedException(); + } + + public IEnumerable GetExportedExtensionTypes(IExtensionInfo extensionInfo) { throw new NotImplementedException(); } diff --git a/test/OrchardCore.Tests/Extensions/ExtensionManagerTests.cs b/test/OrchardCore.Tests/Extensions/ExtensionManagerTests.cs index 1ae8e2bfd45..47b5711f518 100644 --- a/test/OrchardCore.Tests/Extensions/ExtensionManagerTests.cs +++ b/test/OrchardCore.Tests/Extensions/ExtensionManagerTests.cs @@ -31,7 +31,6 @@ public ExtensionManagerTests() _applicationContext, new[] { new ExtensionDependencyStrategy() }, new[] { new ExtensionPriorityStrategy() }, - new TypeFeatureProvider(), _moduleFeatureProvider, new NullLogger() ); @@ -40,7 +39,6 @@ public ExtensionManagerTests() _applicationContext, new[] { new ExtensionDependencyStrategy() }, new[] { new ExtensionPriorityStrategy() }, - new TypeFeatureProvider(), _themeFeatureProvider, new NullLogger() ); @@ -49,7 +47,6 @@ public ExtensionManagerTests() _applicationContext, new IExtensionDependencyStrategy[] { new ExtensionDependencyStrategy(), new ThemeExtensionDependencyStrategy() }, new[] { new ExtensionPriorityStrategy() }, - new TypeFeatureProvider(), _themeFeatureProvider, new NullLogger() ); diff --git a/test/OrchardCore.Tests/Shell/ShellContainerFactoryTests.cs b/test/OrchardCore.Tests/Shell/ShellContainerFactoryTests.cs index 3b8c6f93484..40a9b60d4ca 100644 --- a/test/OrchardCore.Tests/Shell/ShellContainerFactoryTests.cs +++ b/test/OrchardCore.Tests/Shell/ShellContainerFactoryTests.cs @@ -148,24 +148,52 @@ public async Task WhenHostSingletonAndScoped_GetServices_Returns_CorrectImplemen Assert.IsType(services.ElementAt(1)); } + [Fact] + public async Task AssignsTypeToMultipleFeatures() + { + var shellBlueprint = CreateBlueprint(); + + var expectedFeatureInfos = AddStartups(shellBlueprint, typeof(RegisterServiceStartup), typeof(RegisterSecondServiceStartup)); + + var container = (await _shellContainerFactory + .CreateContainerAsync(_uninitializedDefaultShell, shellBlueprint)) + .CreateScope() + .ServiceProvider; + + var typeFeatureProvider = _applicationServiceProvider.GetService(); + + Assert.IsType(container.GetRequiredService(typeof(ITestService))); + Assert.Equal(expectedFeatureInfos, typeFeatureProvider.GetFeaturesForDependency(typeof(TestService))); + } + private static ShellBlueprint CreateBlueprint() { return new ShellBlueprint { Settings = new ShellSettings(), Descriptor = new ShellDescriptor(), - Dependencies = new Dictionary() + Dependencies = new Dictionary>() }; } public static IFeatureInfo AddStartup(ShellBlueprint shellBlueprint, Type startupType) { - var featureInfo = new FeatureInfo(startupType.Name, startupType.Name, 1, "Tests", null, null, null, false, false, false); - shellBlueprint.Dependencies.Add(startupType, new FeatureEntry(featureInfo)); + var featureInfo = new FeatureInfo(startupType.Name, startupType.Name, 1, "Tests", null, new ExtensionInfo(startupType.Name), null, false, false, false); + shellBlueprint.Dependencies.Add(startupType, [featureInfo]); return featureInfo; } + public static IFeatureInfo[] AddStartups(ShellBlueprint shellBlueprint, Type startupType1, Type startupType2) + { + var featureInfo1 = new FeatureInfo(startupType1.Name, startupType1.Name, 1, "Tests", null, new ExtensionInfo(startupType1.Name), null, false, false, false); + var featureInfo2 = new FeatureInfo(startupType2.Name, startupType2.Name, 1, "Tests", null, new ExtensionInfo(startupType2.Name), null, false, false, false); + shellBlueprint.Dependencies.Add(startupType1, [featureInfo1]); + shellBlueprint.Dependencies.Add(startupType2, [featureInfo2]); + + return [featureInfo1, featureInfo2]; + } + private interface ITestService { } @@ -188,6 +216,16 @@ public override void ConfigureServices(IServiceCollection services) } } + private sealed class RegisterSecondServiceStartup : StartupBase + { + public override int Order => 1; + + public override void ConfigureServices(IServiceCollection services) + { + services.AddScoped(); + } + } + private sealed class ReplaceServiceStartup : StartupBase { public override int Order => 2; diff --git a/test/OrchardCore.Tests/Stubs/StubExtensionManager.cs b/test/OrchardCore.Tests/Stubs/StubExtensionManager.cs index 7ddaff5ff6a..2b81df56ef3 100644 --- a/test/OrchardCore.Tests/Stubs/StubExtensionManager.cs +++ b/test/OrchardCore.Tests/Stubs/StubExtensionManager.cs @@ -10,6 +10,11 @@ public IEnumerable GetDependentFeatures(string featureId) throw new NotImplementedException(); } + public IEnumerable GetExportedExtensionTypes(IExtensionInfo extensionInfo) + { + throw new NotImplementedException(); + } + public IExtensionInfo GetExtension(string extensionId) { throw new NotImplementedException(); @@ -17,7 +22,7 @@ public IExtensionInfo GetExtension(string extensionId) public IEnumerable GetExtensions() { - throw new NotImplementedException(); + return []; } public IEnumerable GetFeatureDependencies(string featureId) @@ -40,12 +45,12 @@ public Task LoadExtensionAsync(IExtensionInfo extensionInfo) throw new NotImplementedException(); } - public Task> LoadFeaturesAsync() + public Task> LoadFeaturesAsync() { throw new NotImplementedException(); } - public Task> LoadFeaturesAsync(string[] featureIdsToLoad) + public Task> LoadFeaturesAsync(string[] featureIdsToLoad) { throw new NotImplementedException(); }