committed by
GitHub
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