diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs index 59166d3e1d..a2f1b1b523 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs @@ -4,6 +4,7 @@ using Volo.Abp.AspNetCore.Mvc.Auditing; using Volo.Abp.AspNetCore.Mvc.Conventions; using Volo.Abp.AspNetCore.Mvc.ExceptionHandling; using Volo.Abp.AspNetCore.Mvc.Features; +using Volo.Abp.AspNetCore.Mvc.GlobalFeatures; using Volo.Abp.AspNetCore.Mvc.ModelBinding; using Volo.Abp.AspNetCore.Mvc.Response; using Volo.Abp.AspNetCore.Mvc.Uow; @@ -29,6 +30,7 @@ namespace Volo.Abp.AspNetCore.Mvc private static void AddActionFilters(MvcOptions options) { + options.Filters.AddService(typeof(GlobalFeatureActionFilter)); options.Filters.AddService(typeof(AbpAuditActionFilter)); options.Filters.AddService(typeof(AbpNoContentActionFilter)); options.Filters.AddService(typeof(AbpFeatureActionFilter)); @@ -39,6 +41,7 @@ namespace Volo.Abp.AspNetCore.Mvc private static void AddPageFilters(MvcOptions options) { + options.Filters.AddService(typeof(GlobalFeaturePageFilter)); options.Filters.AddService(typeof(AbpExceptionPageFilter)); options.Filters.AddService(typeof(AbpAuditPageFilter)); options.Filters.AddService(typeof(AbpFeaturePageFilter)); @@ -58,4 +61,4 @@ namespace Volo.Abp.AspNetCore.Mvc ); } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/GlobalFeatureActionFilter.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/GlobalFeatureActionFilter.cs new file mode 100644 index 0000000000..e3fa489115 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/GlobalFeatureActionFilter.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Volo.Abp.DependencyInjection; +using Volo.Abp.GlobalFeatures; +using Volo.Abp.Reflection; + +namespace Volo.Abp.AspNetCore.Mvc.GlobalFeatures +{ + public class GlobalFeatureActionFilter : IAsyncActionFilter, ITransientDependency + { + public ILogger Logger { get; set; } + + public GlobalFeatureActionFilter() + { + Logger = NullLogger.Instance; + } + + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + if (!context.ActionDescriptor.IsControllerAction()) + { + await next(); + return; + } + + if (!IsGlobalFeatureEnabled(context.Controller.GetType(), out var attribute)) + { + Logger.LogWarning($"The '{context.Controller.GetType().FullName}' controller needs to enable '{attribute.Name}' feature."); + context.Result = new NotFoundResult(); + return; + } + + await next(); + } + + protected virtual bool IsGlobalFeatureEnabled(Type controllerType, out RequiresGlobalFeatureAttribute attribute) + { + attribute = ReflectionHelper.GetSingleAttributeOrDefault(controllerType); + return attribute == null || GlobalFeatureManager.Instance.IsEnabled(attribute.GetFeatureName()); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/GlobalFeaturePageFilter.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/GlobalFeaturePageFilter.cs new file mode 100644 index 0000000000..d29c44b4af --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/GlobalFeaturePageFilter.cs @@ -0,0 +1,52 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Volo.Abp.DependencyInjection; +using Volo.Abp.GlobalFeatures; +using Volo.Abp.Reflection; + +namespace Volo.Abp.AspNetCore.Mvc.GlobalFeatures +{ + public class GlobalFeaturePageFilter: IAsyncPageFilter, ITransientDependency + { + public ILogger Logger { get; set; } + + public GlobalFeaturePageFilter() + { + Logger = NullLogger.Instance; + } + + public Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context) + { + return Task.CompletedTask; + } + + public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next) + { + if (context.HandlerInstance == null || !context.ActionDescriptor.IsPageAction()) + { + await next(); + return; + } + + if (!IsGlobalFeatureEnabled(context.HandlerInstance.GetType(), out var attribute)) + { + Logger.LogWarning($"The '{context.HandlerInstance.GetType().FullName}' page needs to enable '{attribute.Name}' feature."); + context.Result = new NotFoundResult(); + return; + } + + await next(); + } + + protected virtual bool IsGlobalFeatureEnabled(Type controllerType, out RequiresGlobalFeatureAttribute attribute) + { + attribute = ReflectionHelper.GetSingleAttributeOrDefault(controllerType); + return attribute == null || GlobalFeatureManager.Instance.IsEnabled(attribute.GetFeatureName()); + } + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo.Abp.AspNetCore.Mvc.Tests.csproj b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo.Abp.AspNetCore.Mvc.Tests.csproj index b3842f7271..c6450e6df1 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo.Abp.AspNetCore.Mvc.Tests.csproj +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo.Abp.AspNetCore.Mvc.Tests.csproj @@ -61,6 +61,14 @@ true PreserveNewest + + true + PreserveNewest + + + true + PreserveNewest + diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs index d7544ebf0d..c666aeaa44 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs @@ -1,19 +1,23 @@ using System; +using System.Collections.Generic; using System.Security.Claims; using Localization.Resources.AbpUi; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AspNetCore.Mvc.Authorization; +using Volo.Abp.AspNetCore.Mvc.GlobalFeatures; using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.Localization.Resource; using Volo.Abp.AspNetCore.Security.Claims; using Volo.Abp.AspNetCore.TestBase; using Volo.Abp.Autofac; +using Volo.Abp.GlobalFeatures; using Volo.Abp.Localization; using Volo.Abp.MemoryDb; using Volo.Abp.Modularity; using Volo.Abp.TestApp; +using Volo.Abp.Threading; using Volo.Abp.Validation.Localization; using Volo.Abp.VirtualFileSystem; @@ -27,6 +31,8 @@ namespace Volo.Abp.AspNetCore.Mvc )] public class AbpAspNetCoreMvcTestModule : AbpModule { + private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner(); + public override void PreConfigureServices(ServiceConfigurationContext context) { context.Services.PreConfigure(options => @@ -40,6 +46,13 @@ namespace Volo.Abp.AspNetCore.Mvc public override void ConfigureServices(ServiceConfigurationContext context) { + OneTimeRunner.Run(() => + { + GlobalFeatureManager.Instance.Modules.GetOrAdd(AbpAspNetCoreMvcTestFeatures.ModuleName, + () => new AbpAspNetCoreMvcTestFeatures(GlobalFeatureManager.Instance)) + .EnableAll(); + }); + context.Services.AddAuthorization(options => { options.AddPolicy("MyClaimTestPolicy", policy => diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/AbpAspNetCoreMvcTestFeatures.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/AbpAspNetCoreMvcTestFeatures.cs new file mode 100644 index 0000000000..e95380e461 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/AbpAspNetCoreMvcTestFeatures.cs @@ -0,0 +1,30 @@ +using JetBrains.Annotations; +using Volo.Abp.GlobalFeatures; + +namespace Volo.Abp.AspNetCore.Mvc.GlobalFeatures +{ + [GlobalFeatureName(Name)] + public class AbpAspNetCoreMvcTestFeature1 : Abp.GlobalFeatures.GlobalFeature + { + public const string Name = "AbpAspNetCoreMvcTest.Feature1"; + + internal AbpAspNetCoreMvcTestFeature1([NotNull] AbpAspNetCoreMvcTestFeatures abpAspNetCoreMvcTestFeatures) + : base(abpAspNetCoreMvcTestFeatures) + { + + } + } + + public class AbpAspNetCoreMvcTestFeatures : GlobalModuleFeatures + { + public const string ModuleName = "AbpAspNetCoreMvcTest"; + + public AbpAspNetCoreMvcTestFeature1 Reactions => GetFeature(); + + public AbpAspNetCoreMvcTestFeatures([NotNull] GlobalFeatureManager featureManager) + : base(featureManager) + { + AddFeature(new AbpAspNetCoreMvcTestFeature1(this)); + } + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/DisabledGlobalFeatureTestController.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/DisabledGlobalFeatureTestController.cs new file mode 100644 index 0000000000..e8197f4374 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/DisabledGlobalFeatureTestController.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.GlobalFeatures; + +namespace Volo.Abp.AspNetCore.Mvc.GlobalFeatures +{ + [RequiresGlobalFeature("Not-Enabled-Feature")] + [Route("api/DisabledGlobalFeatureTestController-Test")] + public class DisabledGlobalFeatureTestController : AbpController + { + [HttpGet] + [Route("TestMethod")] + public string TestMethod() + { + return "TestMethod"; + } + } + +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/DisabledGlobalFeatureTestPage.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/DisabledGlobalFeatureTestPage.cshtml new file mode 100644 index 0000000000..17e1e154f5 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/DisabledGlobalFeatureTestPage.cshtml @@ -0,0 +1,19 @@ +@page +@model Volo.Abp.AspNetCore.Mvc.GlobalFeatures.DisabledGlobalFeatureTestPage + +@{ + Layout = null; +} + + + + + + + + +
+ +
+ + diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/DisabledGlobalFeatureTestPage.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/DisabledGlobalFeatureTestPage.cshtml.cs new file mode 100644 index 0000000000..13ef0dddb5 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/DisabledGlobalFeatureTestPage.cshtml.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; +using Volo.Abp.GlobalFeatures; + +namespace Volo.Abp.AspNetCore.Mvc.GlobalFeatures +{ + [RequiresGlobalFeature("Not-Enabled-Feature")] + public class DisabledGlobalFeatureTestPage : PageModel + { + public void OnGet() + { + + } + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/EnabledGlobalFeatureTestController.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/EnabledGlobalFeatureTestController.cs new file mode 100644 index 0000000000..9cc1d4700a --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/EnabledGlobalFeatureTestController.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.GlobalFeatures; + +namespace Volo.Abp.AspNetCore.Mvc.GlobalFeatures +{ + [RequiresGlobalFeature(AbpAspNetCoreMvcTestFeature1.Name)] + [Route("api/EnabledGlobalFeatureTestController-Test")] + public class EnabledGlobalFeatureTestController : AbpController + { + [HttpGet] + [Route("TestMethod")] + public string TestMethod() + { + return "TestMethod"; + } + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/EnabledGlobalFeatureTestPage.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/EnabledGlobalFeatureTestPage.cshtml new file mode 100644 index 0000000000..56e8934446 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/EnabledGlobalFeatureTestPage.cshtml @@ -0,0 +1,19 @@ +@page +@model Volo.Abp.AspNetCore.Mvc.GlobalFeatures.EnabledGlobalFeatureTestPage + +@{ + Layout = null; +} + + + + + + + + +
+ +
+ + diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/EnabledGlobalFeatureTestPage.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/EnabledGlobalFeatureTestPage.cshtml.cs new file mode 100644 index 0000000000..bc98d06344 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/EnabledGlobalFeatureTestPage.cshtml.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; +using Volo.Abp.GlobalFeatures; + +namespace Volo.Abp.AspNetCore.Mvc.GlobalFeatures +{ + [RequiresGlobalFeature(AbpAspNetCoreMvcTestFeature1.Name)] + public class EnabledGlobalFeatureTestPage : PageModel + { + public void OnGet() + { + + } + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/RequiresGlobalFeatureTestController_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/RequiresGlobalFeatureTestController_Tests.cs new file mode 100644 index 0000000000..c01d9bd508 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/RequiresGlobalFeatureTestController_Tests.cs @@ -0,0 +1,24 @@ +using System.Net; +using System.Threading.Tasks; +using Shouldly; +using Xunit; + +namespace Volo.Abp.AspNetCore.Mvc.GlobalFeatures +{ + public class RequiresGlobalFeatureTestController_Tests: AspNetCoreMvcTestBase + { + [Fact] + public async Task Should_404_If_Feature_Disabled() + { + var result = await GetResponseAsync("/api/DisabledGlobalFeatureTestController-Test/TestMethod", HttpStatusCode.NotFound); + result.StatusCode.ShouldBe(HttpStatusCode.NotFound); + } + + [Fact] + public async Task Should_Pass_Check_If_Feature_Enabled() + { + var result = await GetResponseAsync("/api/EnabledGlobalFeatureTestController-Test/TestMethod", HttpStatusCode.OK); + result.StatusCode.ShouldBe(HttpStatusCode.OK); + } + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/RequiresGlobalFeatureTestPage_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/RequiresGlobalFeatureTestPage_Tests.cs new file mode 100644 index 0000000000..634eefe1fa --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/GlobalFeatures/RequiresGlobalFeatureTestPage_Tests.cs @@ -0,0 +1,24 @@ +using System.Net; +using System.Threading.Tasks; +using Shouldly; +using Xunit; + +namespace Volo.Abp.AspNetCore.Mvc.GlobalFeatures +{ + public class RequiresGlobalFeatureTestPage_Tests : AspNetCoreMvcTestBase + { + [Fact] + public async Task Should_404_If_Feature_Disabled() + { + var result = await GetResponseAsync("/GlobalFeatures/DisabledGlobalFeatureTestPage", HttpStatusCode.NotFound); + result.StatusCode.ShouldBe(HttpStatusCode.NotFound); + } + + [Fact] + public async Task Should_Pass_Check_If_Feature_Enabled() + { + var result = await GetResponseAsync("/GlobalFeatures/EnabledGlobalFeatureTestPage", HttpStatusCode.OK); + result.StatusCode.ShouldBe(HttpStatusCode.OK); + } + } +}