36 changed files with 1033 additions and 11 deletions
@ -0,0 +1,25 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netstandard2.0</TargetFramework> |
||||
|
<RootNamespace /> |
||||
|
<LangVersion>8.0</LangVersion> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Microsoft.Extensions.Options" Version="3.1.7" /> |
||||
|
<PackageReference Include="Polly" Version="7.2.1" /> |
||||
|
<PackageReference Include="StackExchange.Redis" Version="2.0.593" /> |
||||
|
<PackageReference Include="Volo.Abp.Core" Version="3.1.0" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<EmbeddedResource Include="LINGYUN\Abp\Features\Validation\Redis\Lua\*.lua" /> |
||||
|
<Content Remove="LINGYUN\Abp\Features\Validation\Redis\Lua\*.lua" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.Features\LINGYUN.Abp.Features.Validation.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -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<AbpRedisRequiresLimitFeatureOptions>(configuration.GetSection("Features:Validation:Redis")); |
||||
|
|
||||
|
Configure<AbpVirtualFileSystemOptions>(options => |
||||
|
{ |
||||
|
options.FileSets.AddEmbedded<AbpFeaturesValidationRedisModule>(); |
||||
|
}); |
||||
|
|
||||
|
context.Services.Replace(ServiceDescriptor.Singleton<IRequiresLimitFeatureChecker, RedisRequiresLimitFeatureChecker>()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
using Microsoft.Extensions.Options; |
||||
|
using StackExchange.Redis; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Features.Validation.Redis |
||||
|
{ |
||||
|
public class AbpRedisRequiresLimitFeatureOptions : IOptions<AbpRedisRequiresLimitFeatureOptions> |
||||
|
{ |
||||
|
public string Configuration { get; set; } |
||||
|
public string InstanceName { get; set; } |
||||
|
public ConfigurationOptions ConfigurationOptions { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 失败重试次数
|
||||
|
/// default: 3
|
||||
|
/// </summary>
|
||||
|
public int FailedRetryCount { get; set; } = 3; |
||||
|
/// <summary>
|
||||
|
/// 失败重试间隔 ms
|
||||
|
/// default: 1000
|
||||
|
/// </summary>
|
||||
|
public int FailedRetryInterval { get; set; } = 1000; |
||||
|
AbpRedisRequiresLimitFeatureOptions IOptions<AbpRedisRequiresLimitFeatureOptions>.Value |
||||
|
{ |
||||
|
get { return this; } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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])) |
||||
@ -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])) |
||||
@ -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<RedisRequiresLimitFeatureChecker> 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<AbpRedisRequiresLimitFeatureOptions> optionsAccessor) |
||||
|
{ |
||||
|
if (optionsAccessor == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(optionsAccessor)); |
||||
|
} |
||||
|
|
||||
|
_options = optionsAccessor.Value; |
||||
|
_currentTenant = currentTenant; |
||||
|
_virtualFileProvider = virtualFileProvider; |
||||
|
|
||||
|
_instance = _options.InstanceName ?? string.Empty; |
||||
|
|
||||
|
Logger = NullLogger<RedisRequiresLimitFeatureChecker>.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<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($"脚本执行错误:{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"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netstandard2.0</TargetFramework> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Volo.Abp.Features" Version="3.1.0" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<RequiresLimitFeatureAttribute>(false); |
||||
|
if (limitFeature != null) |
||||
|
{ |
||||
|
var featureDefinition = _featureDefinitionManager.GetOrNull(limitFeature.Feature); |
||||
|
if (featureDefinition != null && |
||||
|
typeof(NumericValueValidator).IsAssignableFrom(featureDefinition.ValueType.Validator.GetType())) |
||||
|
{ |
||||
|
return limitFeature; |
||||
|
} |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<FeaturesValidationInterceptor>(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
namespace LINGYUN.Abp.Features.Validation |
||||
|
{ |
||||
|
public enum LimitPolicy |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 按小时限制
|
||||
|
/// </summary>
|
||||
|
Hours = 0, |
||||
|
/// <summary>
|
||||
|
/// 按天限制
|
||||
|
/// </summary>
|
||||
|
Days = 1, |
||||
|
/// <summary>
|
||||
|
/// 按周限制
|
||||
|
/// </summary>
|
||||
|
Weeks = 2, |
||||
|
/// <summary>
|
||||
|
/// 按月限制
|
||||
|
/// </summary>
|
||||
|
Month = 3, |
||||
|
/// <summary>
|
||||
|
/// 按年限制
|
||||
|
/// </summary>
|
||||
|
Years = 4 |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Features.Validation |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 单个功能的调用量限制
|
||||
|
/// </summary>
|
||||
|
[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 Feature { get; } |
||||
|
|
||||
|
public RequiresLimitFeatureAttribute( |
||||
|
string feature, |
||||
|
LimitPolicy policy = LimitPolicy.Month, |
||||
|
int defaultLimit = 1) |
||||
|
{ |
||||
|
DefaultLimit = defaultLimit; |
||||
|
Policy = policy; |
||||
|
Feature = feature; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,56 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Features.Validation |
||||
|
{ |
||||
|
public class RequiresLimitFeatureContext |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 功能限制策略
|
||||
|
/// </summary>
|
||||
|
public LimitPolicy Policy { get; } |
||||
|
/// <summary>
|
||||
|
/// 功能限制时长
|
||||
|
/// </summary>
|
||||
|
public int Limit { get; } |
||||
|
/// <summary>
|
||||
|
/// 功能名称
|
||||
|
/// </summary>
|
||||
|
public string Feature { get; } |
||||
|
|
||||
|
private Lazy<IDictionary<LimitPolicy, Func<int, long>>> effectPolicysLazy; |
||||
|
private IDictionary<LimitPolicy, Func<int, long>> effectPolicys => effectPolicysLazy.Value; |
||||
|
public RequiresLimitFeatureContext( |
||||
|
string feature, |
||||
|
LimitPolicy policy = LimitPolicy.Month, |
||||
|
int limit = 1) |
||||
|
{ |
||||
|
Limit = limit; |
||||
|
Policy = policy; |
||||
|
Feature = feature; |
||||
|
|
||||
|
effectPolicysLazy = new Lazy<IDictionary<LimitPolicy, Func<int, long>>>(() => CreateFeatureLimitPolicy()); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 获取生效时间戳
|
||||
|
/// </summary>
|
||||
|
/// <returns></returns>
|
||||
|
public long GetEffectTicks() |
||||
|
{ |
||||
|
return effectPolicys[Policy](Limit); |
||||
|
} |
||||
|
|
||||
|
protected IDictionary<LimitPolicy, Func<int, long>> CreateFeatureLimitPolicy() |
||||
|
{ |
||||
|
return new Dictionary<LimitPolicy, Func<int, long>>() |
||||
|
{ |
||||
|
{ 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; } } |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>net5.0</TargetFramework> |
||||
|
<RootNamespace /> |
||||
|
|
||||
|
<IsPackable>false</IsPackable> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" /> |
||||
|
<PackageReference Include="xunit" Version="2.4.1" /> |
||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> |
||||
|
<PackageReference Include="coverlet.collector" Version="1.2.0" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\..\modules\common\LINGYUN.Abp.Features.Validation.Redis\LINGYUN.Abp.Features.Validation.Redis.csproj" /> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.Features.Validation.Tests\LINGYUN.Abp.Features.Validation.Tests.csproj" /> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.TestBase\LINGYUN.Abp.TestsBase.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,9 @@ |
|||||
|
using LINGYUN.Abp.Tests; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Features.Validation.Redis |
||||
|
{ |
||||
|
public class AbpFeaturesValidationRedisTestBase : AbpTestsBase<AbpFeaturesValidationRedisTestModule> |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -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)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<IRequiresLimitFeatureChecker>(); |
||||
|
TestValidationFeatureClass = GetRequiredService<TestValidationFeatureClass>(); |
||||
|
} |
||||
|
|
||||
|
[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<AbpAuthorizationException>(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>net5.0</TargetFramework> |
||||
|
<RootNamespace /> |
||||
|
<IsPackable>false</IsPackable> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" /> |
||||
|
<PackageReference Include="xunit" Version="2.4.1" /> |
||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> |
||||
|
<PackageReference Include="coverlet.collector" Version="1.2.0" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\..\modules\common\LINGYUN.Abp.Features\LINGYUN.Abp.Features.Validation.csproj" /> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.TestBase\LINGYUN.Abp.TestsBase.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,8 @@ |
|||||
|
using LINGYUN.Abp.Tests; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Features.Validation |
||||
|
{ |
||||
|
public class AbpFeaturesValidationTestBase : AbpTestsBase<AbpFeaturesValidationTestModule> |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -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<FakeFeatureOptions>(options => |
||||
|
{ |
||||
|
options.Map(TestFeatureNames.TestFeature1, (feature) => |
||||
|
{ |
||||
|
return 2.ToString(); |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<string, int> limitFeatures; |
||||
|
|
||||
|
public FakeRequiresFeatureLimitChecker() |
||||
|
{ |
||||
|
limitFeatures = new Dictionary<string, int>(); |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<ICurrentTenant>(); |
||||
|
TestValidationFeatureClass = GetRequiredService<TestValidationFeatureClass>(); |
||||
|
} |
||||
|
|
||||
|
[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<AbpAuthorizationException>(async () => |
||||
|
{ |
||||
|
await TestValidationFeatureClass.Test1HoursAsync(); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static Guid? ParseNullableGuid(string tenantIdValue) |
||||
|
{ |
||||
|
return tenantIdValue == null ? (Guid?)null : new Guid(tenantIdValue); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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))); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
namespace LINGYUN.Abp.Features.Validation |
||||
|
{ |
||||
|
public class TestFeatureTenant |
||||
|
{ |
||||
|
public const string TenantId = "6355C04C-59D1-4133-944E-157179346BDC"; |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Volo.Abp.Features; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Tests.Features |
||||
|
{ |
||||
|
public class FakeFeatureOptions |
||||
|
{ |
||||
|
public IDictionary<string, Func<FeatureDefinition, string>> FeatureMaps { get; } |
||||
|
public FakeFeatureOptions() |
||||
|
{ |
||||
|
FeatureMaps = new Dictionary<string, Func<FeatureDefinition, string>>(); |
||||
|
} |
||||
|
|
||||
|
public void Map(string featureName, Func<FeatureDefinition, string> func) |
||||
|
{ |
||||
|
FeatureMaps.AddIfNotContains(new KeyValuePair<string, Func<FeatureDefinition, string>>(featureName, func)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<FakeFeatureOptions> options, |
||||
|
IFeatureDefinitionManager featureDefinitionManager) |
||||
|
{ |
||||
|
FakeFeatureOptions = options.Value; |
||||
|
FeatureDefinitionManager = featureDefinitionManager; |
||||
|
} |
||||
|
|
||||
|
public Task<string> GetOrNullAsync(string name, string providerName, string providerKey) |
||||
|
{ |
||||
|
var feature = FeatureDefinitionManager.Get(name); |
||||
|
|
||||
|
var featureFunc = FakeFeatureOptions.FeatureMaps[name]; |
||||
|
|
||||
|
return Task.FromResult(featureFunc(feature)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue