diff --git a/docs/en/Customizing-Application-Modules-Overriding-Services.md b/docs/en/Customizing-Application-Modules-Overriding-Services.md index ab6f29816b..081b142ad7 100644 --- a/docs/en/Customizing-Application-Modules-Overriding-Services.md +++ b/docs/en/Customizing-Application-Modules-Overriding-Services.md @@ -196,12 +196,16 @@ This example replaces the `AccountController` (An API Controller defined in the **`[ExposeServices(typeof(AccountController))]` is essential** here since it registers this controller for the `AccountController` in the dependency injection system. `[Dependency(ReplaceServices = true)]` is also recommended to clear the old registration (even the ASP.NET Core DI system selects the last registered one). -In addition, The `AccountController` will be removed from [`ApplicationModel`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.applicationmodel.controllers) because it defines `ExposeServicesAttribute`. If you don't want to remove it, you can configure `AbpAspNetCoreMvcOptions`: +In addition, the `MyAccountController` will be removed from [`ApplicationModel`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.applicationmodel.controllers) because it defines `ExposeServicesAttribute`. + +If `IncludeSelf = true` is specified, i.e. `[ExposeServices(typeof(AccountController), IncludeSelf = true)]`, then `AccountController` will be removed instead. This is useful for **extending** a controller. + +If you don't want to remove either controller, you can configure `AbpAspNetCoreMvcOptions`: ```csharp Configure(options => { - options.IgnoredControllersOnModelExclusion.AddIfNotContains(typeof(AccountController)); + options.IgnoredControllersOnModelExclusion.AddIfNotContains(typeof(MyAccountController)); }); ``` diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs index 4d1490ec40..996e495588 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs @@ -73,7 +73,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions protected virtual void RemoveDuplicateControllers(ApplicationModel application) { - var derivedControllerModels = new List(); + var controllerModelsToRemove = new List(); foreach (var controllerModel in application.Controllers) { @@ -87,19 +87,42 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions continue; } + var exposeServicesAttr = ReflectionHelper.GetSingleAttributeOrDefault(controllerModel.ControllerType); + if (exposeServicesAttr.IncludeSelf) + { + var exposedControllerModels = application.Controllers + .Where(cm => exposeServicesAttr.ServiceTypes.Contains(cm.ControllerType)) + .ToArray(); + + controllerModelsToRemove.AddRange(exposedControllerModels); + Logger.LogInformation($"Removing the controller{(exposedControllerModels.Length > 1 ? "s" : "")} {exposeServicesAttr.ServiceTypes.Select(c => c.AssemblyQualifiedName).JoinAsString(", ")} from the application model since {(exposedControllerModels.Length > 1 ? "they are" : "it is")} replaced by the controller: {controllerModel.ControllerType.AssemblyQualifiedName}"); + continue; + } + var baseControllerTypes = controllerModel.ControllerType .GetBaseClasses(typeof(Controller), includeObject: false) .Where(t => !t.IsAbstract) .ToArray(); - if (baseControllerTypes.Length > 0) + if (baseControllerTypes.Length == 0) { - derivedControllerModels.Add(controllerModel); - Logger.LogInformation($"Removing the controller {controllerModel.ControllerType.AssemblyQualifiedName} from the application model since it replaces the controller(s): {baseControllerTypes.Select(c => c.AssemblyQualifiedName).JoinAsString(", ")}"); + continue; } + + var baseControllerModels = application.Controllers + .Where(cm => baseControllerTypes.Contains(cm.ControllerType)) + .ToArray(); + + if (baseControllerModels.Length == 0) + { + continue; + } + + controllerModelsToRemove.Add(controllerModel); + Logger.LogInformation($"Removing the controller {controllerModel.ControllerType.AssemblyQualifiedName} from the application model since it replaces the controller(s): {baseControllerTypes.Select(c => c.AssemblyQualifiedName).JoinAsString(", ")}"); } - application.Controllers.RemoveAll(derivedControllerModels); + application.Controllers.RemoveAll(controllerModelsToRemove); } protected virtual void ConfigureRemoteService(ControllerModel controller, [CanBeNull] ConventionalControllerSetting configuration) diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention_Tests.cs new file mode 100644 index 0000000000..ae10a00654 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention_Tests.cs @@ -0,0 +1,143 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Shouldly; +using System; +using System.Reflection; +using Volo.Abp.DependencyInjection; +using Xunit; + +namespace Volo.Abp.AspNetCore.Mvc.Conventions +{ + public class AbpServiceConvention_Tests : AspNetCoreMvcTestBase + { + private readonly IConventionalRouteBuilder _conventionalRouteBuilder; + private readonly IOptions _options; + + public AbpServiceConvention_Tests() + { + _conventionalRouteBuilder = GetRequiredService(); + _options = GetRequiredService>(); + } + + [Fact] + public void Should_Not_Remove_Derived_Controller_If_Not_Expose_Service() + { + // Arrange + var applicationModel = new ApplicationModel(); + var baseControllerModel = new ControllerModel(typeof(BaseController).GetTypeInfo(), Array.Empty()) + { + Application = applicationModel + }; + applicationModel.Controllers.Add(baseControllerModel); + + var derivedControllerModel = new ControllerModel(typeof(DerivedController).GetTypeInfo(), Array.Empty()) + { + Application = applicationModel + }; + applicationModel.Controllers.Add(derivedControllerModel); + + var abpServiceConvention = new AbpServiceConvention(_options, _conventionalRouteBuilder); + + // Act + abpServiceConvention.Apply(applicationModel); + + // Assert + applicationModel.Controllers.ShouldContain(baseControllerModel); + applicationModel.Controllers.ShouldContain(derivedControllerModel); + } + + [Fact] + public void Should_Remove_Exposed_Controller_If_Expose_Self() + { + // Arrange + var applicationModel = new ApplicationModel(); + var baseControllerModel = new ControllerModel(typeof(BaseController).GetTypeInfo(), Array.Empty()) + { + Application = applicationModel + }; + applicationModel.Controllers.Add(baseControllerModel); + + var derivedControllerModel = new ControllerModel(typeof(ExposeServiceIncludeSelfDerivedController).GetTypeInfo(), Array.Empty()) + { + Application = applicationModel + }; + applicationModel.Controllers.Add(derivedControllerModel); + + var abpServiceConvention = new AbpServiceConvention(_options, _conventionalRouteBuilder); + + // Act + abpServiceConvention.Apply(applicationModel); + + // Assert + applicationModel.Controllers.ShouldNotContain(baseControllerModel); + applicationModel.Controllers.ShouldContain(derivedControllerModel); + } + + [Fact] + public void Should_Not_Remove_Derived_Controller_If_No_Base_Controller_Model() + { + // Arrange + var applicationModel = new ApplicationModel(); + var derivedControllerModel = new ControllerModel(typeof(ExposeServiceDerivedController).GetTypeInfo(), Array.Empty()) + { + Application = applicationModel + }; + applicationModel.Controllers.Add(derivedControllerModel); + + var abpServiceConvention = new AbpServiceConvention(_options, _conventionalRouteBuilder); + + // Act + abpServiceConvention.Apply(applicationModel); + + // Assert + applicationModel.Controllers.ShouldContain(derivedControllerModel); + } + + [Fact] + public void Should_Remove_Derived_Controller_If_Expose_Service() + { + // Arrange + var applicationModel = new ApplicationModel(); + var baseControllerModel = new ControllerModel(typeof(BaseController).GetTypeInfo(), Array.Empty()) + { + Application = applicationModel + }; + applicationModel.Controllers.Add(baseControllerModel); + + var derivedControllerModel = new ControllerModel(typeof(ExposeServiceDerivedController).GetTypeInfo(), Array.Empty()) + { + Application = applicationModel + }; + applicationModel.Controllers.Add(derivedControllerModel); + + var abpServiceConvention = new AbpServiceConvention(_options, _conventionalRouteBuilder); + + // Act + abpServiceConvention.Apply(applicationModel); + + // Assert + applicationModel.Controllers.ShouldContain(baseControllerModel); + applicationModel.Controllers.ShouldNotContain(derivedControllerModel); + } + } + + public class BaseController : Controller + { + } + + public class DerivedController : BaseController + { + } + + [ExposeServices(typeof(BaseController))] + public class ExposeServiceDerivedController : BaseController + { + } + + [ExposeServices(typeof(BaseController), IncludeSelf = true)] + public class ExposeServiceIncludeSelfDerivedController : BaseController + { + } +}