From 8512aaa33a320c5cd257dda35170edd30864ffeb Mon Sep 17 00:00:00 2001
From: cKey <35512826+colinin@users.noreply.github.com>
Date: Sat, 26 Sep 2020 17:29:24 +0800
Subject: [PATCH] Add feature: Function check for call limit
---
aspnet-core/LINGYUN.MicroService.sln | 32 ++-
...NGYUN.Abp.Features.Validation.Redis.csproj | 25 +++
.../Redis/AbpFeaturesValidationRedisModule.cs | 25 +++
.../AbpRedisRequiresLimitFeatureOptions.cs | 26 +++
.../Features/Validation/Redis/Lua/check.lua | 4 +
.../Features/Validation/Redis/Lua/process.lua | 6 +
.../Redis/RedisRequiresLimitFeatureChecker.cs | 198 ++++++++++++++++++
.../System/BytesExtensions.cs | 16 ++
.../System/StringExtensions.cs | 17 ++
.../LINGYUN.Abp.Features.Validation.csproj | 12 ++
.../Validation/AbpFeaturesValidationModule.cs | 15 ++
.../FeaturesValidationInterceptor.cs | 82 ++++++++
.../FeaturesValidationInterceptorRegistrar.cs | 38 ++++
.../IRequiresLimitFeatureChecker.cs | 12 ++
.../Abp/Features/Validation/LimitPolicy.cs | 26 +++
.../NullRequiresLimitFeatureChecker.cs | 19 ++
.../RequiresLimitFeatureAttribute.cs | 34 +++
.../Validation/RequiresLimitFeatureContext.cs | 56 +++++
.../AuthIdentityServerModule.cs | 10 +-
...Abp.Features.Validation.Redis.Tests.csproj | 23 ++
.../AbpFeaturesValidationRedisTestBase.cs | 9 +
.../AbpFeaturesValidationRedisTestModule.cs | 25 +++
.../RedisRequiresLimitFeatureCheckerTests.cs | 53 +++++
...NGYUN.Abp.Features.Validation.Tests.csproj | 21 ++
.../AbpFeaturesValidationTestBase.cs | 8 +
.../AbpFeaturesValidationTestModule.cs | 23 ++
.../FakeRequiresFeatureLimitChecker.cs | 44 ++++
.../Validation/FeaturesValidationTests.cs | 42 ++++
.../TestFeatureDefinitionProvider.cs | 17 ++
.../Features/Validation/TestFeatureNames.cs | 9 +
.../Features/Validation/TestFeatureTenant.cs | 7 +
.../Validation/TestValidationFeatureClass.cs | 50 +++++
.../LINGYUN.Abp.TestsBase.csproj | 1 +
.../LINGYUN/Abp/Tests/AbpTestsBaseModule.cs | 10 +-
.../Abp/Tests/Features/FakeFeatureOptions.cs | 20 ++
.../Abp/Tests/Features/FakeFeatureStore.cs | 29 +++
36 files changed, 1033 insertions(+), 11 deletions(-)
create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN.Abp.Features.Validation.Redis.csproj
create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/AbpFeaturesValidationRedisModule.cs
create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/AbpRedisRequiresLimitFeatureOptions.cs
create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/Lua/check.lua
create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/Lua/process.lua
create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/RedisRequiresLimitFeatureChecker.cs
create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/System/BytesExtensions.cs
create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/System/StringExtensions.cs
create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN.Abp.Features.Validation.csproj
create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/AbpFeaturesValidationModule.cs
create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/FeaturesValidationInterceptor.cs
create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/FeaturesValidationInterceptorRegistrar.cs
create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/IRequiresLimitFeatureChecker.cs
create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/LimitPolicy.cs
create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/NullRequiresLimitFeatureChecker.cs
create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/RequiresLimitFeatureAttribute.cs
create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/RequiresLimitFeatureContext.cs
create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN.Abp.Features.Validation.Redis.Tests.csproj
create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN/Abp/Features/Validation/Redis/AbpFeaturesValidationRedisTestBase.cs
create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN/Abp/Features/Validation/Redis/AbpFeaturesValidationRedisTestModule.cs
create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN/Abp/Features/Validation/Redis/RedisRequiresLimitFeatureCheckerTests.cs
create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN.Abp.Features.Validation.Tests.csproj
create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/AbpFeaturesValidationTestBase.cs
create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/AbpFeaturesValidationTestModule.cs
create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/FakeRequiresFeatureLimitChecker.cs
create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/FeaturesValidationTests.cs
create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestFeatureDefinitionProvider.cs
create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestFeatureNames.cs
create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestFeatureTenant.cs
create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestValidationFeatureClass.cs
create mode 100644 aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/Features/FakeFeatureOptions.cs
create mode 100644 aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/Features/FakeFeatureStore.cs
diff --git a/aspnet-core/LINGYUN.MicroService.sln b/aspnet-core/LINGYUN.MicroService.sln
index 11889f78e..efdbe84ca 100644
--- a/aspnet-core/LINGYUN.MicroService.sln
+++ b/aspnet-core/LINGYUN.MicroService.sln
@@ -213,9 +213,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Identity.Entity
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "account", "account", "{685188AC-A145-4A27-BF5F-9C970A59AA9C}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.IdentityServer4.HttpApi.Host", "services\identity-server\LINGYUN.Abp.IdentityServer4.HttpApi.Host\LINGYUN.Abp.IdentityServer4.HttpApi.Host.csproj", "{F85552D4-D22E-483A-B1F8-3DFB840F6F7C}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.IdentityServer4.HttpApi.Host", "services\identity-server\LINGYUN.Abp.IdentityServer4.HttpApi.Host\LINGYUN.Abp.IdentityServer4.HttpApi.Host.csproj", "{F85552D4-D22E-483A-B1F8-3DFB840F6F7C}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.Settings", "modules\common\LINGYUN.Abp.Settings\LINGYUN.Abp.Settings.csproj", "{6AA0785D-9B6C-4EAE-AB83-0C4CF2B6B473}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Settings", "modules\common\LINGYUN.Abp.Settings\LINGYUN.Abp.Settings.csproj", "{6AA0785D-9B6C-4EAE-AB83-0C4CF2B6B473}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Features.Validation", "modules\common\LINGYUN.Abp.Features\LINGYUN.Abp.Features.Validation.csproj", "{65DE28D5-DFEA-43E5-B820-BAF09A1FC4ED}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Features.Validation.Redis", "modules\common\LINGYUN.Abp.Features.Validation.Redis\LINGYUN.Abp.Features.Validation.Redis.csproj", "{D3E65610-4167-4235-9C9D-1E1FAD4C0CE6}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Features.Validation.Redis.Tests", "tests\LINGYUN.Abp.Features.Validation.Redis.Tests\LINGYUN.Abp.Features.Validation.Redis.Tests.csproj", "{F12F4645-C0FE-4129-8C71-65B4039DC445}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.Features.Validation.Tests", "tests\LINGYUN.Abp.Features.Validation.Tests\LINGYUN.Abp.Features.Validation.Tests.csproj", "{C457FA70-8732-44B8-A018-C96D14025D4B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -575,6 +583,22 @@ Global
{6AA0785D-9B6C-4EAE-AB83-0C4CF2B6B473}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6AA0785D-9B6C-4EAE-AB83-0C4CF2B6B473}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6AA0785D-9B6C-4EAE-AB83-0C4CF2B6B473}.Release|Any CPU.Build.0 = Release|Any CPU
+ {65DE28D5-DFEA-43E5-B820-BAF09A1FC4ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {65DE28D5-DFEA-43E5-B820-BAF09A1FC4ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {65DE28D5-DFEA-43E5-B820-BAF09A1FC4ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {65DE28D5-DFEA-43E5-B820-BAF09A1FC4ED}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D3E65610-4167-4235-9C9D-1E1FAD4C0CE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D3E65610-4167-4235-9C9D-1E1FAD4C0CE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D3E65610-4167-4235-9C9D-1E1FAD4C0CE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D3E65610-4167-4235-9C9D-1E1FAD4C0CE6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F12F4645-C0FE-4129-8C71-65B4039DC445}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F12F4645-C0FE-4129-8C71-65B4039DC445}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F12F4645-C0FE-4129-8C71-65B4039DC445}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F12F4645-C0FE-4129-8C71-65B4039DC445}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C457FA70-8732-44B8-A018-C96D14025D4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C457FA70-8732-44B8-A018-C96D14025D4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C457FA70-8732-44B8-A018-C96D14025D4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C457FA70-8732-44B8-A018-C96D14025D4B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -684,6 +708,10 @@ Global
{685188AC-A145-4A27-BF5F-9C970A59AA9C} = {672E1170-7B18-474B-85C7-1961BF2A48AE}
{F85552D4-D22E-483A-B1F8-3DFB840F6F7C} = {E2408063-FB1F-4513-B4A7-1FE50667C6E8}
{6AA0785D-9B6C-4EAE-AB83-0C4CF2B6B473} = {8AC72641-30D3-4ACF-89FA-808FADC55C2E}
+ {65DE28D5-DFEA-43E5-B820-BAF09A1FC4ED} = {8AC72641-30D3-4ACF-89FA-808FADC55C2E}
+ {D3E65610-4167-4235-9C9D-1E1FAD4C0CE6} = {8AC72641-30D3-4ACF-89FA-808FADC55C2E}
+ {F12F4645-C0FE-4129-8C71-65B4039DC445} = {370D7CD5-1E17-4F3D-BBFA-03429F6D4F2F}
+ {C457FA70-8732-44B8-A018-C96D14025D4B} = {370D7CD5-1E17-4F3D-BBFA-03429F6D4F2F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C95FDF91-16F2-4A8B-A4BE-0E62D1B66718}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN.Abp.Features.Validation.Redis.csproj b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN.Abp.Features.Validation.Redis.csproj
new file mode 100644
index 000000000..16c2c6c76
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN.Abp.Features.Validation.Redis.csproj
@@ -0,0 +1,25 @@
+
+
+
+ netstandard2.0
+
+ 8.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/AbpFeaturesValidationRedisModule.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/AbpFeaturesValidationRedisModule.cs
new file mode 100644
index 000000000..0168165aa
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/AbpFeaturesValidationRedisModule.cs
@@ -0,0 +1,25 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Volo.Abp.Modularity;
+using Volo.Abp.VirtualFileSystem;
+
+namespace LINGYUN.Abp.Features.Validation.Redis
+{
+ [DependsOn(
+ typeof(AbpFeaturesValidationModule))]
+ public class AbpFeaturesValidationRedisModule : AbpModule
+ {
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ var configuration = context.Services.GetConfiguration();
+ Configure(configuration.GetSection("Features:Validation:Redis"));
+
+ Configure(options =>
+ {
+ options.FileSets.AddEmbedded();
+ });
+
+ context.Services.Replace(ServiceDescriptor.Singleton());
+ }
+ }
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/AbpRedisRequiresLimitFeatureOptions.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/AbpRedisRequiresLimitFeatureOptions.cs
new file mode 100644
index 000000000..ae09acb15
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/AbpRedisRequiresLimitFeatureOptions.cs
@@ -0,0 +1,26 @@
+using Microsoft.Extensions.Options;
+using StackExchange.Redis;
+
+namespace LINGYUN.Abp.Features.Validation.Redis
+{
+ public class AbpRedisRequiresLimitFeatureOptions : IOptions
+ {
+ public string Configuration { get; set; }
+ public string InstanceName { get; set; }
+ public ConfigurationOptions ConfigurationOptions { get; set; }
+ ///
+ /// 失败重试次数
+ /// default: 3
+ ///
+ public int FailedRetryCount { get; set; } = 3;
+ ///
+ /// 失败重试间隔 ms
+ /// default: 1000
+ ///
+ public int FailedRetryInterval { get; set; } = 1000;
+ AbpRedisRequiresLimitFeatureOptions IOptions.Value
+ {
+ get { return this; }
+ }
+ }
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/Lua/check.lua b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/Lua/check.lua
new file mode 100644
index 000000000..5a96ae20a
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/Lua/check.lua
@@ -0,0 +1,4 @@
+if (redis.call('EXISTS', KEYS[1]) == 0) then
+ redis.call('SETEX',KEYS[1],ARGV[1], 0)
+end
+return tonumber(redis.call('GET', KEYS[1]))
\ No newline at end of file
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/Lua/process.lua b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/Lua/process.lua
new file mode 100644
index 000000000..9585c4990
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/Lua/process.lua
@@ -0,0 +1,6 @@
+if (redis.call('EXISTS',KEYS[1]) ~= 0) then
+ redis.call('INCRBY',KEYS[1], 1)
+else
+ redis.call('SETEX',KEYS[1],ARGV[1],0)
+end
+return tonumber(redis.call('GET',KEYS[1]))
\ No newline at end of file
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/RedisRequiresLimitFeatureChecker.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/RedisRequiresLimitFeatureChecker.cs
new file mode 100644
index 000000000..c07e601e5
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/RedisRequiresLimitFeatureChecker.cs
@@ -0,0 +1,198 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using StackExchange.Redis;
+using System;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Volo.Abp;
+using Volo.Abp.Authorization;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.MultiTenancy;
+using Volo.Abp.VirtualFileSystem;
+
+namespace LINGYUN.Abp.Features.Validation.Redis
+{
+ [DisableConventionalRegistration]
+ public class RedisRequiresLimitFeatureChecker : IRequiresLimitFeatureChecker
+ {
+ private const string CHECK_LUA_SCRIPT = "/LINGYUN/Abp/Features/Validation/Redis/Lua/check.lua";
+ private const string PROCESS_LUA_SCRIPT = "/LINGYUN/Abp/Features/Validation/Redis/Lua/process.lua";
+
+ public ILogger Logger { protected get; set; }
+
+ private volatile ConnectionMultiplexer _connection;
+ private volatile ConfigurationOptions _redisConfig;
+ private IDatabaseAsync _redis;
+ private IServer _server;
+
+ private IVirtualFileProvider _virtualFileProvider;
+ private ICurrentTenant _currentTenant;
+ private AbpRedisRequiresLimitFeatureOptions _options;
+ private readonly string _instance;
+
+ private readonly SemaphoreSlim _connectionLock = new SemaphoreSlim(initialCount: 1, maxCount: 1);
+
+ public RedisRequiresLimitFeatureChecker(
+ ICurrentTenant currentTenant,
+ IVirtualFileProvider virtualFileProvider,
+ IOptions optionsAccessor)
+ {
+ if (optionsAccessor == null)
+ {
+ throw new ArgumentNullException(nameof(optionsAccessor));
+ }
+
+ _options = optionsAccessor.Value;
+ _currentTenant = currentTenant;
+ _virtualFileProvider = virtualFileProvider;
+
+ _instance = _options.InstanceName ?? string.Empty;
+
+ Logger = NullLogger.Instance;
+ }
+
+ public virtual async Task CheckAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default)
+ {
+ await ConnectAsync(cancellation);
+
+ var result = await EvaluateAsync(CHECK_LUA_SCRIPT, context, cancellation);
+ if (result + 1 > context.Limit)
+ {
+ throw new AbpAuthorizationException("已经超出功能次数限制,请联系管理员");
+ }
+ }
+
+ public virtual async Task ProcessAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default)
+ {
+ await ConnectAsync(cancellation);
+
+ await EvaluateAsync(PROCESS_LUA_SCRIPT, context, cancellation);
+ }
+
+ private async Task EvaluateAsync(string luaScriptFilePath, RequiresLimitFeatureContext context, CancellationToken cancellation = default)
+ {
+ var luaScriptFile = _virtualFileProvider.GetFileInfo(luaScriptFilePath);
+ using var luaScriptFileStream = luaScriptFile.CreateReadStream();
+ var fileBytes = await luaScriptFileStream.GetAllBytesAsync(cancellation);
+
+ var luaSha1 = fileBytes.Sha1();
+ if (!await _server.ScriptExistsAsync(luaSha1))
+ {
+ var luaScript = Encoding.UTF8.GetString(fileBytes);
+ luaSha1 = await _server.ScriptLoadAsync(luaScript);
+ }
+
+ var keys = new RedisKey[1] { NormalizeKey(context) };
+ var values = new RedisValue[] { context.GetEffectTicks() };
+ var result = await _redis.ScriptEvaluateAsync(luaSha1, keys, values);
+ if (result.Type == ResultType.Error)
+ {
+ throw new AbpException($"脚本执行错误:{result}");
+ }
+ return (int)result;
+ }
+
+ private string NormalizeKey(RequiresLimitFeatureContext context)
+ {
+ if (_currentTenant.IsAvailable)
+ {
+ return $"{_instance}t:RequiresLimitFeature;t:{_currentTenant.Id};f:{context.Feature}";
+ }
+ return $"{_instance}c:RequiresLimitFeature;f:{context.Feature}";
+ }
+
+ private void RegistenConnectionEvent(ConnectionMultiplexer connection)
+ {
+ if (connection != null)
+ {
+ connection.ConnectionFailed += OnConnectionFailed;
+ connection.ConnectionRestored += OnConnectionRestored;
+ connection.ErrorMessage += OnErrorMessage;
+ connection.ConfigurationChanged += OnConfigurationChanged;
+ connection.HashSlotMoved += OnHashSlotMoved;
+ connection.InternalError += OnInternalError;
+ connection.ConfigurationChangedBroadcast += OnConfigurationChangedBroadcast;
+ }
+ }
+
+ private async Task ConnectAsync(CancellationToken token = default(CancellationToken))
+ {
+ token.ThrowIfCancellationRequested();
+
+ if (_redis != null)
+ {
+ return;
+ }
+
+ await _connectionLock.WaitAsync(token);
+ try
+ {
+ if (_redis == null)
+ {
+ if (_options.ConfigurationOptions != null)
+ {
+ _redisConfig = _options.ConfigurationOptions;
+ }
+ else
+ {
+ _redisConfig = ConfigurationOptions.Parse(_options.Configuration);
+ }
+ _redisConfig.AllowAdmin = true;
+ _redisConfig.SetDefaultPorts();
+ _connection = await ConnectionMultiplexer.ConnectAsync(_redisConfig);
+ RegistenConnectionEvent(_connection);
+ _redis = _connection.GetDatabase();
+ _server = _connection.GetServer(_redisConfig.EndPoints[0]);
+ }
+ }
+ finally
+ {
+ _connectionLock.Release();
+ }
+ }
+
+ private void OnConfigurationChangedBroadcast(object sender, EndPointEventArgs e)
+ {
+ Logger.LogInformation("Redis server master/slave changes");
+ }
+
+ private void OnInternalError(object sender, InternalErrorEventArgs e)
+ {
+ Logger.LogError("Redis internal error, origin:{0}, connectionType:{1}",
+ e.Origin, e.ConnectionType);
+ Logger.LogError(e.Exception, "Redis internal error");
+
+ }
+
+ private void OnHashSlotMoved(object sender, HashSlotMovedEventArgs e)
+ {
+ Logger.LogInformation("Redis configuration changed");
+ }
+
+ private void OnConfigurationChanged(object sender, EndPointEventArgs e)
+ {
+ Logger.LogInformation("Redis configuration changed");
+ }
+
+ private void OnErrorMessage(object sender, RedisErrorEventArgs e)
+ {
+ Logger.LogWarning("Redis error, message:{0}", e.Message);
+ }
+
+ private void OnConnectionRestored(object sender, ConnectionFailedEventArgs e)
+ {
+ Logger.LogWarning("Redis connection restored, failureType:{0}, connectionType:{1}",
+ e.FailureType, e.ConnectionType);
+ }
+
+ private void OnConnectionFailed(object sender, ConnectionFailedEventArgs e)
+ {
+ Logger.LogError("Redis connection failed, failureType:{0}, connectionType:{1}",
+ e.FailureType, e.ConnectionType);
+ Logger.LogError(e.Exception, "Redis lock connection failed");
+ }
+ }
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/System/BytesExtensions.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/System/BytesExtensions.cs
new file mode 100644
index 000000000..d1bad7915
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/System/BytesExtensions.cs
@@ -0,0 +1,16 @@
+using System.Security.Cryptography;
+
+namespace System
+{
+ internal static class BytesExtensions
+ {
+ public static byte[] Sha1(this byte[] data)
+ {
+ using (var sha = SHA1.Create())
+ {
+ var hashBytes = sha.ComputeHash(data);
+ return hashBytes;
+ }
+ }
+ }
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/System/StringExtensions.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/System/StringExtensions.cs
new file mode 100644
index 000000000..3571cef99
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/System/StringExtensions.cs
@@ -0,0 +1,17 @@
+using System.Security.Cryptography;
+using System.Text;
+
+namespace System
+{
+ internal static class StringExtensions
+ {
+ public static byte[] Sha1(this string str)
+ {
+ using (var sha = SHA1.Create())
+ {
+ var hashBytes = sha.ComputeHash(Encoding.UTF8.GetBytes(str));
+ return hashBytes;
+ }
+ }
+ }
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN.Abp.Features.Validation.csproj b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN.Abp.Features.Validation.csproj
new file mode 100644
index 000000000..a9f25c974
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN.Abp.Features.Validation.csproj
@@ -0,0 +1,12 @@
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/AbpFeaturesValidationModule.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/AbpFeaturesValidationModule.cs
new file mode 100644
index 000000000..07ec363e1
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/AbpFeaturesValidationModule.cs
@@ -0,0 +1,15 @@
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.Features;
+using Volo.Abp.Modularity;
+
+namespace LINGYUN.Abp.Features.Validation
+{
+ [DependsOn(typeof(AbpFeaturesModule))]
+ public class AbpFeaturesValidationModule : AbpModule
+ {
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.OnRegistred(FeaturesValidationInterceptorRegistrar.RegisterIfNeeded);
+ }
+ }
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/FeaturesValidationInterceptor.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/FeaturesValidationInterceptor.cs
new file mode 100644
index 000000000..4035664fd
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/FeaturesValidationInterceptor.cs
@@ -0,0 +1,82 @@
+using System.Reflection;
+using System.Threading.Tasks;
+using Volo.Abp.Aspects;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.DynamicProxy;
+using Volo.Abp.Features;
+using Volo.Abp.Validation.StringValues;
+
+namespace LINGYUN.Abp.Features.Validation
+{
+ public class FeaturesValidationInterceptor : AbpInterceptor, ITransientDependency
+ {
+ private readonly IFeatureChecker _featureChecker;
+ private readonly IRequiresLimitFeatureChecker _limitFeatureChecker;
+ private readonly IFeatureDefinitionManager _featureDefinitionManager;
+
+ public FeaturesValidationInterceptor(
+ IFeatureChecker featureChecker,
+ IRequiresLimitFeatureChecker limitFeatureChecker,
+ IFeatureDefinitionManager featureDefinitionManager)
+ {
+ _featureChecker = featureChecker;
+ _limitFeatureChecker = limitFeatureChecker;
+ _featureDefinitionManager = featureDefinitionManager;
+
+ }
+
+ public override async Task InterceptAsync(IAbpMethodInvocation invocation)
+ {
+ if (AbpCrossCuttingConcerns.IsApplied(invocation.TargetObject, AbpCrossCuttingConcerns.FeatureChecking))
+ {
+ await invocation.ProceedAsync();
+ return;
+ }
+
+ var limitFeature = GetRequiresLimitFeature(invocation.Method);
+
+ if (limitFeature == null)
+ {
+ await invocation.ProceedAsync();
+ return;
+ }
+
+ // 获取功能限制时长
+ var limit = await _featureChecker.GetAsync(limitFeature.Feature, limitFeature.DefaultLimit);
+
+ var limitFeatureContext = new RequiresLimitFeatureContext(limitFeature.Feature, limitFeature.Policy, limit);
+ // 检查次数限制
+ await PreCheckFeatureAsync(limitFeatureContext);
+ // 执行代理方法
+ await invocation.ProceedAsync();
+ // 调用次数递增
+ // TODO: 使用Redis结合Lua脚本?
+ await PostCheckFeatureAsync(limitFeatureContext);
+ }
+
+ protected virtual async Task PreCheckFeatureAsync(RequiresLimitFeatureContext context)
+ {
+ await _limitFeatureChecker.CheckAsync(context);
+ }
+
+ protected virtual async Task PostCheckFeatureAsync(RequiresLimitFeatureContext context)
+ {
+ await _limitFeatureChecker.ProcessAsync(context);
+ }
+
+ protected virtual RequiresLimitFeatureAttribute GetRequiresLimitFeature(MethodInfo methodInfo)
+ {
+ var limitFeature = methodInfo.GetCustomAttribute(false);
+ if (limitFeature != null)
+ {
+ var featureDefinition = _featureDefinitionManager.GetOrNull(limitFeature.Feature);
+ if (featureDefinition != null &&
+ typeof(NumericValueValidator).IsAssignableFrom(featureDefinition.ValueType.Validator.GetType()))
+ {
+ return limitFeature;
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/FeaturesValidationInterceptorRegistrar.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/FeaturesValidationInterceptorRegistrar.cs
new file mode 100644
index 000000000..40f7fa486
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/FeaturesValidationInterceptorRegistrar.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.DynamicProxy;
+
+namespace LINGYUN.Abp.Features.Validation
+{
+ public static class FeaturesValidationInterceptorRegistrar
+ {
+ public static void RegisterIfNeeded(IOnServiceRegistredContext context)
+ {
+ if (ShouldIntercept(context.ImplementationType))
+ {
+ context.Interceptors.TryAdd();
+ }
+ }
+
+ private static bool ShouldIntercept(Type type)
+ {
+ return !DynamicProxyIgnoreTypes.Contains(type) &&
+ (type.IsDefined(typeof(RequiresLimitFeatureAttribute), true) ||
+ AnyMethodHasRequiresLimitFeatureAttribute(type));
+ }
+
+ private static bool AnyMethodHasRequiresLimitFeatureAttribute(Type implementationType)
+ {
+ return implementationType
+ .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
+ .Any(HasRequiresLimitFeatureAttribute);
+ }
+
+ private static bool HasRequiresLimitFeatureAttribute(MemberInfo methodInfo)
+ {
+ return methodInfo.IsDefined(typeof(RequiresLimitFeatureAttribute), true);
+ }
+ }
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/IRequiresLimitFeatureChecker.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/IRequiresLimitFeatureChecker.cs
new file mode 100644
index 000000000..cc73875d5
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/IRequiresLimitFeatureChecker.cs
@@ -0,0 +1,12 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Features.Validation
+{
+ public interface IRequiresLimitFeatureChecker
+ {
+ Task CheckAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default);
+
+ Task ProcessAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default);
+ }
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/LimitPolicy.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/LimitPolicy.cs
new file mode 100644
index 000000000..b09e07ba7
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/LimitPolicy.cs
@@ -0,0 +1,26 @@
+namespace LINGYUN.Abp.Features.Validation
+{
+ public enum LimitPolicy
+ {
+ ///
+ /// 按小时限制
+ ///
+ Hours = 0,
+ ///
+ /// 按天限制
+ ///
+ Days = 1,
+ ///
+ /// 按周限制
+ ///
+ Weeks = 2,
+ ///
+ /// 按月限制
+ ///
+ Month = 3,
+ ///
+ /// 按年限制
+ ///
+ Years = 4
+ }
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/NullRequiresLimitFeatureChecker.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/NullRequiresLimitFeatureChecker.cs
new file mode 100644
index 000000000..e6bed432c
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/NullRequiresLimitFeatureChecker.cs
@@ -0,0 +1,19 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+
+namespace LINGYUN.Abp.Features.Validation
+{
+ public class NullRequiresLimitFeatureChecker : IRequiresLimitFeatureChecker, ISingletonDependency
+ {
+ public Task CheckAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default)
+ {
+ return Task.CompletedTask;
+ }
+
+ public Task ProcessAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default)
+ {
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/RequiresLimitFeatureAttribute.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/RequiresLimitFeatureAttribute.cs
new file mode 100644
index 000000000..1d19af4e5
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/RequiresLimitFeatureAttribute.cs
@@ -0,0 +1,34 @@
+using System;
+
+namespace LINGYUN.Abp.Features.Validation
+{
+ ///
+ /// 单个功能的调用量限制
+ ///
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
+ public class RequiresLimitFeatureAttribute : Attribute
+ {
+ ///
+ /// 功能限制策略
+ ///
+ public LimitPolicy Policy { get; }
+ ///
+ /// 默认限制时长
+ ///
+ public int DefaultLimit { get; }
+ ///
+ /// 功能名称
+ ///
+ public string Feature { get; }
+
+ public RequiresLimitFeatureAttribute(
+ string feature,
+ LimitPolicy policy = LimitPolicy.Month,
+ int defaultLimit = 1)
+ {
+ DefaultLimit = defaultLimit;
+ Policy = policy;
+ Feature = feature;
+ }
+ }
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/RequiresLimitFeatureContext.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/RequiresLimitFeatureContext.cs
new file mode 100644
index 000000000..568b0f793
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/RequiresLimitFeatureContext.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Collections.Generic;
+
+namespace LINGYUN.Abp.Features.Validation
+{
+ public class RequiresLimitFeatureContext
+ {
+ ///
+ /// 功能限制策略
+ ///
+ public LimitPolicy Policy { get; }
+ ///
+ /// 功能限制时长
+ ///
+ public int Limit { get; }
+ ///
+ /// 功能名称
+ ///
+ public string Feature { get; }
+
+ private Lazy>> effectPolicysLazy;
+ private IDictionary> effectPolicys => effectPolicysLazy.Value;
+ public RequiresLimitFeatureContext(
+ string feature,
+ LimitPolicy policy = LimitPolicy.Month,
+ int limit = 1)
+ {
+ Limit = limit;
+ Policy = policy;
+ Feature = feature;
+
+ effectPolicysLazy = new Lazy>>(() => CreateFeatureLimitPolicy());
+ }
+
+ ///
+ /// 获取生效时间戳
+ ///
+ ///
+ public long GetEffectTicks()
+ {
+ return effectPolicys[Policy](Limit);
+ }
+
+ protected IDictionary> CreateFeatureLimitPolicy()
+ {
+ return new Dictionary>()
+ {
+ { LimitPolicy.Days, (time) => { return (long)(DateTimeOffset.UtcNow.AddDays(time) - DateTimeOffset.UtcNow).TotalSeconds; } },
+ { LimitPolicy.Hours, (time) => { return (long)(DateTimeOffset.UtcNow.AddHours(time) - DateTimeOffset.UtcNow).TotalSeconds; } },
+ { LimitPolicy.Month, (time) => { return (long)(DateTimeOffset.UtcNow.AddMonths(time) - DateTimeOffset.UtcNow).TotalSeconds; } },
+ { LimitPolicy.Weeks, (time) => { return (long)(DateTimeOffset.UtcNow.AddDays(time * 7) - DateTimeOffset.UtcNow).TotalSeconds; } },
+ { LimitPolicy.Years, (time) => { return (long)(DateTimeOffset.UtcNow.AddYears(time) - DateTimeOffset.UtcNow).TotalSeconds; } }
+ };
+ }
+ }
+}
diff --git a/aspnet-core/services/account/AuthServer.Host/AuthIdentityServerModule.cs b/aspnet-core/services/account/AuthServer.Host/AuthIdentityServerModule.cs
index 616b1ca65..ac9129eca 100644
--- a/aspnet-core/services/account/AuthServer.Host/AuthIdentityServerModule.cs
+++ b/aspnet-core/services/account/AuthServer.Host/AuthIdentityServerModule.cs
@@ -1,4 +1,5 @@
using DotNetCore.CAP;
+using IdentityServer4;
using LINGYUN.Abp.EventBus.CAP;
using LINGYUN.Abp.IdentityServer;
using LINGYUN.Abp.MultiTenancy.DbFinder;
@@ -10,6 +11,7 @@ using Microsoft.Extensions.Caching.StackExchangeRedis;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
+using Microsoft.IdentityModel.Tokens;
using StackExchange.Redis;
using System;
using System.Linq;
@@ -147,13 +149,7 @@ namespace AuthServer.Host
});
// context.Services.AddAuthentication();
- //context.Services.AddAuthentication()
- // .AddIdentityServerAuthentication(options =>
- // {
- // options.Authority = configuration["AuthServer:Authority"];
- // options.RequireHttpsMetadata = false;
- // options.ApiName = configuration["AuthServer:ApiName"];
- // });
+ context.Services.AddAuthentication().AddCookie("Cookie");
Configure(options =>
{
diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN.Abp.Features.Validation.Redis.Tests.csproj b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN.Abp.Features.Validation.Redis.Tests.csproj
new file mode 100644
index 000000000..ae4b84013
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN.Abp.Features.Validation.Redis.Tests.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net5.0
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN/Abp/Features/Validation/Redis/AbpFeaturesValidationRedisTestBase.cs b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN/Abp/Features/Validation/Redis/AbpFeaturesValidationRedisTestBase.cs
new file mode 100644
index 000000000..55ee092dc
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN/Abp/Features/Validation/Redis/AbpFeaturesValidationRedisTestBase.cs
@@ -0,0 +1,9 @@
+using LINGYUN.Abp.Tests;
+
+namespace LINGYUN.Abp.Features.Validation.Redis
+{
+ public class AbpFeaturesValidationRedisTestBase : AbpTestsBase
+ {
+
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN/Abp/Features/Validation/Redis/AbpFeaturesValidationRedisTestModule.cs b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN/Abp/Features/Validation/Redis/AbpFeaturesValidationRedisTestModule.cs
new file mode 100644
index 000000000..827e67adf
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN/Abp/Features/Validation/Redis/AbpFeaturesValidationRedisTestModule.cs
@@ -0,0 +1,25 @@
+using LINGYUN.Abp.Tests;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.Modularity;
+
+namespace LINGYUN.Abp.Features.Validation.Redis
+{
+ [DependsOn(
+ typeof(AbpFeaturesValidationTestModule),
+ typeof(AbpFeaturesValidationRedisModule),
+ typeof(AbpTestsBaseModule))]
+ public class AbpFeaturesValidationRedisTestModule : AbpModule
+ {
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ var configurationOptions = new AbpConfigurationBuilderOptions
+ {
+ BasePath = @"D:\Projects\Development\Abp\FeaturesValidation\Redis",
+ EnvironmentName = "Development"
+ };
+
+ context.Services.ReplaceConfiguration(ConfigurationHelper.BuildConfiguration(configurationOptions));
+ }
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN/Abp/Features/Validation/Redis/RedisRequiresLimitFeatureCheckerTests.cs b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN/Abp/Features/Validation/Redis/RedisRequiresLimitFeatureCheckerTests.cs
new file mode 100644
index 000000000..642ab3aa6
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN/Abp/Features/Validation/Redis/RedisRequiresLimitFeatureCheckerTests.cs
@@ -0,0 +1,53 @@
+using Shouldly;
+using System;
+using System.Threading.Tasks;
+using Volo.Abp.Authorization;
+using Xunit;
+
+namespace LINGYUN.Abp.Features.Validation.Redis
+{
+ public class RedisRequiresLimitFeatureCheckerTests : AbpFeaturesValidationRedisTestBase
+ {
+ protected IRequiresLimitFeatureChecker RequiresLimitFeatureChecker { get; }
+ protected TestValidationFeatureClass TestValidationFeatureClass { get; }
+ public RedisRequiresLimitFeatureCheckerTests()
+ {
+ RequiresLimitFeatureChecker = GetRequiredService();
+ TestValidationFeatureClass = GetRequiredService();
+ }
+
+ [Fact]
+ public virtual async Task Check_Test_Async()
+ {
+ var context = new RequiresLimitFeatureContext(TestFeatureNames.TestFeature1, LimitPolicy.Days, 10);
+ await RequiresLimitFeatureChecker.CheckAsync(context);
+ }
+
+ [Fact]
+ public virtual async Task Process_Test_Async()
+ {
+ var context = new RequiresLimitFeatureContext(TestFeatureNames.TestFeature1, LimitPolicy.Days, 10);
+ await RequiresLimitFeatureChecker.ProcessAsync(context);
+ }
+
+ [Fact]
+ public virtual async Task Check_Limit_Test_Async()
+ {
+ var context = new RequiresLimitFeatureContext(TestFeatureNames.TestFeature1, LimitPolicy.Days, 5);
+ await RequiresLimitFeatureChecker.ProcessAsync(context);
+ await RequiresLimitFeatureChecker.ProcessAsync(context);
+ await RequiresLimitFeatureChecker.ProcessAsync(context);
+ await RequiresLimitFeatureChecker.ProcessAsync(context);
+ await RequiresLimitFeatureChecker.ProcessAsync(context);
+ await RequiresLimitFeatureChecker.ProcessAsync(context);
+ try
+ {
+ await RequiresLimitFeatureChecker.CheckAsync(context);
+ }
+ catch(Exception ex)
+ {
+ ex.ShouldBeOfType();
+ }
+ }
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN.Abp.Features.Validation.Tests.csproj b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN.Abp.Features.Validation.Tests.csproj
new file mode 100644
index 000000000..3ccc79b33
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN.Abp.Features.Validation.Tests.csproj
@@ -0,0 +1,21 @@
+
+
+
+ net5.0
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/AbpFeaturesValidationTestBase.cs b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/AbpFeaturesValidationTestBase.cs
new file mode 100644
index 000000000..9aa794120
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/AbpFeaturesValidationTestBase.cs
@@ -0,0 +1,8 @@
+using LINGYUN.Abp.Tests;
+
+namespace LINGYUN.Abp.Features.Validation
+{
+ public class AbpFeaturesValidationTestBase : AbpTestsBase
+ {
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/AbpFeaturesValidationTestModule.cs b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/AbpFeaturesValidationTestModule.cs
new file mode 100644
index 000000000..2f8f68595
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/AbpFeaturesValidationTestModule.cs
@@ -0,0 +1,23 @@
+using LINGYUN.Abp.Tests;
+using LINGYUN.Abp.Tests.Features;
+using Volo.Abp.Modularity;
+
+namespace LINGYUN.Abp.Features.Validation
+{
+ [DependsOn(
+ typeof(AbpTestsBaseModule),
+ typeof(AbpFeaturesValidationModule))]
+ public class AbpFeaturesValidationTestModule : AbpModule
+ {
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ Configure(options =>
+ {
+ options.Map(TestFeatureNames.TestFeature1, (feature) =>
+ {
+ return 2.ToString();
+ });
+ });
+ }
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/FakeRequiresFeatureLimitChecker.cs b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/FakeRequiresFeatureLimitChecker.cs
new file mode 100644
index 000000000..41929b8be
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/FakeRequiresFeatureLimitChecker.cs
@@ -0,0 +1,44 @@
+using Microsoft.Extensions.DependencyInjection;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Volo.Abp.Authorization;
+using Volo.Abp.DependencyInjection;
+
+namespace LINGYUN.Abp.Features.Validation
+{
+ [Dependency(ServiceLifetime.Singleton, ReplaceServices = true)]
+ [ExposeServices(typeof(IRequiresLimitFeatureChecker))]
+ public class FakeRequiresFeatureLimitChecker : IRequiresLimitFeatureChecker
+ {
+ private readonly IDictionary limitFeatures;
+
+ public FakeRequiresFeatureLimitChecker()
+ {
+ limitFeatures = new Dictionary();
+ }
+
+ public virtual Task CheckAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default)
+ {
+ if (!limitFeatures.ContainsKey(context.Feature))
+ {
+ limitFeatures.Add(context.Feature, 0);
+ }
+ if (limitFeatures[context.Feature] > context.Limit)
+ {
+ throw new AbpAuthorizationException("已经超出功能次数限制,请联系管理员");
+ }
+ return Task.CompletedTask;
+ }
+
+ public Task ProcessAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default)
+ {
+ if (!limitFeatures.ContainsKey(context.Feature))
+ {
+ limitFeatures.Add(context.Feature, 1);
+ }
+ limitFeatures[context.Feature] += 1;
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/FeaturesValidationTests.cs b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/FeaturesValidationTests.cs
new file mode 100644
index 000000000..e330f5c93
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/FeaturesValidationTests.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Threading.Tasks;
+using Volo.Abp.Authorization;
+using Volo.Abp.MultiTenancy;
+using Xunit;
+
+namespace LINGYUN.Abp.Features.Validation
+{
+ public class FeaturesValidationTests : AbpFeaturesValidationTestBase
+ {
+ protected ICurrentTenant CurrentTenant { get; }
+ protected TestValidationFeatureClass TestValidationFeatureClass { get; }
+ public FeaturesValidationTests()
+ {
+ CurrentTenant = GetRequiredService();
+ TestValidationFeatureClass = GetRequiredService();
+ }
+
+ [Theory]
+ [InlineData(TestFeatureTenant.TenantId)] //Features were not enabled for Tenant 2
+ public async Task Should_Not_Allow_To_Call_Method_If_Has_Limit_Feature_Async(string tenantId)
+ {
+ using (CurrentTenant.Change(ParseNullableGuid(tenantId)))
+ {
+ // it's ok
+ await TestValidationFeatureClass.Test1HoursAsync();
+ await TestValidationFeatureClass.Test1HoursAsync();
+ await TestValidationFeatureClass.Test1HoursAsync();
+
+ await Assert.ThrowsAsync(async () =>
+ {
+ await TestValidationFeatureClass.Test1HoursAsync();
+ });
+ }
+ }
+
+ private static Guid? ParseNullableGuid(string tenantIdValue)
+ {
+ return tenantIdValue == null ? (Guid?)null : new Guid(tenantIdValue);
+ }
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestFeatureDefinitionProvider.cs b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestFeatureDefinitionProvider.cs
new file mode 100644
index 000000000..73bdf8c1a
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestFeatureDefinitionProvider.cs
@@ -0,0 +1,17 @@
+using Volo.Abp.Features;
+using Volo.Abp.Validation.StringValues;
+
+namespace LINGYUN.Abp.Features.Validation
+{
+ public class TestFeatureDefinitionProvider : FeatureDefinitionProvider
+ {
+ public override void Define(IFeatureDefinitionContext context)
+ {
+ var featureGroup = context.AddGroup(TestFeatureNames.GroupName);
+ featureGroup.AddFeature(
+ name: TestFeatureNames.TestFeature1,
+ defaultValue: 100.ToString(),
+ valueType: new ToggleStringValueType(new NumericValueValidator(1, 1000)));
+ }
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestFeatureNames.cs b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestFeatureNames.cs
new file mode 100644
index 000000000..3224a0cb4
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestFeatureNames.cs
@@ -0,0 +1,9 @@
+namespace LINGYUN.Abp.Features.Validation
+{
+ public class TestFeatureNames
+ {
+ public const string GroupName = "Abp.Features.Validation.Tests";
+
+ public const string TestFeature1 = GroupName + ".TestFeature1";
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestFeatureTenant.cs b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestFeatureTenant.cs
new file mode 100644
index 000000000..658f72478
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestFeatureTenant.cs
@@ -0,0 +1,7 @@
+namespace LINGYUN.Abp.Features.Validation
+{
+ public class TestFeatureTenant
+ {
+ public const string TenantId = "6355C04C-59D1-4133-944E-157179346BDC";
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestValidationFeatureClass.cs b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestValidationFeatureClass.cs
new file mode 100644
index 000000000..bb75c3442
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestValidationFeatureClass.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Features;
+
+namespace LINGYUN.Abp.Features.Validation
+{
+ public class TestValidationFeatureClass : ITransientDependency
+ {
+ [RequiresLimitFeature(TestFeatureNames.TestFeature1, LimitPolicy.Days, 1)]
+ public virtual Task Test1DaysAsync()
+ {
+ Console.WriteLine("this limit 1 days feature");
+
+ return Task.CompletedTask;
+ }
+
+ [RequiresLimitFeature(TestFeatureNames.TestFeature1, LimitPolicy.Month, 1)]
+ public virtual Task Test1MonthsAsync()
+ {
+ Console.WriteLine("this limit 1 month feature");
+
+ return Task.CompletedTask;
+ }
+
+ [RequiresLimitFeature(TestFeatureNames.TestFeature1, LimitPolicy.Weeks, 1)]
+ public virtual Task Test1WeeksAsync()
+ {
+ Console.WriteLine("this limit 1 weeks feature");
+
+ return Task.CompletedTask;
+ }
+
+ [RequiresLimitFeature(TestFeatureNames.TestFeature1, LimitPolicy.Hours, 1)]
+ public virtual Task Test1HoursAsync()
+ {
+ Console.WriteLine("this limit 1 hours feature");
+
+ return Task.CompletedTask;
+ }
+
+ [RequiresLimitFeature(TestFeatureNames.TestFeature1, LimitPolicy.Years, 1)]
+ public virtual Task Test1YearsAsync()
+ {
+ Console.WriteLine("this limit 1 years feature");
+
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN.Abp.TestsBase.csproj b/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN.Abp.TestsBase.csproj
index 7b049b352..007cb1002 100644
--- a/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN.Abp.TestsBase.csproj
+++ b/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN.Abp.TestsBase.csproj
@@ -15,6 +15,7 @@
+
diff --git a/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/AbpTestsBaseModule.cs b/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/AbpTestsBaseModule.cs
index f7b0d6cff..e9c19e6b1 100644
--- a/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/AbpTestsBaseModule.cs
+++ b/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/AbpTestsBaseModule.cs
@@ -1,7 +1,10 @@
-using Microsoft.Extensions.DependencyInjection;
+using LINGYUN.Abp.Tests.Features;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
using Volo.Abp;
using Volo.Abp.Authorization;
using Volo.Abp.Autofac;
+using Volo.Abp.Features;
using Volo.Abp.Modularity;
namespace LINGYUN.Abp.Tests
@@ -9,13 +12,16 @@ namespace LINGYUN.Abp.Tests
[DependsOn(
typeof(AbpAutofacModule),
typeof(AbpTestBaseModule),
- typeof(AbpAuthorizationModule)
+ typeof(AbpAuthorizationModule),
+ typeof(AbpFeaturesModule)
)]
public class AbpTestsBaseModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAlwaysAllowAuthorization();
+
+ context.Services.Replace(ServiceDescriptor.Singleton());
}
}
}
diff --git a/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/Features/FakeFeatureOptions.cs b/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/Features/FakeFeatureOptions.cs
new file mode 100644
index 000000000..98ad26c9b
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/Features/FakeFeatureOptions.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using Volo.Abp.Features;
+
+namespace LINGYUN.Abp.Tests.Features
+{
+ public class FakeFeatureOptions
+ {
+ public IDictionary> FeatureMaps { get; }
+ public FakeFeatureOptions()
+ {
+ FeatureMaps = new Dictionary>();
+ }
+
+ public void Map(string featureName, Func func)
+ {
+ FeatureMaps.AddIfNotContains(new KeyValuePair>(featureName, func));
+ }
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/Features/FakeFeatureStore.cs b/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/Features/FakeFeatureStore.cs
new file mode 100644
index 000000000..e84462a03
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/Features/FakeFeatureStore.cs
@@ -0,0 +1,29 @@
+using Microsoft.Extensions.Options;
+using System.Threading.Tasks;
+using Volo.Abp.Features;
+
+namespace LINGYUN.Abp.Tests.Features
+{
+ public class FakeFeatureStore : IFeatureStore
+ {
+ protected FakeFeatureOptions FakeFeatureOptions { get; }
+ protected IFeatureDefinitionManager FeatureDefinitionManager { get; }
+
+ public FakeFeatureStore(
+ IOptions options,
+ IFeatureDefinitionManager featureDefinitionManager)
+ {
+ FakeFeatureOptions = options.Value;
+ FeatureDefinitionManager = featureDefinitionManager;
+ }
+
+ public Task GetOrNullAsync(string name, string providerName, string providerKey)
+ {
+ var feature = FeatureDefinitionManager.Get(name);
+
+ var featureFunc = FakeFeatureOptions.FeatureMaps[name];
+
+ return Task.FromResult(featureFunc(feature));
+ }
+ }
+}