committed by
GitHub
45 changed files with 1127 additions and 6 deletions
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,26 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<Nullable>enable</Nullable> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<None Remove="LINGYUN\Abp\Idempotent\Localization\Resources\*.json" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<EmbeddedResource Include="LINGYUN\Abp\Idempotent\Localization\Resources\*.json" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.Ddd.Application.Contracts" Version="$(VoloAbpPackageVersion)" /> |
|||
<PackageReference Include="Volo.Abp.DistributedLocking.Abstractions" Version="$(VoloAbpPackageVersion)" /> |
|||
<PackageReference Include="Volo.Abp.Json.Abstractions" Version="$(VoloAbpPackageVersion)" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,36 @@ |
|||
using LINGYUN.Abp.Idempotent.Localization; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.Application; |
|||
using Volo.Abp.DistributedLocking; |
|||
using Volo.Abp.Json; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.VirtualFileSystem; |
|||
|
|||
namespace LINGYUN.Abp.Idempotent; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpDddApplicationContractsModule), |
|||
typeof(AbpDistributedLockingAbstractionsModule), |
|||
typeof(AbpJsonAbstractionsModule))] |
|||
public class AbpIdempotentModule : AbpModule |
|||
{ |
|||
public override void PreConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.OnRegistred(IdempotentInterceptorRegistrar.RegisterIfNeeded); |
|||
} |
|||
|
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpVirtualFileSystemOptions>(options => |
|||
{ |
|||
options.FileSets.AddEmbedded<AbpIdempotentModule>(); |
|||
}); |
|||
|
|||
Configure<AbpLocalizationOptions>(options => |
|||
{ |
|||
options.Resources.Add<IdempotentResource>() |
|||
.AddVirtualJson("/LINGYUN/Abp/Idempotent/Localization/Resources"); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
namespace LINGYUN.Abp.Idempotent; |
|||
public class AbpIdempotentOptions |
|||
{ |
|||
public bool IsEnabled { get; set; } |
|||
public int DefaultTimeout { get; set; } |
|||
public string IdempotentTokenName { get; set; } |
|||
public int HttpStatusCode { get; set; } |
|||
public AbpIdempotentOptions() |
|||
{ |
|||
DefaultTimeout = 30000; |
|||
IdempotentTokenName = "X-With-Idempotent-Token"; |
|||
// 默认使用 TooManyRequests(429)代码
|
|||
HttpStatusCode = 429; |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.Idempotent; |
|||
public interface IIdempotentChecker |
|||
{ |
|||
Task CheckAsync(IdempotentCheckContext context); |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
namespace LINGYUN.Abp.Idempotent; |
|||
public interface IIdempotentDeniedHandler |
|||
{ |
|||
void Denied(IdempotentDeniedContext context); |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
using Volo.Abp.DynamicProxy; |
|||
|
|||
namespace LINGYUN.Abp.Idempotent; |
|||
|
|||
public interface IIdempotentKeyNormalizer |
|||
{ |
|||
string NormalizeKey(IdempotentKeyNormalizerContext context); |
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
using System; |
|||
|
|||
namespace LINGYUN.Abp.Idempotent; |
|||
|
|||
[AttributeUsage(AttributeTargets.Method)] |
|||
public class IdempotentAttribute : Attribute |
|||
{ |
|||
/// <summary>
|
|||
/// 超时等待时间
|
|||
/// </summary>
|
|||
public int? Timeout { get; } |
|||
/// <summary>
|
|||
/// 用作建立唯一标识的参数名称列表
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// 通过查找参数列表中的值定义序列化唯一md5值
|
|||
/// </remarks>
|
|||
public string[]? KeyMap { get; } |
|||
/// <summary>
|
|||
/// 资源重定向路径模板
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// 定义一个可格式化的资源路径,当请求冲突时如果参数名称匹配成功则重定向目标路径
|
|||
/// <br />
|
|||
/// 例: /api/app/demo/{id}
|
|||
/// <br />
|
|||
/// 例: /api/app/demo/method?id={id}
|
|||
/// </remarks>
|
|||
public string? RedirectUrl { get; } |
|||
/// <summary>
|
|||
/// 自定义的幂等key
|
|||
/// </summary>
|
|||
public string? IdempotentKey { get; } |
|||
public IdempotentAttribute() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public IdempotentAttribute( |
|||
int? timeout = null, |
|||
string? redirectUrl = null, |
|||
string[]? keyMap = null) |
|||
{ |
|||
Timeout = timeout; |
|||
KeyMap = keyMap; |
|||
RedirectUrl = redirectUrl; |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Reflection; |
|||
|
|||
namespace LINGYUN.Abp.Idempotent; |
|||
public class IdempotentCheckContext |
|||
{ |
|||
public Type Target { get; } |
|||
public MethodInfo Method { get; } |
|||
public string IdempotentKey { get; } |
|||
public IReadOnlyDictionary<string, object?> ArgumentsDictionary { get; } |
|||
|
|||
public IdempotentCheckContext( |
|||
Type target, |
|||
MethodInfo method, |
|||
string idempotentKey, |
|||
IReadOnlyDictionary<string, object?> argumentsDictionary) |
|||
{ |
|||
Target = target; |
|||
Method = method; |
|||
IdempotentKey = idempotentKey; |
|||
ArgumentsDictionary = argumentsDictionary; |
|||
} |
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using System.Reflection; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.DistributedLocking; |
|||
|
|||
namespace LINGYUN.Abp.Idempotent; |
|||
|
|||
public class IdempotentChecker : IIdempotentChecker, ITransientDependency |
|||
{ |
|||
private readonly IAbpDistributedLock _distributedLock; |
|||
private readonly AbpIdempotentOptions _idempotentOptions; |
|||
private readonly IIdempotentDeniedHandler _idempotentDeniedHandler; |
|||
|
|||
public IdempotentChecker( |
|||
IAbpDistributedLock distributedLock, |
|||
IOptions<AbpIdempotentOptions> idempotentOptions, |
|||
IIdempotentDeniedHandler idempotentDeniedHandler) |
|||
{ |
|||
_distributedLock = distributedLock; |
|||
_idempotentOptions = idempotentOptions.Value; |
|||
_idempotentDeniedHandler = idempotentDeniedHandler; |
|||
} |
|||
|
|||
public async virtual Task CheckAsync(IdempotentCheckContext context) |
|||
{ |
|||
if (!_idempotentOptions.IsEnabled) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var attr = context.Method.GetCustomAttribute<IdempotentAttribute>(); |
|||
|
|||
var methodLockTimeout = _idempotentOptions.DefaultTimeout; |
|||
|
|||
if (attr != null) |
|||
{ |
|||
if (attr.Timeout.HasValue) |
|||
{ |
|||
methodLockTimeout = attr.Timeout.Value; |
|||
} |
|||
} |
|||
|
|||
await using var handle = await _distributedLock.TryAcquireAsync(context.IdempotentKey, TimeSpan.FromMilliseconds(methodLockTimeout)); |
|||
|
|||
if (handle == null) |
|||
{ |
|||
var deniedContext = new IdempotentDeniedContext( |
|||
context.IdempotentKey, |
|||
attr, |
|||
context.Method, |
|||
context.ArgumentsDictionary) |
|||
{ |
|||
HttpStatusCode = _idempotentOptions.HttpStatusCode, |
|||
}; |
|||
|
|||
_idempotentDeniedHandler.Denied(deniedContext); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
using System.Collections.Generic; |
|||
using System.Reflection; |
|||
|
|||
namespace LINGYUN.Abp.Idempotent; |
|||
public class IdempotentDeniedContext |
|||
{ |
|||
public IdempotentAttribute? Attribute { get; } |
|||
public string IdempotentKey { get; } |
|||
public MethodInfo Method { get; } |
|||
public IReadOnlyDictionary<string, object?> ArgumentsDictionary { get; } |
|||
public int HttpStatusCode { get; set; } |
|||
public IdempotentDeniedContext( |
|||
string idempotentKey, |
|||
IdempotentAttribute? attribute, |
|||
MethodInfo method, |
|||
IReadOnlyDictionary<string, object?> argumentsDictionary) |
|||
{ |
|||
IdempotentKey = idempotentKey; |
|||
Attribute = attribute; |
|||
Method = method; |
|||
ArgumentsDictionary = argumentsDictionary; |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
using Microsoft.Extensions.Logging; |
|||
using System; |
|||
using System.Runtime.Serialization; |
|||
using Volo.Abp; |
|||
using Volo.Abp.ExceptionHandling; |
|||
|
|||
namespace LINGYUN.Abp.Idempotent; |
|||
public class IdempotentDeniedException : BusinessException, IHasHttpStatusCode |
|||
{ |
|||
public string IdempotentKey { get; } |
|||
|
|||
public int HttpStatusCode { get; set; } |
|||
|
|||
public IdempotentDeniedException( |
|||
string idempotentKey, |
|||
string? code = null, |
|||
string? message = null, |
|||
string? details = null, |
|||
Exception? innerException = null, |
|||
LogLevel logLevel = LogLevel.Warning) |
|||
: base(code, message, details, innerException, logLevel) |
|||
{ |
|||
IdempotentKey = idempotentKey; |
|||
} |
|||
|
|||
public IdempotentDeniedException(SerializationInfo serializationInfo, StreamingContext context) |
|||
: base(serializationInfo, context) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
using System; |
|||
using System.Text.RegularExpressions; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace LINGYUN.Abp.Idempotent; |
|||
|
|||
public class IdempotentDeniedHandler : IIdempotentDeniedHandler, ISingletonDependency |
|||
{ |
|||
public virtual void Denied(IdempotentDeniedContext context) |
|||
{ |
|||
var exception = new IdempotentDeniedException(context.IdempotentKey, IdempotentErrorCodes.IdempotentDenied) |
|||
.WithData(nameof(IdempotentAttribute.IdempotentKey), context.IdempotentKey); |
|||
|
|||
if (context.Attribute != null && !string.IsNullOrWhiteSpace(context.Attribute.RedirectUrl)) |
|||
{ |
|||
var regex = new Regex("(?<={).+(?=})"); |
|||
if (regex.IsMatch(context.Attribute.RedirectUrl)) |
|||
{ |
|||
var matchValue = regex.Match(context.Attribute.RedirectUrl).Value; |
|||
var replaceMatchKey = "{" + matchValue + "}"; |
|||
var redirectUrl = ""; |
|||
foreach (var arg in context.ArgumentsDictionary) |
|||
{ |
|||
if (arg.Value != null && string.Equals(arg.Key, matchValue, StringComparison.InvariantCultureIgnoreCase)) |
|||
{ |
|||
redirectUrl = context.Attribute.RedirectUrl!.Replace(replaceMatchKey, arg.Value.ToString()); |
|||
} |
|||
} |
|||
|
|||
if (redirectUrl.IsNullOrWhiteSpace()) |
|||
{ |
|||
foreach (var arg in context.ArgumentsDictionary) |
|||
{ |
|||
if (arg.Value == null) |
|||
{ |
|||
continue; |
|||
} |
|||
var properties = arg.Value.GetType().GetProperties(); |
|||
foreach (var propertyInfo in properties) |
|||
{ |
|||
if (string.Equals(propertyInfo.Name, matchValue, StringComparison.InvariantCultureIgnoreCase)) |
|||
{ |
|||
var propValue = propertyInfo.GetValue(arg.Value); |
|||
if (propValue != null) |
|||
{ |
|||
redirectUrl = context.Attribute.RedirectUrl!.Replace(replaceMatchKey, propValue.ToString()); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (!redirectUrl.IsNullOrWhiteSpace()) |
|||
{ |
|||
exception.WithData(nameof(IdempotentAttribute.RedirectUrl), redirectUrl); |
|||
} |
|||
} |
|||
} |
|||
|
|||
throw exception; |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
namespace LINGYUN.Abp.Idempotent; |
|||
public static class IdempotentErrorCodes |
|||
{ |
|||
private const string Namespace = "Idempotent"; |
|||
|
|||
public const string IdempotentDenied = Namespace + ":010001"; |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.DynamicProxy; |
|||
|
|||
namespace LINGYUN.Abp.Idempotent; |
|||
|
|||
public class IdempotentInterceptor : AbpInterceptor, ITransientDependency |
|||
{ |
|||
private readonly IIdempotentChecker _idempotentChecker; |
|||
private readonly IIdempotentKeyNormalizer _idempotentKeyNormalizer; |
|||
|
|||
public IdempotentInterceptor( |
|||
IIdempotentChecker idempotentChecker, |
|||
IIdempotentKeyNormalizer idempotentKeyNormalizer) |
|||
{ |
|||
_idempotentChecker = idempotentChecker; |
|||
_idempotentKeyNormalizer = idempotentKeyNormalizer; |
|||
} |
|||
|
|||
public async override Task InterceptAsync(IAbpMethodInvocation invocation) |
|||
{ |
|||
var targetType = ProxyHelper.GetUnProxiedType(invocation.TargetObject); |
|||
|
|||
var keyNormalizerContext = new IdempotentKeyNormalizerContext( |
|||
targetType, |
|||
invocation.Method, |
|||
invocation.ArgumentsDictionary); |
|||
|
|||
var idempotentKey = _idempotentKeyNormalizer.NormalizeKey(keyNormalizerContext); |
|||
|
|||
var checkContext = new IdempotentCheckContext( |
|||
targetType, |
|||
invocation.Method, |
|||
idempotentKey, |
|||
invocation.ArgumentsDictionary); |
|||
|
|||
await _idempotentChecker.CheckAsync(checkContext); |
|||
|
|||
await invocation.ProceedAsync(); |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using System.Reflection; |
|||
using Volo.Abp.Application.Services; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.DynamicProxy; |
|||
|
|||
namespace LINGYUN.Abp.Idempotent; |
|||
public static class IdempotentInterceptorRegistrar |
|||
{ |
|||
public static void RegisterIfNeeded(IOnServiceRegistredContext context) |
|||
{ |
|||
if (ShouldIntercept(context.ImplementationType)) |
|||
{ |
|||
context.Interceptors.TryAdd<IdempotentInterceptor>(); |
|||
} |
|||
} |
|||
|
|||
private static bool ShouldIntercept(Type type) |
|||
{ |
|||
return !DynamicProxyIgnoreTypes.Contains(type) && |
|||
(typeof(ICreateAppService<,>).IsAssignableFrom(type) || |
|||
typeof(IUpdateAppService<,>).IsAssignableFrom(type) || |
|||
typeof(IDeleteAppService<>).IsAssignableFrom(type) || |
|||
AnyMethodHasIdempotentAttribute(type)); |
|||
} |
|||
|
|||
private static bool AnyMethodHasIdempotentAttribute(Type implementationType) |
|||
{ |
|||
return implementationType |
|||
.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) |
|||
.Any(HasIdempotentAttribute); |
|||
} |
|||
|
|||
private static bool HasIdempotentAttribute(MemberInfo methodInfo) |
|||
{ |
|||
return methodInfo.IsDefined(typeof(IdempotentAttribute), true); |
|||
} |
|||
} |
|||
@ -0,0 +1,99 @@ |
|||
using System; |
|||
using System.Reflection; |
|||
using System.Text; |
|||
using Volo.Abp.Application.Services; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Json; |
|||
|
|||
namespace LINGYUN.Abp.Idempotent; |
|||
public class IdempotentKeyNormalizer : IIdempotentKeyNormalizer, ITransientDependency |
|||
{ |
|||
private const string KeyFormat = "t:{0};m:{1};k:{}"; |
|||
|
|||
private readonly IJsonSerializer _jsonSerializer; |
|||
|
|||
public IdempotentKeyNormalizer( |
|||
IJsonSerializer jsonSerializer) |
|||
{ |
|||
_jsonSerializer = jsonSerializer; |
|||
} |
|||
|
|||
public virtual string NormalizeKey(IdempotentKeyNormalizerContext context) |
|||
{ |
|||
var methodIdBuilder = new StringBuilder(); |
|||
|
|||
if (context.Method.IsDefined(typeof(IdempotentAttribute))) |
|||
{ |
|||
var attr = context.Method.GetCustomAttribute<IdempotentAttribute>(); |
|||
if (!attr.IdempotentKey.IsNullOrWhiteSpace()) |
|||
{ |
|||
return attr.IdempotentKey!; |
|||
} |
|||
if (attr.KeyMap != null) |
|||
{ |
|||
var index = 0; |
|||
foreach (var key in attr.KeyMap) |
|||
{ |
|||
if (context.ArgumentsDictionary.TryGetValue(key, out var value)) |
|||
{ |
|||
var objectToString = _jsonSerializer.Serialize(value); |
|||
var objectMd5 = objectToString.ToMd5(); |
|||
methodIdBuilder.AppendFormat(";i:{0};v:{1}", key, objectMd5); |
|||
} |
|||
else |
|||
{ |
|||
methodIdBuilder.AppendFormat(";i:{0}", key); |
|||
} |
|||
index++; |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
if (typeof(ICreateAppService<,>).IsAssignableFrom(context.Target) && |
|||
"CreateAsync".Equals(context.Method.Name, StringComparison.InvariantCultureIgnoreCase)) |
|||
{ |
|||
if (context.ArgumentsDictionary.TryGetValue("input", out var args)) |
|||
{ |
|||
var objectToString = _jsonSerializer.Serialize(args); |
|||
var objectMd5 = objectToString.ToMd5(); |
|||
methodIdBuilder.AppendFormat(";i:input;v:{0}", objectMd5); |
|||
} |
|||
} |
|||
|
|||
if (typeof(IUpdateAppService<,>).IsAssignableFrom(context.Target) && |
|||
"UpdateAsync".Equals(context.Method.Name, StringComparison.InvariantCultureIgnoreCase)) |
|||
{ |
|||
if (context.ArgumentsDictionary.TryGetValue("id", out var idArgs) && idArgs != null) |
|||
{ |
|||
var idMd5 = idArgs.ToString().ToMd5(); |
|||
methodIdBuilder.AppendFormat(";i:id;v:{0}", idMd5); |
|||
} |
|||
|
|||
if (context.ArgumentsDictionary.TryGetValue("input", out var inputArgs) && inputArgs != null) |
|||
{ |
|||
var objectToString = _jsonSerializer.Serialize(inputArgs); |
|||
var objectMd5 = objectToString.ToMd5(); |
|||
methodIdBuilder.AppendFormat(";i:input;v:{0}", objectMd5); |
|||
} |
|||
} |
|||
|
|||
if (typeof(IDeleteAppService<>).IsAssignableFrom(context.Target) && |
|||
"DeleteAsync".Equals(context.Method.Name, StringComparison.InvariantCultureIgnoreCase)) |
|||
{ |
|||
if (context.ArgumentsDictionary.TryGetValue("id", out var idArgs) && idArgs != null) |
|||
{ |
|||
var idMd5 = idArgs.ToString().ToMd5(); |
|||
methodIdBuilder.AppendFormat(";i:id;v:{0}", idMd5); |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (methodIdBuilder.Length <= 0) |
|||
{ |
|||
methodIdBuilder.Append("unknown"); |
|||
} |
|||
|
|||
return string.Format(KeyFormat, context.Target.FullName, context.Method.Name, methodIdBuilder.ToString()); |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Reflection; |
|||
|
|||
namespace LINGYUN.Abp.Idempotent; |
|||
|
|||
public class IdempotentKeyNormalizerContext |
|||
{ |
|||
public Type Target { get; } |
|||
public MethodInfo Method { get; } |
|||
public IReadOnlyDictionary<string, object?> ArgumentsDictionary { get; } |
|||
|
|||
public IdempotentKeyNormalizerContext( |
|||
Type target, |
|||
MethodInfo method, |
|||
IReadOnlyDictionary<string, object?> argumentsDictionary) |
|||
{ |
|||
Target = target; |
|||
Method = method; |
|||
ArgumentsDictionary = argumentsDictionary; |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
using Volo.Abp.Localization; |
|||
|
|||
namespace LINGYUN.Abp.Idempotent.Localization; |
|||
|
|||
[LocalizationResourceName("Idempotent")] |
|||
public class IdempotentResource |
|||
{ |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
{ |
|||
"culture": "en", |
|||
"texts": { |
|||
"Idempotent:010001": "Unable to submit multiple requests within the time limit, please try again later!" |
|||
} |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
{ |
|||
"culture": "zh-Hans", |
|||
"texts": { |
|||
"Idempotent:010001": "无法在限定时间内重复提交多次请求, 请稍后再试!" |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
# LINGYUN.Abp.Idempotent |
|||
|
|||
接口幂等性检查模块 |
|||
|
|||
## 配置使用 |
|||
|
|||
```csharp |
|||
[DependsOn(typeof(AbpIdempotentModule))] |
|||
public class YouProjectModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpIdempotentOptions>(options => |
|||
{ |
|||
// 全局启用幂等检查 |
|||
options.IsEnabled = true; |
|||
// 默认每个接口提供30秒超时 |
|||
options.DefaultTimeout = 30000; |
|||
// 幂等token名称, 通过HttpHeader传递 |
|||
options.IdempotentTokenName = "X-With-Idempotent-Token"; |
|||
// 幂等校验失败时Http响应代码 |
|||
options.HttpStatusCode = 429; |
|||
}); |
|||
} |
|||
} |
|||
``` |
|||
## 配置项说明 |
|||
|
|||
## 其他 |
|||
|
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,16 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net7.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\LINGYUN.Abp.AspNetCore.Mvc.Idempotent\LINGYUN.Abp.AspNetCore.Mvc.Idempotent.csproj" /> |
|||
<ProjectReference Include="..\LINGYUN.Abp.AspNetCore.Mvc.Wrapper\LINGYUN.Abp.AspNetCore.Mvc.Wrapper.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,12 @@ |
|||
using LINGYUN.Abp.AspNetCore.Mvc.Wrapper; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpAspNetCoreMvcIdempotentModule), |
|||
typeof(AbpAspNetCoreMvcWrapperModule))] |
|||
public class AbpAspNetCoreMvcIdempotentWrapperModule : AbpModule |
|||
{ |
|||
|
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
using LINGYUN.Abp.AspNetCore.Mvc.Wrapper.ExceptionHandling; |
|||
using LINGYUN.Abp.Idempotent; |
|||
using Microsoft.AspNetCore.Mvc.Filters; |
|||
using Microsoft.Extensions.Options; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc.ExceptionHandling; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper; |
|||
|
|||
[Dependency(ReplaceServices = true)] |
|||
[ExposeServices( |
|||
typeof(AbpExceptionPageFilter), |
|||
typeof(AbpExceptionPageWrapResultFilter))] |
|||
public class AbpIdempotentExceptionPageWrapResultFilter : AbpExceptionPageWrapResultFilter, ITransientDependency |
|||
{ |
|||
protected async override Task HandleAndWrapException(PageHandlerExecutedContext context) |
|||
{ |
|||
if (context.Exception is IdempotentDeniedException deniedException) |
|||
{ |
|||
var options = context.GetRequiredService<IOptions<AbpIdempotentOptions>>().Value; |
|||
if (!context.HttpContext.Items.ContainsKey(options.IdempotentTokenName)) |
|||
{ |
|||
context.HttpContext.Items.Add(options.IdempotentTokenName, deniedException.IdempotentKey); |
|||
} |
|||
} |
|||
await base.HandleAndWrapException(context); |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
using LINGYUN.Abp.AspNetCore.Mvc.Wrapper.ExceptionHandling; |
|||
using LINGYUN.Abp.Idempotent; |
|||
using Microsoft.AspNetCore.Mvc.Filters; |
|||
using Microsoft.Extensions.Options; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc.ExceptionHandling; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace LINGYUN.Abp.AspNetCore.Mvc.Idempotent; |
|||
|
|||
[Dependency(ReplaceServices = true)] |
|||
[ExposeServices( |
|||
typeof(AbpExceptionFilter), |
|||
typeof(AbpExceptionWrapResultFilter))] |
|||
public class AbpIdempotentExceptionWrapResultFilter : AbpExceptionWrapResultFilter, ITransientDependency |
|||
{ |
|||
protected async override Task HandleAndWrapException(ExceptionContext context) |
|||
{ |
|||
if (context.Exception is IdempotentDeniedException deniedException) |
|||
{ |
|||
var options = context.GetRequiredService<IOptions<AbpIdempotentOptions>>().Value; |
|||
if (!context.HttpContext.Items.ContainsKey(options.IdempotentTokenName)) |
|||
{ |
|||
context.HttpContext.Items.Add(options.IdempotentTokenName, deniedException.IdempotentKey); |
|||
} |
|||
} |
|||
await base.HandleAndWrapException(context); |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
using LINGYUN.Abp.AspNetCore.Mvc.Wrapper; |
|||
using LINGYUN.Abp.Idempotent; |
|||
using LINGYUN.Abp.Wrapper; |
|||
using Microsoft.Extensions.Options; |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper; |
|||
|
|||
[Dependency(ReplaceServices = true)] |
|||
[ExposeServices( |
|||
typeof(IHttpResponseWrapper), |
|||
typeof(HttpResponseWrapper))] |
|||
public class IdempotentHttpResponseWrapper : HttpResponseWrapper, ITransientDependency |
|||
{ |
|||
protected AbpIdempotentOptions IdempotentOptions { get; } |
|||
public IdempotentHttpResponseWrapper( |
|||
IOptions<AbpWrapperOptions> wrapperOptions, |
|||
IOptions<AbpIdempotentOptions> idempotentOptions) : base(wrapperOptions) |
|||
{ |
|||
IdempotentOptions = idempotentOptions.Value; |
|||
} |
|||
|
|||
public override void Wrap(HttpResponseWrapperContext context) |
|||
{ |
|||
if (context.HttpContext.Items.TryGetValue(nameof(IdempotentAttribute.RedirectUrl), out var redirectUrl) && redirectUrl != null) |
|||
{ |
|||
context.HttpContext.Response.Headers.Add(AbpHttpWrapConsts.AbpWrapResult, "true"); |
|||
context.HttpContext.Response.Redirect(redirectUrl.ToString()); |
|||
return; |
|||
} |
|||
|
|||
if (context.HttpContext.Items.TryGetValue(IdempotentOptions.IdempotentTokenName, out var idempotentKey) && idempotentKey != null) |
|||
{ |
|||
context.HttpHeaders.TryAdd(IdempotentOptions.IdempotentTokenName, idempotentKey.ToString()); |
|||
} |
|||
|
|||
base.Wrap(context); |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
# LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper |
|||
|
|||
MVC 接口幂等性包装器模块, 启用包装器模块后, 写入校验失败的请求头 |
|||
|
|||
## 配置使用 |
|||
|
|||
```csharp |
|||
[DependsOn(typeof(AbpAspNetCoreMvcIdempotentWrapperModule))] |
|||
public class YouProjectModule : AbpModule |
|||
{ |
|||
|
|||
} |
|||
``` |
|||
## 配置项说明 |
|||
|
|||
## 其他 |
|||
|
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,19 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net7.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.AspNetCore.Mvc" Version="$(VoloAbpPackageVersion)" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\common\LINGYUN.Abp.Idempotent\LINGYUN.Abp.Idempotent.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,21 @@ |
|||
using LINGYUN.Abp.Idempotent; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.ExceptionHandling; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.AspNetCore.Mvc.Idempotent; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpIdempotentModule), |
|||
typeof(AbpAspNetCoreMvcModule))] |
|||
public class AbpAspNetCoreMvcIdempotentModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<MvcOptions>(options => |
|||
{ |
|||
options.Filters.AddService(typeof(AbpIdempotentActionFilter)); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace LINGYUN.Abp.AspNetCore.Mvc.Idempotent; |
|||
public class AbpAspNetCoreMvcIdempotentOptions |
|||
{ |
|||
public List<string> SupportedMethods { get; } |
|||
public AbpAspNetCoreMvcIdempotentOptions() |
|||
{ |
|||
SupportedMethods = new List<string> |
|||
{ |
|||
"POST", |
|||
"PUT", |
|||
"PATCH", |
|||
// "DELETE"
|
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,77 @@ |
|||
using LINGYUN.Abp.Idempotent; |
|||
using Microsoft.AspNetCore.Mvc.Abstractions; |
|||
using Microsoft.AspNetCore.Mvc.Filters; |
|||
using Microsoft.Extensions.Options; |
|||
using System.Collections.Immutable; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace LINGYUN.Abp.AspNetCore.Mvc.Idempotent; |
|||
|
|||
public class AbpIdempotentActionFilter : IAsyncActionFilter, ITransientDependency |
|||
{ |
|||
public async virtual Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) |
|||
{ |
|||
if (!ShouldCheckIdempotent(context)) |
|||
{ |
|||
await next(); |
|||
return; |
|||
} |
|||
|
|||
var idempotentChecker = context.GetRequiredService<IIdempotentChecker>(); |
|||
var options = context.GetRequiredService<IOptions<AbpIdempotentOptions>>().Value; |
|||
|
|||
var targetType = context.ActionDescriptor.AsControllerActionDescriptor().ControllerTypeInfo.AsType(); |
|||
var methodInfo = context.ActionDescriptor.AsControllerActionDescriptor().MethodInfo; |
|||
|
|||
string idempotentKey; |
|||
// 可以用户传递幂等性token
|
|||
// 否则通过用户定义action创建token
|
|||
if (context.HttpContext.Request.Headers.TryGetValue(options.IdempotentTokenName, out var key)) |
|||
{ |
|||
idempotentKey = key.ToString(); |
|||
} |
|||
else |
|||
{ |
|||
|
|||
var idempotentKeyNormalizer = context.GetRequiredService<IIdempotentKeyNormalizer>(); |
|||
|
|||
var keyNormalizerContext = new IdempotentKeyNormalizerContext( |
|||
targetType, |
|||
methodInfo, |
|||
context.ActionArguments.ToImmutableDictionary()); |
|||
|
|||
idempotentKey = idempotentKeyNormalizer.NormalizeKey(keyNormalizerContext); |
|||
} |
|||
|
|||
var checkContext = new IdempotentCheckContext( |
|||
targetType, |
|||
methodInfo, |
|||
idempotentKey, |
|||
context.ActionArguments.ToImmutableDictionary()); |
|||
// 进行幂等性校验
|
|||
await idempotentChecker.CheckAsync(checkContext); |
|||
|
|||
await next(); |
|||
} |
|||
|
|||
protected virtual bool ShouldCheckIdempotent(ActionExecutingContext context) |
|||
{ |
|||
var options = context.GetRequiredService<IOptions<AbpIdempotentOptions>>().Value; |
|||
if (!options.IsEnabled) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var mvcIdempotentOptions = context.GetRequiredService<IOptions<AbpAspNetCoreMvcIdempotentOptions>>().Value; |
|||
if (mvcIdempotentOptions.SupportedMethods.Any() && |
|||
!mvcIdempotentOptions.SupportedMethods.Contains(context.HttpContext.Request.Method)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
# LINGYUN.Abp.AspNetCore.Mvc.Idempotent |
|||
|
|||
MVC 接口幂等性检查模块 |
|||
|
|||
## 配置使用 |
|||
|
|||
```csharp |
|||
[DependsOn(typeof(AbpAspNetCoreMvcIdempotentModule))] |
|||
public class YouProjectModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpMvcIdempotentOptions>(options => |
|||
{ |
|||
// 例如: 对 DELETE 请求方法进行幂等校验 |
|||
options.SupportedMethods.Add("DELETE"); |
|||
}); |
|||
} |
|||
} |
|||
``` |
|||
## 配置项说明 |
|||
|
|||
## 其他 |
|||
|
|||
@ -0,0 +1,29 @@ |
|||
using LINGYUN.Abp.AspNetCore.Mvc.Wrapper; |
|||
using LINGYUN.Abp.Wrapper; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace LINGYUN.Abp.AspNetCore.Mvc; |
|||
|
|||
public class HttpResponseWrapper : IHttpResponseWrapper, ITransientDependency |
|||
{ |
|||
protected AbpWrapperOptions Options { get; } |
|||
|
|||
public HttpResponseWrapper(IOptions<AbpWrapperOptions> options) |
|||
{ |
|||
Options = options.Value; |
|||
} |
|||
|
|||
public virtual void Wrap(HttpResponseWrapperContext context) |
|||
{ |
|||
context.HttpContext.Response.Headers.Add(AbpHttpWrapConsts.AbpWrapResult, "true"); |
|||
context.HttpContext.Response.StatusCode = context.HttpStatusCode; |
|||
if (context.HttpHeaders != null) |
|||
{ |
|||
foreach (var header in context.HttpHeaders) |
|||
{ |
|||
context.HttpContext.Response.Headers.Add(header.Key, header.Value); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
using Microsoft.AspNetCore.Http; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace LINGYUN.Abp.AspNetCore.Mvc.Wrapper; |
|||
public class HttpResponseWrapperContext |
|||
{ |
|||
public HttpContext HttpContext { get; } |
|||
public int HttpStatusCode { get; } |
|||
public IDictionary<string, string> HttpHeaders { get; } |
|||
public HttpResponseWrapperContext( |
|||
HttpContext httpContext, |
|||
int httpStatusCode, |
|||
IDictionary<string, string> httpHeaders = null) |
|||
{ |
|||
HttpContext = httpContext; |
|||
HttpStatusCode = httpStatusCode; |
|||
HttpHeaders = httpHeaders ?? new Dictionary<string, string>(); |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
using Microsoft.AspNetCore.Http; |
|||
|
|||
namespace LINGYUN.Abp.AspNetCore.Mvc.Wrapper; |
|||
public interface IHttpResponseWrapper |
|||
{ |
|||
void Wrap(HttpResponseWrapperContext context); |
|||
} |
|||
Loading…
Reference in new issue