Browse Source

LimitFeature

master
wwwk 3 years ago
parent
commit
4bd53a6102
  1. 2
      Directory.Build.props
  2. 21
      Sanhe.Abp.Framework.sln
  3. 3
      modules/common/Sanhe.Abp.Features.LimitValidation.Redis.Client/FodyWeavers.xml
  4. 15
      modules/common/Sanhe.Abp.Features.LimitValidation.Redis.Client/Sanhe.Abp.Features.LimitValidation.Redis.Client.csproj
  5. 9
      modules/common/Sanhe.Abp.Features.LimitValidation.Redis.Client/Sanhe/Abp/Features/LimitValidation/Redis/Client/AbpFeaturesValidationRedisClientModule.cs
  6. 33
      modules/common/Sanhe.Abp.Features.LimitValidation.Redis.Client/Sanhe/Abp/Features/LimitValidation/Redis/Client/RedisClientLimitFeatureNamingNormalizer.cs
  7. 3
      modules/common/Sanhe.Abp.Features.LimitValidation.Redis/FodyWeavers.xml
  8. 26
      modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe.Abp.Features.LimitValidation.Redis.csproj
  9. 24
      modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/AbpFeaturesValidationRedisModule.cs
  10. 22
      modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/AbpRedisRequiresLimitFeatureOptions.cs
  11. 6
      modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/IRedisLimitFeatureNamingNormalizer.cs
  12. 4
      modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/Lua/check.lua
  13. 6
      modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/Lua/process.lua
  14. 24
      modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/RedisLimitFeatureNamingNormalizer.cs
  15. 135
      modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/RedisRequiresLimitFeatureChecker.cs
  16. 15
      modules/common/Sanhe.Abp.Features.LimitValidation.Redis/System/BytesExtensions.cs
  17. 3
      modules/common/Sanhe.Abp.Features.LimitValidation/FodyWeavers.xml
  18. 85
      modules/common/Sanhe.Abp.Features.LimitValidation/README.md
  19. 24
      modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe.Abp.Features.LimitValidation.csproj
  20. 33
      modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/AbpFeatureLimitException.cs
  21. 34
      modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/AbpFeaturesLimitValidationModule.cs
  22. 48
      modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/AbpFeaturesLimitValidationOptions.cs
  23. 99
      modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/FeaturesLimitValidationInterceptor.cs
  24. 37
      modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/FeaturesLimitValidationInterceptorRegistrar.cs
  25. 11
      modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/IRequiresLimitFeatureChecker.cs
  26. 32
      modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/LimitPolicy.cs
  27. 8
      modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/Localization/FeaturesLimitValidationResource.cs
  28. 6
      modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/Localization/Resources/en.json
  29. 6
      modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/Localization/Resources/zh-Hans.json
  30. 18
      modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/NullRequiresLimitFeatureChecker.cs
  31. 53
      modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/RequiresLimitFeatureAttribute.cs
  32. 46
      modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/RequiresLimitFeatureContext.cs

2
Directory.Build.props

@ -1,5 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<VoloAbpVersion>5.1.4</VoloAbpVersion> <VoloAbpVersion>5.1.4</VoloAbpVersion>
<StackExchangeRedisVersion>2.0.593</StackExchangeRedisVersion>
<MicrosoftPackageVersion>6.0.*</MicrosoftPackageVersion>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

21
Sanhe.Abp.Framework.sln

@ -25,6 +25,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sanhe.Abp.ExceptionHandling
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sanhe.Abp.ExceptionHandling.Emailing", "modules\common\Sanhe.Abp.ExceptionHandling.Emailing\Sanhe.Abp.ExceptionHandling.Emailing.csproj", "{0692C2EA-7119-4065-8504-D7FDA70135A9}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sanhe.Abp.ExceptionHandling.Emailing", "modules\common\Sanhe.Abp.ExceptionHandling.Emailing\Sanhe.Abp.ExceptionHandling.Emailing.csproj", "{0692C2EA-7119-4065-8504-D7FDA70135A9}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sanhe.Abp.Features.LimitValidation", "modules\common\Sanhe.Abp.Features.LimitValidation\Sanhe.Abp.Features.LimitValidation.csproj", "{BE246DD7-2DDB-4064-9017-7E1DD2E67195}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sanhe.Abp.Features.LimitValidation.Redis", "modules\common\Sanhe.Abp.Features.LimitValidation.Redis\Sanhe.Abp.Features.LimitValidation.Redis.csproj", "{A2BD1C66-505E-48BB-A356-38D74AA3AE0F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sanhe.Abp.Features.LimitValidation.Redis.Client", "modules\common\Sanhe.Abp.Features.LimitValidation.Redis.Client\Sanhe.Abp.Features.LimitValidation.Redis.Client.csproj", "{97DDB479-946C-489D-9089-6EAEBFEE97C6}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -51,6 +57,18 @@ Global
{0692C2EA-7119-4065-8504-D7FDA70135A9}.Debug|Any CPU.Build.0 = Debug|Any CPU {0692C2EA-7119-4065-8504-D7FDA70135A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0692C2EA-7119-4065-8504-D7FDA70135A9}.Release|Any CPU.ActiveCfg = Release|Any CPU {0692C2EA-7119-4065-8504-D7FDA70135A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0692C2EA-7119-4065-8504-D7FDA70135A9}.Release|Any CPU.Build.0 = Release|Any CPU {0692C2EA-7119-4065-8504-D7FDA70135A9}.Release|Any CPU.Build.0 = Release|Any CPU
{BE246DD7-2DDB-4064-9017-7E1DD2E67195}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BE246DD7-2DDB-4064-9017-7E1DD2E67195}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BE246DD7-2DDB-4064-9017-7E1DD2E67195}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BE246DD7-2DDB-4064-9017-7E1DD2E67195}.Release|Any CPU.Build.0 = Release|Any CPU
{A2BD1C66-505E-48BB-A356-38D74AA3AE0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A2BD1C66-505E-48BB-A356-38D74AA3AE0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A2BD1C66-505E-48BB-A356-38D74AA3AE0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A2BD1C66-505E-48BB-A356-38D74AA3AE0F}.Release|Any CPU.Build.0 = Release|Any CPU
{97DDB479-946C-489D-9089-6EAEBFEE97C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{97DDB479-946C-489D-9089-6EAEBFEE97C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{97DDB479-946C-489D-9089-6EAEBFEE97C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{97DDB479-946C-489D-9089-6EAEBFEE97C6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -65,6 +83,9 @@ Global
{64178F61-A488-4182-A409-C32AE51E33A1} = {928FDD8C-1EE8-455E-952F-11039B52FE03} {64178F61-A488-4182-A409-C32AE51E33A1} = {928FDD8C-1EE8-455E-952F-11039B52FE03}
{FBEB7703-CF8A-4E5B-B1C7-ED9DC1ABC7BD} = {2A768109-31B7-4C52-928C-3023DAB9F254} {FBEB7703-CF8A-4E5B-B1C7-ED9DC1ABC7BD} = {2A768109-31B7-4C52-928C-3023DAB9F254}
{0692C2EA-7119-4065-8504-D7FDA70135A9} = {2A768109-31B7-4C52-928C-3023DAB9F254} {0692C2EA-7119-4065-8504-D7FDA70135A9} = {2A768109-31B7-4C52-928C-3023DAB9F254}
{BE246DD7-2DDB-4064-9017-7E1DD2E67195} = {2A768109-31B7-4C52-928C-3023DAB9F254}
{A2BD1C66-505E-48BB-A356-38D74AA3AE0F} = {2A768109-31B7-4C52-928C-3023DAB9F254}
{97DDB479-946C-489D-9089-6EAEBFEE97C6} = {2A768109-31B7-4C52-928C-3023DAB9F254}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {AB69BFDE-9DDB-4D16-8CB8-72472C0319CD} SolutionGuid = {AB69BFDE-9DDB-4D16-8CB8-72472C0319CD}

3
modules/common/Sanhe.Abp.Features.LimitValidation.Redis.Client/FodyWeavers.xml

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

15
modules/common/Sanhe.Abp.Features.LimitValidation.Redis.Client/Sanhe.Abp.Features.LimitValidation.Redis.Client.csproj

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\configureawait.props" />
<Import Project="..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Sanhe.Abp.Features.LimitValidation.Redis\Sanhe.Abp.Features.LimitValidation.Redis.csproj" />
</ItemGroup>
</Project>

9
modules/common/Sanhe.Abp.Features.LimitValidation.Redis.Client/Sanhe/Abp/Features/LimitValidation/Redis/Client/AbpFeaturesValidationRedisClientModule.cs

@ -0,0 +1,9 @@
using Volo.Abp.Modularity;
namespace Sanhe.Abp.Features.LimitValidation.Redis.Client;
[DependsOn(typeof(AbpFeaturesValidationRedisModule))]
public class AbpFeaturesValidationRedisClientModule : AbpModule
{
}

33
modules/common/Sanhe.Abp.Features.LimitValidation.Redis.Client/Sanhe/Abp/Features/LimitValidation/Redis/Client/RedisClientLimitFeatureNamingNormalizer.cs

@ -0,0 +1,33 @@
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Clients;
using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy;
namespace Sanhe.Abp.Features.LimitValidation.Redis.Client;
[Dependency(ServiceLifetime.Singleton, ReplaceServices = true)]
[ExposeServices(
typeof(IRedisLimitFeatureNamingNormalizer),
typeof(RedisLimitFeatureNamingNormalizer))]
public class RedisClientLimitFeatureNamingNormalizer : RedisLimitFeatureNamingNormalizer
{
protected ICurrentClient CurrentClient { get; }
public RedisClientLimitFeatureNamingNormalizer(
ICurrentClient currentClient,
ICurrentTenant currentTenant) : base(currentTenant)
{
CurrentClient = currentClient;
}
public override string NormalizeFeatureName(string instance, RequiresLimitFeatureContext context)
{
if (CurrentClient.IsAuthenticated)
{
return CurrentTenant.IsAvailable
? $"{instance}t:RequiresLimitFeature;t:{CurrentTenant.Id};c:{CurrentClient.Id};f:{context.LimitFeature}"
: $"{instance}tc:RequiresLimitFeature;c:{CurrentClient.Id};f:{context.LimitFeature}";
}
return base.NormalizeFeatureName(instance, context);
}
}

3
modules/common/Sanhe.Abp.Features.LimitValidation.Redis/FodyWeavers.xml

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

26
modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe.Abp.Features.LimitValidation.Redis.csproj

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\configureawait.props" />
<Import Project="..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftPackageVersion)" />
<PackageReference Include="StackExchange.Redis" Version="$(StackExchangeRedisVersion)" />
<PackageReference Include="Volo.Abp.Core" Version="$(VoloAbpVersion)" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Sanhe\Abp\Features\LimitValidation\Redis\Lua\*.lua" />
<Content Remove="Sanhe\Abp\Features\LimitValidation\Redis\Lua\*.lua" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Sanhe.Abp.Features.LimitValidation\Sanhe.Abp.Features.LimitValidation.csproj" />
</ItemGroup>
</Project>

24
modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/AbpFeaturesValidationRedisModule.cs

@ -0,0 +1,24 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Volo.Abp.Modularity;
using Volo.Abp.VirtualFileSystem;
namespace Sanhe.Abp.Features.LimitValidation.Redis;
[DependsOn(
typeof(AbpFeaturesLimitValidationModule))]
public class AbpFeaturesValidationRedisModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
Configure<AbpRedisRequiresLimitFeatureOptions>(configuration.GetSection("Features:Validation:Redis"));
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<AbpFeaturesValidationRedisModule>();
});
context.Services.Replace(ServiceDescriptor.Singleton<IRequiresLimitFeatureChecker, RedisRequiresLimitFeatureChecker>());
}
}

22
modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/AbpRedisRequiresLimitFeatureOptions.cs

@ -0,0 +1,22 @@
using Microsoft.Extensions.Options;
using StackExchange.Redis;
namespace Sanhe.Abp.Features.LimitValidation.Redis;
public class AbpRedisRequiresLimitFeatureOptions : IOptions<AbpRedisRequiresLimitFeatureOptions>
{
/// <summary>
/// Redis连接字符串
/// </summary>
public string Configuration { get; set; }
/// <summary>
/// 前缀
/// </summary>
public string InstanceName { get; set; }
/// <summary>
/// Redis连接配置
/// </summary>
public ConfigurationOptions ConfigurationOptions { get; set; }
AbpRedisRequiresLimitFeatureOptions IOptions<AbpRedisRequiresLimitFeatureOptions>.Value => this;
}

6
modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/IRedisLimitFeatureNamingNormalizer.cs

@ -0,0 +1,6 @@
namespace Sanhe.Abp.Features.LimitValidation.Redis;
public interface IRedisLimitFeatureNamingNormalizer
{
string NormalizeFeatureName(string instance, RequiresLimitFeatureContext context);
}

4
modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/Lua/check.lua

@ -0,0 +1,4 @@
if (redis.call('EXISTS', KEYS[1]) == 0) then
return 0
end
return tonumber(redis.call('GET', KEYS[1]))

6
modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/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],1)
end
return tonumber(redis.call('GET',KEYS[1]))

24
modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/RedisLimitFeatureNamingNormalizer.cs

@ -0,0 +1,24 @@
using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy;
namespace Sanhe.Abp.Features.LimitValidation.Redis;
public class RedisLimitFeatureNamingNormalizer : IRedisLimitFeatureNamingNormalizer, ISingletonDependency
{
protected ICurrentTenant CurrentTenant { get; }
public RedisLimitFeatureNamingNormalizer(ICurrentTenant currentTenant)
{
CurrentTenant = currentTenant;
}
public virtual string NormalizeFeatureName(string instance, RequiresLimitFeatureContext context)
{
if (CurrentTenant.IsAvailable)
{
return $"{instance}t:RequiresLimitFeature;t:{CurrentTenant.Id};f:{context.LimitFeature}";
}
return $"{instance}c:RequiresLimitFeature;f:{context.LimitFeature}";
}
}

135
modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/RedisRequiresLimitFeatureChecker.cs

@ -0,0 +1,135 @@
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.DependencyInjection;
using Volo.Abp.VirtualFileSystem;
namespace Sanhe.Abp.Features.LimitValidation.Redis;
[DisableConventionalRegistration]
public class RedisRequiresLimitFeatureChecker : IRequiresLimitFeatureChecker
{
private const string CHECK_LUA_SCRIPT = "/Sanhe/Abp/Features/LimitValidation/Redis/Lua/check.lua";
private const string PROCESS_LUA_SCRIPT = "/Sanhe/Abp/Features/LimitValidation/Redis/Lua/process.lua";
public ILogger<RedisRequiresLimitFeatureChecker> Logger { protected get; set; }
private volatile ConnectionMultiplexer _connection;
private volatile ConfigurationOptions _redisConfig;
private IDatabaseAsync _redis;
private IServer _server;
private readonly IVirtualFileProvider _virtualFileProvider;
private readonly IRedisLimitFeatureNamingNormalizer _featureNamingNormalizer;
private readonly AbpRedisRequiresLimitFeatureOptions _options;
private readonly string _instance;
private readonly SemaphoreSlim _connectionLock = new(initialCount: 1, maxCount: 1);
public RedisRequiresLimitFeatureChecker(
IVirtualFileProvider virtualFileProvider,
IRedisLimitFeatureNamingNormalizer featureNamingNormalizer,
IOptions<AbpRedisRequiresLimitFeatureOptions> optionsAccessor)
{
if (optionsAccessor == null)
{
throw new ArgumentNullException(nameof(optionsAccessor));
}
_options = optionsAccessor.Value;
_virtualFileProvider = virtualFileProvider;
_featureNamingNormalizer = featureNamingNormalizer;
_instance = _options.InstanceName ?? string.Empty;
Logger = NullLogger<RedisRequiresLimitFeatureChecker>.Instance;
}
public virtual async Task<bool> CheckAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default)
{
await ConnectAsync(cancellation);
var result = await EvaluateAsync(CHECK_LUA_SCRIPT, context, cancellation);
return result + 1 <= context.Limit;
}
public virtual async Task ProcessAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default)
{
await ConnectAsync(cancellation);
await EvaluateAsync(PROCESS_LUA_SCRIPT, context, cancellation);
}
private async Task<int> 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($"Script evaluate error: {result}");
}
return (int)result;
}
private string NormalizeKey(RequiresLimitFeatureContext context)
{
return _featureNamingNormalizer.NormalizeFeatureName(_instance, context);
}
private async Task ConnectAsync(CancellationToken token = default)
{
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);
// fix: 无需关注redis连接事件
_redis = _connection.GetDatabase();
_server = _connection.GetServer(_redisConfig.EndPoints[0]);
}
}
finally
{
_connectionLock.Release();
}
}
}

15
modules/common/Sanhe.Abp.Features.LimitValidation.Redis/System/BytesExtensions.cs

@ -0,0 +1,15 @@
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;
}
}
}

3
modules/common/Sanhe.Abp.Features.LimitValidation/FodyWeavers.xml

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

85
modules/common/Sanhe.Abp.Features.LimitValidation/README.md

@ -0,0 +1,85 @@
# Sanhe.Abp.Features.LimitValidation
功能上限验证组件
检查定义的功能调用次数,来限制特定的实体(租户、用户、客户端等)对于应用程序的调用
预先设定了如下几个策略
LimitPolicy.Minute 按分钟计算流量
LimitPolicy.Hours 按小时计算流量
LimitPolicy.Days 按天数计算流量
LimitPolicy.Weeks 按周数计算流量
LimitPolicy.Month 按月数计算流量
LimitPolicy.Years 按年数计算流量
## 配置使用
```csharp
[DependsOn(typeof(AbpFeaturesLimitValidationModule))]
public class YouProjectModule : AbpModule
{
// other
}
public static class FakeFeatureNames
{
public const string GroupName = "FakeFeature.Tests";
// 类型限制调用次数功能名称
public const string ClassLimitFeature = GroupName + ".LimitFeature";
// 方法限制调用次数功能名称
public const string MethodLimitFeature = GroupName + ".MethodLimitFeature";
// 限制调用间隔功能名称
public const string IntervalFeature = GroupName + ".IntervalFeature";
}
// 流量限制依赖自定义功能
public class FakeFeatureDefinitionProvider : FeatureDefinitionProvider
{
public override void Define(IFeatureDefinitionContext context)
{
var featureGroup = context.AddGroup(FakeFeatureNames.GroupName);
featureGroup.AddFeature(
name: FakeFeatureNames.ClassLimitFeature,
defaultValue: 1000.ToString(), // 周期内最大允许调用1000次
valueType: new ToggleStringValueType(new NumericValueValidator(1, 1000)));
featureGroup.AddFeature(
name: FakeFeatureNames.MethodLimitFeature,
defaultValue: 100.ToString(), // 周期内最大允许调用100次
valueType: new ToggleStringValueType(new NumericValueValidator(1, 1000)));
featureGroup.AddFeature(
name: FakeFeatureNames.IntervalFeature,
defaultValue: 1.ToString(), // 限制周期
valueType: new ToggleStringValueType(new NumericValueValidator(1, 1000)));
}
}
// 按照预设的参数,类型在一天钟内仅允许调用1000次
[RequiresLimitFeature(FakeFeatureNames.ClassLimitFeature, FakeFeatureNames.IntervalFeature, LimitPolicy.Days)]
public class FakeLimitClass
{
// 按照预设的参数,方法在一分钟内仅允许调用100次
[RequiresLimitFeature(FakeFeatureNames.MethodLimitFeature, FakeFeatureNames.IntervalFeature, LimitPolicy.Minute)]
public void LimitMethod()
{
// other...
}
}
```
如果需要自行处理功能限制策略时长,请覆盖对应策略的默认策略,返回的时钟刻度单位始终是秒
```csharp
[DependsOn(typeof(AbpFeaturesLimitValidationModule))]
public class YouProjectModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpFeaturesLimitValidationOptions>(options =>
{
options.MapEffectPolicy(LimitPolicy.Minute, (time) => return 60;); // 表示不管多少分钟(time),都只会限制60秒
});
}
}
```

24
modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe.Abp.Features.LimitValidation.csproj

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\configureawait.props" />
<Import Project="..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<None Remove="Sanhe\Abp\Features\LimitValidation\Localization\Resources\en.json" />
<None Remove="Sanhe\Abp\Features\LimitValidation\Localization\Resources\zh-Hans.json" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Sanhe\Abp\Features\LimitValidation\Localization\Resources\en.json" />
<EmbeddedResource Include="Sanhe\Abp\Features\LimitValidation\Localization\Resources\zh-Hans.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Volo.Abp.Features" Version="$(VoloAbpVersion)" />
</ItemGroup>
</Project>

33
modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/AbpFeatureLimitException.cs

@ -0,0 +1,33 @@
using Microsoft.Extensions.Localization;
using Sanhe.Abp.Features.LimitValidation.Localization;
using Volo.Abp;
using Volo.Abp.ExceptionHandling;
using Volo.Abp.Localization;
namespace Sanhe.Abp.Features.LimitValidation;
public class AbpFeatureLimitException : AbpException, ILocalizeErrorMessage, IBusinessException
{
/// <summary>
/// 功能名称名称
/// </summary>
public string Feature { get; }
/// <summary>
/// 上限
/// </summary>
public int Limit { get; }
public AbpFeatureLimitException(string feature, int limit)
: base($"Features {feature} has exceeded the maximum number of calls {limit}, please apply for the appropriate permission")
{
Feature = feature;
Limit = limit;
}
public string LocalizeMessage(LocalizationContext context)
{
var localizer = context.LocalizerFactory.Create<FeaturesLimitValidationResource>();
return localizer["FeaturesLimitException", Limit];
}
}

34
modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/AbpFeaturesLimitValidationModule.cs

@ -0,0 +1,34 @@
using Microsoft.Extensions.DependencyInjection;
using Sanhe.Abp.Features.LimitValidation.Localization;
using Volo.Abp.Features;
using Volo.Abp.Localization;
using Volo.Abp.Modularity;
using Volo.Abp.VirtualFileSystem;
namespace Sanhe.Abp.Features.LimitValidation;
[DependsOn(typeof(AbpFeaturesModule))]
public class AbpFeaturesLimitValidationModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.OnRegistred(FeaturesLimitValidationInterceptorRegistrar.RegisterIfNeeded);
Configure<AbpFeaturesLimitValidationOptions>(options =>
{
options.MapDefaultEffectPolicys();
});
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<AbpFeaturesLimitValidationModule>();
});
Configure<AbpLocalizationOptions>(options =>
{
options.Resources
.Add<FeaturesLimitValidationResource>("en")
.AddVirtualJson("/Sanhe/Abp/Features/LimitValidation/Localization/Resources");
});
}
}

48
modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/AbpFeaturesLimitValidationOptions.cs

@ -0,0 +1,48 @@
using JetBrains.Annotations;
using System;
using System.Collections.Generic;
using Volo.Abp;
namespace Sanhe.Abp.Features.LimitValidation;
public class AbpFeaturesLimitValidationOptions
{
public IDictionary<LimitPolicy, Func<int, long>> EffectPolicys { get; }
public AbpFeaturesLimitValidationOptions()
{
EffectPolicys = new Dictionary<LimitPolicy, Func<int, long>>();
}
/// <summary>
/// 变更功能限制策略时长计算方法
/// </summary>
/// <param name="policy">限制策略</param>
/// <param name="func">自定义的计算方法</param>
/// <remarks>
/// 返回值一定要是秒钟刻度
/// </remarks>
public void MapEffectPolicy(LimitPolicy policy, [NotNull] Func<int, long> func)
{
Check.NotNull(func, nameof(func));
if (EffectPolicys.ContainsKey(policy))
{
EffectPolicys[policy] = func;
}
else
{
EffectPolicys.Add(policy, func);
}
}
internal void MapDefaultEffectPolicys()
{
MapEffectPolicy(LimitPolicy.Minute, (time) => { return (long)(DateTimeOffset.UtcNow.AddMinutes(time) - DateTimeOffset.UtcNow).TotalSeconds; });
MapEffectPolicy(LimitPolicy.Hours, (time) => { return (long)(DateTimeOffset.UtcNow.AddHours(time) - DateTimeOffset.UtcNow).TotalSeconds; });
MapEffectPolicy(LimitPolicy.Days, (time) => { return (long)(DateTimeOffset.UtcNow.AddDays(time) - DateTimeOffset.UtcNow).TotalSeconds; });
MapEffectPolicy(LimitPolicy.Weeks, (time) => { return (long)(DateTimeOffset.UtcNow.AddDays(time * 7) - DateTimeOffset.UtcNow).TotalSeconds; });
MapEffectPolicy(LimitPolicy.Month, (time) => { return (long)(DateTimeOffset.UtcNow.AddMonths(time) - DateTimeOffset.UtcNow).TotalSeconds; });
MapEffectPolicy(LimitPolicy.Years, (time) => { return (long)(DateTimeOffset.UtcNow.AddYears(time) - DateTimeOffset.UtcNow).TotalSeconds; });
}
}

99
modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/FeaturesLimitValidationInterceptor.cs

@ -0,0 +1,99 @@
using Microsoft.Extensions.Options;
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 Sanhe.Abp.Features.LimitValidation;
public class FeaturesLimitValidationInterceptor : AbpInterceptor, ITransientDependency
{
private readonly IFeatureChecker _featureChecker;
private readonly AbpFeaturesLimitValidationOptions _options;
private readonly IRequiresLimitFeatureChecker _limitFeatureChecker;
private readonly IFeatureDefinitionManager _featureDefinitionManager;
public FeaturesLimitValidationInterceptor(
IFeatureChecker featureChecker,
IRequiresLimitFeatureChecker limitFeatureChecker,
IFeatureDefinitionManager featureDefinitionManager,
IOptions<AbpFeaturesLimitValidationOptions> options)
{
_options = options.Value;
_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.LimitFeature, limitFeature.DefaultLimit);
// 获取功能限制时长
var interval = await _featureChecker.GetAsync(limitFeature.IntervalFeature, limitFeature.DefaultInterval);
// 必要的上下文参数
var limitFeatureContext = new RequiresLimitFeatureContext(limitFeature.LimitFeature, _options, limitFeature.Policy, interval, limit);
// 检查次数限制
await PreCheckFeatureAsync(limitFeatureContext);
// 执行代理方法
await invocation.ProceedAsync();
// 调用次数递增
// TODO: 使用Redis结合Lua脚本?
await PostCheckFeatureAsync(limitFeatureContext);
}
protected virtual async Task PreCheckFeatureAsync(RequiresLimitFeatureContext context)
{
var allowed = await _limitFeatureChecker.CheckAsync(context);
if (!allowed)
{
throw new AbpFeatureLimitException(context.LimitFeature, context.Limit);
}
}
protected virtual async Task PostCheckFeatureAsync(RequiresLimitFeatureContext context)
{
await _limitFeatureChecker.ProcessAsync(context);
}
protected virtual RequiresLimitFeatureAttribute GetRequiresLimitFeature(MethodInfo methodInfo)
{
var limitFeature = methodInfo.GetCustomAttribute<RequiresLimitFeatureAttribute>(false);
if (limitFeature != null)
{
// 限制次数定义的不是范围参数,则不参与限制功能
var featureLimitDefinition = _featureDefinitionManager.GetOrNull(limitFeature.LimitFeature);
if (featureLimitDefinition == null ||
!typeof(NumericValueValidator).IsAssignableFrom(featureLimitDefinition.ValueType.Validator.GetType()))
{
return null;
}
// 时长刻度定义的不是范围参数,则不参与限制功能
var featureIntervalDefinition = _featureDefinitionManager.GetOrNull(limitFeature.IntervalFeature);
if (featureIntervalDefinition == null ||
!typeof(NumericValueValidator).IsAssignableFrom(featureIntervalDefinition.ValueType.Validator.GetType()))
{
return null;
}
}
return limitFeature;
}
}

37
modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/FeaturesLimitValidationInterceptorRegistrar.cs

@ -0,0 +1,37 @@
using System;
using System.Linq;
using System.Reflection;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy;
namespace Sanhe.Abp.Features.LimitValidation;
public static class FeaturesLimitValidationInterceptorRegistrar
{
public static void RegisterIfNeeded(IOnServiceRegistredContext context)
{
if (ShouldIntercept(context.ImplementationType))
{
context.Interceptors.TryAdd<FeaturesLimitValidationInterceptor>();
}
}
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);
}
}

11
modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/IRequiresLimitFeatureChecker.cs

@ -0,0 +1,11 @@
using System.Threading;
using System.Threading.Tasks;
namespace Sanhe.Abp.Features.LimitValidation;
public interface IRequiresLimitFeatureChecker
{
Task<bool> CheckAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default);
Task ProcessAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default);
}

32
modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/LimitPolicy.cs

@ -0,0 +1,32 @@
namespace Sanhe.Abp.Features.LimitValidation;
/// <summary>
/// 限制策略。
/// </summary>
public enum LimitPolicy : byte
{
/// <summary>
/// 按分钟限制
/// </summary>
Minute = 0,
/// <summary>
/// 按小时限制
/// </summary>
Hours = 10,
/// <summary>
/// 按天限制
/// </summary>
Days = 20,
/// <summary>
/// 按周限制
/// </summary>
Weeks = 30,
/// <summary>
/// 按月限制
/// </summary>
Month = 40,
/// <summary>
/// 按年限制
/// </summary>
Years = 50
}

8
modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/Localization/FeaturesLimitValidationResource.cs

@ -0,0 +1,8 @@
using Volo.Abp.Localization;
namespace Sanhe.Abp.Features.LimitValidation.Localization;
[LocalizationResourceName("AbpFeaturesLimitValidation")]
public class FeaturesLimitValidationResource
{
}

6
modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/Localization/Resources/en.json

@ -0,0 +1,6 @@
{
"culture": "en",
"texts": {
"FeaturesLimitException": "Service has exceeded the maximum number of calls {0}. Please apply for the appropriate permissions"
}
}

6
modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/Localization/Resources/zh-Hans.json

@ -0,0 +1,6 @@
{
"culture": "zh-Hans",
"texts": {
"FeaturesLimitException": "服务已超过最大调用次数 {0},请申请适当的权限"
}
}

18
modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/NullRequiresLimitFeatureChecker.cs

@ -0,0 +1,18 @@
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
namespace Sanhe.Abp.Features.LimitValidation;
public class NullRequiresLimitFeatureChecker : IRequiresLimitFeatureChecker, ISingletonDependency
{
public Task<bool> CheckAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default)
{
return Task.FromResult(true);
}
public Task ProcessAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default)
{
return Task.CompletedTask;
}
}

53
modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/RequiresLimitFeatureAttribute.cs

@ -0,0 +1,53 @@
using JetBrains.Annotations;
using System;
using Volo.Abp;
namespace Sanhe.Abp.Features.LimitValidation;
/// <summary>
/// 单个功能的调用量限制
/// </summary>
/// <remarks>
/// 需要对于限制时长和限制上限功能区分,以便于更细粒度的限制
/// </remarks>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class RequiresLimitFeatureAttribute : Attribute
{
/// <summary>
/// 功能限制策略
/// </summary>
public LimitPolicy Policy { get; }
/// <summary>
/// 默认限制时长
/// </summary>
public int DefaultLimit { get; }
/// <summary>
/// 限制上限名称
/// </summary>
public string LimitFeature { get; }
/// <summary>
/// 默认限制时长
/// </summary>
public int DefaultInterval { get; }
/// <summary>
/// 限制时长名称
/// </summary>
public string IntervalFeature { get; }
public RequiresLimitFeatureAttribute(
[NotNull] string limitFeature,
[NotNull] string intervalFeature,
LimitPolicy policy = LimitPolicy.Month,
int defaultLimit = 1,
int defaultInterval = 1)
{
Check.NotNullOrWhiteSpace(limitFeature, nameof(limitFeature));
Check.NotNullOrWhiteSpace(intervalFeature, nameof(intervalFeature));
Policy = policy;
LimitFeature = limitFeature;
DefaultLimit = defaultLimit;
IntervalFeature = intervalFeature;
DefaultInterval = defaultInterval;
}
}

46
modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/RequiresLimitFeatureContext.cs

@ -0,0 +1,46 @@
namespace Sanhe.Abp.Features.LimitValidation;
public class RequiresLimitFeatureContext
{
/// <summary>
/// 功能限制策略
/// </summary>
public LimitPolicy Policy { get; }
/// <summary>
/// 限制时长
/// </summary>
public int Interval { get; }
/// <summary>
/// 功能限制次数
/// </summary>
public int Limit { get; }
/// <summary>
/// 功能限制次数名称
/// </summary>
public string LimitFeature { get; }
public AbpFeaturesLimitValidationOptions Options { get; }
public RequiresLimitFeatureContext(
string limitFeature,
AbpFeaturesLimitValidationOptions options,
LimitPolicy policy = LimitPolicy.Month,
int interval = 1,
int limit = 1)
{
Limit = limit;
Policy = policy;
Interval = interval;
LimitFeature = limitFeature;
Options = options;
}
/// <summary>
/// 获取生效时间跨度,单位:s
/// </summary>
/// <returns></returns>
public long GetEffectTicks()
{
return Options.EffectPolicys[Policy](Interval);
}
}
Loading…
Cancel
Save