diff --git a/aspnet-core/LINGYUN.MicroService.Common.sln b/aspnet-core/LINGYUN.MicroService.Common.sln
index e3b9965ee..c878eb1b2 100644
--- a/aspnet-core/LINGYUN.MicroService.Common.sln
+++ b/aspnet-core/LINGYUN.MicroService.Common.sln
@@ -321,6 +321,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
Directory.Build.props = Directory.Build.props
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.AspNetCore.Mvc.Idempotent", "modules\mvc\LINGYUN.Abp.AspNetCore.Mvc.Idempotent\LINGYUN.Abp.AspNetCore.Mvc.Idempotent.csproj", "{347413DD-1B30-46B5-87A0-828A11FAA87D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.Idempotent", "modules\common\LINGYUN.Abp.Idempotent\LINGYUN.Abp.Idempotent.csproj", "{13FCEB03-E300-4CE2-A789-78D9F41C903E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper", "modules\mvc\LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper\LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper.csproj", "{8B15AAB5-18BB-4A2E-86F1-4A2F04C9FAFF}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -823,6 +829,18 @@ Global
{4059233C-C651-4DA2-A1BC-26196362062A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4059233C-C651-4DA2-A1BC-26196362062A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4059233C-C651-4DA2-A1BC-26196362062A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {347413DD-1B30-46B5-87A0-828A11FAA87D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {347413DD-1B30-46B5-87A0-828A11FAA87D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {347413DD-1B30-46B5-87A0-828A11FAA87D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {347413DD-1B30-46B5-87A0-828A11FAA87D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {13FCEB03-E300-4CE2-A789-78D9F41C903E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {13FCEB03-E300-4CE2-A789-78D9F41C903E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {13FCEB03-E300-4CE2-A789-78D9F41C903E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {13FCEB03-E300-4CE2-A789-78D9F41C903E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8B15AAB5-18BB-4A2E-86F1-4A2F04C9FAFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8B15AAB5-18BB-4A2E-86F1-4A2F04C9FAFF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8B15AAB5-18BB-4A2E-86F1-4A2F04C9FAFF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8B15AAB5-18BB-4A2E-86F1-4A2F04C9FAFF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -979,6 +997,9 @@ Global
{4CEFE059-B30E-4121-AA12-10EC72709758} = {C12EEBC0-0407-4AEF-81C4-EDF5E22BB00E}
{C9EC8CCF-5CA7-4332-B7B7-FF9B094FA418} = {C12EEBC0-0407-4AEF-81C4-EDF5E22BB00E}
{4059233C-C651-4DA2-A1BC-26196362062A} = {C12EEBC0-0407-4AEF-81C4-EDF5E22BB00E}
+ {347413DD-1B30-46B5-87A0-828A11FAA87D} = {F55B987D-1DFF-4EB0-9949-8A7136A7B689}
+ {13FCEB03-E300-4CE2-A789-78D9F41C903E} = {086BE5BE-8594-4DA7-8819-935FEF76DABD}
+ {8B15AAB5-18BB-4A2E-86F1-4A2F04C9FAFF} = {F55B987D-1DFF-4EB0-9949-8A7136A7B689}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {06C707C6-02C0-411A-AD3B-2D0E13787CB8}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/FodyWeavers.xml b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/FodyWeavers.xml
new file mode 100644
index 000000000..1715698cc
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/FodyWeavers.xsd b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/FodyWeavers.xsd
new file mode 100644
index 000000000..11da52550
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN.Abp.Idempotent.csproj b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN.Abp.Idempotent.csproj
new file mode 100644
index 000000000..8897ad0dc
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN.Abp.Idempotent.csproj
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ netstandard2.0
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/AbpIdempotentModule.cs b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/AbpIdempotentModule.cs
new file mode 100644
index 000000000..81ca45879
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/AbpIdempotentModule.cs
@@ -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(options =>
+ {
+ options.FileSets.AddEmbedded();
+ });
+
+ Configure(options =>
+ {
+ options.Resources.Add()
+ .AddVirtualJson("/LINGYUN/Abp/Idempotent/Localization/Resources");
+ });
+ }
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/AbpIdempotentOptions.cs b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/AbpIdempotentOptions.cs
new file mode 100644
index 000000000..885f4aa95
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/AbpIdempotentOptions.cs
@@ -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;
+ }
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IIdempotentChecker.cs b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IIdempotentChecker.cs
new file mode 100644
index 000000000..d0849d764
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IIdempotentChecker.cs
@@ -0,0 +1,7 @@
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Idempotent;
+public interface IIdempotentChecker
+{
+ Task CheckAsync(IdempotentCheckContext context);
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IIdempotentDeniedHandler.cs b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IIdempotentDeniedHandler.cs
new file mode 100644
index 000000000..ec7ade92e
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IIdempotentDeniedHandler.cs
@@ -0,0 +1,5 @@
+namespace LINGYUN.Abp.Idempotent;
+public interface IIdempotentDeniedHandler
+{
+ void Denied(IdempotentDeniedContext context);
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IIdempotentKeyNormalizer.cs b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IIdempotentKeyNormalizer.cs
new file mode 100644
index 000000000..69a3a5b58
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IIdempotentKeyNormalizer.cs
@@ -0,0 +1,8 @@
+using Volo.Abp.DynamicProxy;
+
+namespace LINGYUN.Abp.Idempotent;
+
+public interface IIdempotentKeyNormalizer
+{
+ string NormalizeKey(IdempotentKeyNormalizerContext context);
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentAttribute.cs b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentAttribute.cs
new file mode 100644
index 000000000..09b36e5ce
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentAttribute.cs
@@ -0,0 +1,48 @@
+using System;
+
+namespace LINGYUN.Abp.Idempotent;
+
+[AttributeUsage(AttributeTargets.Method)]
+public class IdempotentAttribute : Attribute
+{
+ ///
+ /// 超时等待时间
+ ///
+ public int? Timeout { get; }
+ ///
+ /// 用作建立唯一标识的参数名称列表
+ ///
+ ///
+ /// 通过查找参数列表中的值定义序列化唯一md5值
+ ///
+ public string[]? KeyMap { get; }
+ ///
+ /// 资源重定向路径模板
+ ///
+ ///
+ /// 定义一个可格式化的资源路径,当请求冲突时如果参数名称匹配成功则重定向目标路径
+ ///
+ /// 例: /api/app/demo/{id}
+ ///
+ /// 例: /api/app/demo/method?id={id}
+ ///
+ public string? RedirectUrl { get; }
+ ///
+ /// 自定义的幂等key
+ ///
+ public string? IdempotentKey { get; }
+ public IdempotentAttribute()
+ {
+
+ }
+
+ public IdempotentAttribute(
+ int? timeout = null,
+ string? redirectUrl = null,
+ string[]? keyMap = null)
+ {
+ Timeout = timeout;
+ KeyMap = keyMap;
+ RedirectUrl = redirectUrl;
+ }
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentCheckContext.cs b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentCheckContext.cs
new file mode 100644
index 000000000..13bd8ffd6
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentCheckContext.cs
@@ -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 ArgumentsDictionary { get; }
+
+ public IdempotentCheckContext(
+ Type target,
+ MethodInfo method,
+ string idempotentKey,
+ IReadOnlyDictionary argumentsDictionary)
+ {
+ Target = target;
+ Method = method;
+ IdempotentKey = idempotentKey;
+ ArgumentsDictionary = argumentsDictionary;
+ }
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentChecker.cs b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentChecker.cs
new file mode 100644
index 000000000..a7bfaf791
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentChecker.cs
@@ -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 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();
+
+ 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);
+ }
+ }
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentDeniedContext.cs b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentDeniedContext.cs
new file mode 100644
index 000000000..3fc8be3ad
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentDeniedContext.cs
@@ -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 ArgumentsDictionary { get; }
+ public int HttpStatusCode { get; set; }
+ public IdempotentDeniedContext(
+ string idempotentKey,
+ IdempotentAttribute? attribute,
+ MethodInfo method,
+ IReadOnlyDictionary argumentsDictionary)
+ {
+ IdempotentKey = idempotentKey;
+ Attribute = attribute;
+ Method = method;
+ ArgumentsDictionary = argumentsDictionary;
+ }
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentDeniedException.cs b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentDeniedException.cs
new file mode 100644
index 000000000..3f8d38dd5
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentDeniedException.cs
@@ -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)
+ {
+ }
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentDeniedHandler.cs b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentDeniedHandler.cs
new file mode 100644
index 000000000..517c48467
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentDeniedHandler.cs
@@ -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;
+ }
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentErrorCodes.cs b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentErrorCodes.cs
new file mode 100644
index 000000000..0ac89d147
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentErrorCodes.cs
@@ -0,0 +1,7 @@
+namespace LINGYUN.Abp.Idempotent;
+public static class IdempotentErrorCodes
+{
+ private const string Namespace = "Idempotent";
+
+ public const string IdempotentDenied = Namespace + ":010001";
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentInterceptor.cs b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentInterceptor.cs
new file mode 100644
index 000000000..4cceef3d6
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentInterceptor.cs
@@ -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();
+ }
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentInterceptorRegistrar.cs b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentInterceptorRegistrar.cs
new file mode 100644
index 000000000..2fc1a0e1e
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentInterceptorRegistrar.cs
@@ -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();
+ }
+ }
+
+ 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);
+ }
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentKeyNormalizer.cs b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentKeyNormalizer.cs
new file mode 100644
index 000000000..dbd32af04
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentKeyNormalizer.cs
@@ -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();
+ 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());
+ }
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentKeyNormalizerContext.cs b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentKeyNormalizerContext.cs
new file mode 100644
index 000000000..151e6443b
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentKeyNormalizerContext.cs
@@ -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 ArgumentsDictionary { get; }
+
+ public IdempotentKeyNormalizerContext(
+ Type target,
+ MethodInfo method,
+ IReadOnlyDictionary argumentsDictionary)
+ {
+ Target = target;
+ Method = method;
+ ArgumentsDictionary = argumentsDictionary;
+ }
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/Localization/IdempotentResource.cs b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/Localization/IdempotentResource.cs
new file mode 100644
index 000000000..be20d8208
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/Localization/IdempotentResource.cs
@@ -0,0 +1,8 @@
+using Volo.Abp.Localization;
+
+namespace LINGYUN.Abp.Idempotent.Localization;
+
+[LocalizationResourceName("Idempotent")]
+public class IdempotentResource
+{
+}
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/Localization/Resources/en.json b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/Localization/Resources/en.json
new file mode 100644
index 000000000..d0701c831
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/Localization/Resources/en.json
@@ -0,0 +1,6 @@
+{
+ "culture": "en",
+ "texts": {
+ "Idempotent:010001": "Unable to submit multiple requests within the time limit, please try again later!"
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/Localization/Resources/zh-Hans.json b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/Localization/Resources/zh-Hans.json
new file mode 100644
index 000000000..db0fc0001
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/Localization/Resources/zh-Hans.json
@@ -0,0 +1,6 @@
+{
+ "culture": "zh-Hans",
+ "texts": {
+ "Idempotent:010001": "无法在限定时间内重复提交多次请求, 请稍后再试!"
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/README.md b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/README.md
new file mode 100644
index 000000000..ff294a45e
--- /dev/null
+++ b/aspnet-core/modules/common/LINGYUN.Abp.Idempotent/README.md
@@ -0,0 +1,30 @@
+# LINGYUN.Abp.Idempotent
+
+接口幂等性检查模块
+
+## 配置使用
+
+```csharp
+[DependsOn(typeof(AbpIdempotentModule))]
+public class YouProjectModule : AbpModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ Configure(options =>
+ {
+ // 全局启用幂等检查
+ options.IsEnabled = true;
+ // 默认每个接口提供30秒超时
+ options.DefaultTimeout = 30000;
+ // 幂等token名称, 通过HttpHeader传递
+ options.IdempotentTokenName = "X-With-Idempotent-Token";
+ // 幂等校验失败时Http响应代码
+ options.HttpStatusCode = 429;
+ });
+ }
+}
+```
+## 配置项说明
+
+## 其他
+
diff --git a/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/FodyWeavers.xml b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/FodyWeavers.xml
new file mode 100644
index 000000000..1715698cc
--- /dev/null
+++ b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/FodyWeavers.xsd b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/FodyWeavers.xsd
new file mode 100644
index 000000000..11da52550
--- /dev/null
+++ b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper.csproj b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper.csproj
new file mode 100644
index 000000000..bf06a2e47
--- /dev/null
+++ b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper.csproj
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ net7.0
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/Wrapper/AbpAspNetCoreMvcIdempotentWrapperModule.cs b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/Wrapper/AbpAspNetCoreMvcIdempotentWrapperModule.cs
new file mode 100644
index 000000000..0a06ea19e
--- /dev/null
+++ b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/Wrapper/AbpAspNetCoreMvcIdempotentWrapperModule.cs
@@ -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
+{
+
+}
diff --git a/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/Wrapper/AbpIdempotentExceptionPageWrapResultFilter.cs b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/Wrapper/AbpIdempotentExceptionPageWrapResultFilter.cs
new file mode 100644
index 000000000..b854b37ec
--- /dev/null
+++ b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/Wrapper/AbpIdempotentExceptionPageWrapResultFilter.cs
@@ -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>().Value;
+ if (!context.HttpContext.Items.ContainsKey(options.IdempotentTokenName))
+ {
+ context.HttpContext.Items.Add(options.IdempotentTokenName, deniedException.IdempotentKey);
+ }
+ }
+ await base.HandleAndWrapException(context);
+ }
+}
diff --git a/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/Wrapper/AbpIdempotentExceptionWrapResultFilter.cs b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/Wrapper/AbpIdempotentExceptionWrapResultFilter.cs
new file mode 100644
index 000000000..9ba50c757
--- /dev/null
+++ b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/Wrapper/AbpIdempotentExceptionWrapResultFilter.cs
@@ -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>().Value;
+ if (!context.HttpContext.Items.ContainsKey(options.IdempotentTokenName))
+ {
+ context.HttpContext.Items.Add(options.IdempotentTokenName, deniedException.IdempotentKey);
+ }
+ }
+ await base.HandleAndWrapException(context);
+ }
+}
diff --git a/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/Wrapper/IdempotentHttpResponseWrapper.cs b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/Wrapper/IdempotentHttpResponseWrapper.cs
new file mode 100644
index 000000000..e9392129d
--- /dev/null
+++ b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/Wrapper/IdempotentHttpResponseWrapper.cs
@@ -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 wrapperOptions,
+ IOptions 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);
+ }
+}
diff --git a/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/README.md b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/README.md
new file mode 100644
index 000000000..1daaa38f6
--- /dev/null
+++ b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/README.md
@@ -0,0 +1,17 @@
+# LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper
+
+MVC 接口幂等性包装器模块, 启用包装器模块后, 写入校验失败的请求头
+
+## 配置使用
+
+```csharp
+[DependsOn(typeof(AbpAspNetCoreMvcIdempotentWrapperModule))]
+public class YouProjectModule : AbpModule
+{
+
+}
+```
+## 配置项说明
+
+## 其他
+
diff --git a/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/FodyWeavers.xml b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/FodyWeavers.xml
new file mode 100644
index 000000000..1715698cc
--- /dev/null
+++ b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/FodyWeavers.xsd b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/FodyWeavers.xsd
new file mode 100644
index 000000000..11da52550
--- /dev/null
+++ b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.csproj b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.csproj
new file mode 100644
index 000000000..1f95eb8d9
--- /dev/null
+++ b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.csproj
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ net7.0
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/AbpAspNetCoreMvcIdempotentModule.cs b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/AbpAspNetCoreMvcIdempotentModule.cs
new file mode 100644
index 000000000..025f9c02b
--- /dev/null
+++ b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/AbpAspNetCoreMvcIdempotentModule.cs
@@ -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(options =>
+ {
+ options.Filters.AddService(typeof(AbpIdempotentActionFilter));
+ });
+ }
+}
diff --git a/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/AbpAspNetCoreMvcIdempotentOptions.cs b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/AbpAspNetCoreMvcIdempotentOptions.cs
new file mode 100644
index 000000000..034e70c8d
--- /dev/null
+++ b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/AbpAspNetCoreMvcIdempotentOptions.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+
+namespace LINGYUN.Abp.AspNetCore.Mvc.Idempotent;
+public class AbpAspNetCoreMvcIdempotentOptions
+{
+ public List SupportedMethods { get; }
+ public AbpAspNetCoreMvcIdempotentOptions()
+ {
+ SupportedMethods = new List
+ {
+ "POST",
+ "PUT",
+ "PATCH",
+ // "DELETE"
+ };
+ }
+}
diff --git a/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/AbpIdempotentActionFilter.cs b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/AbpIdempotentActionFilter.cs
new file mode 100644
index 000000000..33e5f6359
--- /dev/null
+++ b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/AbpIdempotentActionFilter.cs
@@ -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();
+ var options = context.GetRequiredService>().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();
+
+ 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>().Value;
+ if (!options.IsEnabled)
+ {
+ return false;
+ }
+
+ var mvcIdempotentOptions = context.GetRequiredService>().Value;
+ if (mvcIdempotentOptions.SupportedMethods.Any() &&
+ !mvcIdempotentOptions.SupportedMethods.Contains(context.HttpContext.Request.Method))
+ {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/README.md b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/README.md
new file mode 100644
index 000000000..e9347635c
--- /dev/null
+++ b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/README.md
@@ -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(options =>
+ {
+ // 例如: 对 DELETE 请求方法进行幂等校验
+ options.SupportedMethods.Add("DELETE");
+ });
+ }
+}
+```
+## 配置项说明
+
+## 其他
+
diff --git a/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/HttpResponseWrapper.cs b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/HttpResponseWrapper.cs
new file mode 100644
index 000000000..4e98dcdfe
--- /dev/null
+++ b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/HttpResponseWrapper.cs
@@ -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 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);
+ }
+ }
+ }
+}
diff --git a/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Wrapper/ExceptionHandling/AbpExceptionPageWrapResultFilter.cs b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Wrapper/ExceptionHandling/AbpExceptionPageWrapResultFilter.cs
index b011aa1a3..b95572d30 100644
--- a/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Wrapper/ExceptionHandling/AbpExceptionPageWrapResultFilter.cs
+++ b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Wrapper/ExceptionHandling/AbpExceptionPageWrapResultFilter.cs
@@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using System;
+using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp.AspNetCore.ExceptionHandling;
@@ -62,6 +63,7 @@ namespace LINGYUN.Abp.AspNetCore.Mvc.Wrapper.ExceptionHandling
}
else
{
+ var httpResponseWrapper = context.GetRequiredService();
var statusCodFinder = context.GetRequiredService();
var exceptionWrapHandler = context.GetRequiredService();
var exceptionWrapContext = new ExceptionWrapContext(
@@ -75,8 +77,19 @@ namespace LINGYUN.Abp.AspNetCore.Mvc.Wrapper.ExceptionHandling
exceptionWrapContext.ErrorInfo.Message,
exceptionWrapContext.ErrorInfo.Details));
- context.HttpContext.Response.Headers.Add(AbpHttpWrapConsts.AbpWrapResult, "true");
- context.HttpContext.Response.StatusCode = (int)wrapOptions.HttpStatusCode;
+ var wrapperHeaders = new Dictionary()
+ {
+ { AbpHttpWrapConsts.AbpWrapResult, "true" }
+ };
+ var responseWrapperContext = new HttpResponseWrapperContext(
+ context.HttpContext,
+ (int)wrapOptions.HttpStatusCode,
+ wrapperHeaders);
+
+ httpResponseWrapper.Wrap(responseWrapperContext);
+
+ //context.HttpContext.Response.Headers.Add(AbpHttpWrapConsts.AbpWrapResult, "true");
+ //context.HttpContext.Response.StatusCode = (int)wrapOptions.HttpStatusCode;
}
context.Exception = null; //Handled!
diff --git a/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Wrapper/ExceptionHandling/AbpExceptionWrapResultFilter.cs b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Wrapper/ExceptionHandling/AbpExceptionWrapResultFilter.cs
index 45d85708c..efe21eb9a 100644
--- a/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Wrapper/ExceptionHandling/AbpExceptionWrapResultFilter.cs
+++ b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Wrapper/ExceptionHandling/AbpExceptionWrapResultFilter.cs
@@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using System;
+using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp.AspNetCore.ExceptionHandling;
@@ -63,6 +64,7 @@ namespace LINGYUN.Abp.AspNetCore.Mvc.Wrapper.ExceptionHandling
}
else
{
+ var httpResponseWrapper = context.GetRequiredService();
var statusCodFinder = context.GetRequiredService();
var exceptionWrapHandler = context.GetRequiredService();
var exceptionWrapContext = new ExceptionWrapContext(
@@ -76,8 +78,19 @@ namespace LINGYUN.Abp.AspNetCore.Mvc.Wrapper.ExceptionHandling
exceptionWrapContext.ErrorInfo.Message,
exceptionWrapContext.ErrorInfo.Details));
- context.HttpContext.Response.Headers.Add(AbpHttpWrapConsts.AbpWrapResult, "true");
- context.HttpContext.Response.StatusCode = (int)wrapOptions.HttpStatusCode;
+ var wrapperHeaders = new Dictionary()
+ {
+ { AbpHttpWrapConsts.AbpWrapResult, "true" }
+ };
+ var responseWrapperContext = new HttpResponseWrapperContext(
+ context.HttpContext,
+ (int)wrapOptions.HttpStatusCode,
+ wrapperHeaders);
+
+ httpResponseWrapper.Wrap(responseWrapperContext);
+
+ //context.HttpContext.Response.Headers.Add(AbpHttpWrapConsts.AbpWrapResult, "true");
+ //context.HttpContext.Response.StatusCode = (int)wrapOptions.HttpStatusCode;
}
context.Exception = null; //Handled!
diff --git a/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Wrapper/Filters/AbpWrapResultFilter.cs b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Wrapper/Filters/AbpWrapResultFilter.cs
index 85ef9c8b6..9325b4414 100644
--- a/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Wrapper/Filters/AbpWrapResultFilter.cs
+++ b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Wrapper/Filters/AbpWrapResultFilter.cs
@@ -2,6 +2,7 @@
using LINGYUN.Abp.Wrapper;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Options;
+using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.DependencyInjection;
@@ -30,10 +31,23 @@ namespace LINGYUN.Abp.AspNetCore.Mvc.Wrapper.Filters
protected virtual Task HandleAndWrapResult(ResultExecutingContext context)
{
var options = context.GetRequiredService>().Value;
+ var httpResponseWrapper = context.GetRequiredService();
var actionResultWrapperFactory = context.GetRequiredService();
actionResultWrapperFactory.CreateFor(context).Wrap(context);
- context.HttpContext.Response.Headers.Add(AbpHttpWrapConsts.AbpWrapResult, "true");
- context.HttpContext.Response.StatusCode = (int)options.HttpStatusCode;
+
+ var wrapperHeaders = new Dictionary()
+ {
+ { AbpHttpWrapConsts.AbpWrapResult, "true" }
+ };
+ var responseWrapperContext = new HttpResponseWrapperContext(
+ context.HttpContext,
+ (int)options.HttpStatusCode,
+ wrapperHeaders);
+
+ httpResponseWrapper.Wrap(responseWrapperContext);
+
+ //context.HttpContext.Response.Headers.Add(AbpHttpWrapConsts.AbpWrapResult, "true");
+ //context.HttpContext.Response.StatusCode = (int)options.HttpStatusCode;
return Task.CompletedTask;
}
diff --git a/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Wrapper/HttpResponseWrapperContext.cs b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Wrapper/HttpResponseWrapperContext.cs
new file mode 100644
index 000000000..a7ad8c1cc
--- /dev/null
+++ b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Wrapper/HttpResponseWrapperContext.cs
@@ -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 HttpHeaders { get; }
+ public HttpResponseWrapperContext(
+ HttpContext httpContext,
+ int httpStatusCode,
+ IDictionary httpHeaders = null)
+ {
+ HttpContext = httpContext;
+ HttpStatusCode = httpStatusCode;
+ HttpHeaders = httpHeaders ?? new Dictionary();
+ }
+}
diff --git a/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Wrapper/IHttpResponseWrapper.cs b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Wrapper/IHttpResponseWrapper.cs
new file mode 100644
index 000000000..0729ddf6f
--- /dev/null
+++ b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Wrapper/IHttpResponseWrapper.cs
@@ -0,0 +1,7 @@
+using Microsoft.AspNetCore.Http;
+
+namespace LINGYUN.Abp.AspNetCore.Mvc.Wrapper;
+public interface IHttpResponseWrapper
+{
+ void Wrap(HttpResponseWrapperContext context);
+}