Browse Source

Merge pull request #806 from colinin/support-idempotent

feat(idempotent): add idempotent support
pull/808/head
yx lin 3 years ago
committed by GitHub
parent
commit
0b62c4fe0d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 21
      aspnet-core/LINGYUN.MicroService.Common.sln
  2. 3
      aspnet-core/modules/common/LINGYUN.Abp.Idempotent/FodyWeavers.xml
  3. 30
      aspnet-core/modules/common/LINGYUN.Abp.Idempotent/FodyWeavers.xsd
  4. 26
      aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN.Abp.Idempotent.csproj
  5. 36
      aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/AbpIdempotentModule.cs
  6. 15
      aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/AbpIdempotentOptions.cs
  7. 7
      aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IIdempotentChecker.cs
  8. 5
      aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IIdempotentDeniedHandler.cs
  9. 8
      aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IIdempotentKeyNormalizer.cs
  10. 48
      aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentAttribute.cs
  11. 24
      aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentCheckContext.cs
  12. 61
      aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentChecker.cs
  13. 23
      aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentDeniedContext.cs
  14. 30
      aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentDeniedException.cs
  15. 62
      aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentDeniedHandler.cs
  16. 7
      aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentErrorCodes.cs
  17. 41
      aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentInterceptor.cs
  18. 39
      aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentInterceptorRegistrar.cs
  19. 99
      aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentKeyNormalizer.cs
  20. 22
      aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/IdempotentKeyNormalizerContext.cs
  21. 8
      aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/Localization/IdempotentResource.cs
  22. 6
      aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/Localization/Resources/en.json
  23. 6
      aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN/Abp/Idempotent/Localization/Resources/zh-Hans.json
  24. 30
      aspnet-core/modules/common/LINGYUN.Abp.Idempotent/README.md
  25. 3
      aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/FodyWeavers.xml
  26. 30
      aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/FodyWeavers.xsd
  27. 16
      aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper.csproj
  28. 12
      aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/Wrapper/AbpAspNetCoreMvcIdempotentWrapperModule.cs
  29. 30
      aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/Wrapper/AbpIdempotentExceptionPageWrapResultFilter.cs
  30. 30
      aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/Wrapper/AbpIdempotentExceptionWrapResultFilter.cs
  31. 40
      aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/Wrapper/IdempotentHttpResponseWrapper.cs
  32. 17
      aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/README.md
  33. 3
      aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/FodyWeavers.xml
  34. 30
      aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/FodyWeavers.xsd
  35. 19
      aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.csproj
  36. 21
      aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/AbpAspNetCoreMvcIdempotentModule.cs
  37. 17
      aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/AbpAspNetCoreMvcIdempotentOptions.cs
  38. 77
      aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/LINGYUN/Abp/AspNetCore/Mvc/Idempotent/AbpIdempotentActionFilter.cs
  39. 24
      aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/README.md
  40. 29
      aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/HttpResponseWrapper.cs
  41. 17
      aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Wrapper/ExceptionHandling/AbpExceptionPageWrapResultFilter.cs
  42. 17
      aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Wrapper/ExceptionHandling/AbpExceptionWrapResultFilter.cs
  43. 18
      aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Wrapper/Filters/AbpWrapResultFilter.cs
  44. 19
      aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Wrapper/HttpResponseWrapperContext.cs
  45. 7
      aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/LINGYUN/Abp/AspNetCore/Mvc/Wrapper/IHttpResponseWrapper.cs

21
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}

3
aspnet-core/modules/common/LINGYUN.Abp.Idempotent/FodyWeavers.xml

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

30
aspnet-core/modules/common/LINGYUN.Abp.Idempotent/FodyWeavers.xsd

@ -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>

26
aspnet-core/modules/common/LINGYUN.Abp.Idempotent/LINGYUN.Abp.Idempotent.csproj

@ -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>

36
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<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<AbpIdempotentModule>();
});
Configure<AbpLocalizationOptions>(options =>
{
options.Resources.Add<IdempotentResource>()
.AddVirtualJson("/LINGYUN/Abp/Idempotent/Localization/Resources");
});
}
}

15
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;
}
}

7
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);
}

5
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);
}

8
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);
}

48
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
{
/// <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;
}
}

24
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<string, object?> ArgumentsDictionary { get; }
public IdempotentCheckContext(
Type target,
MethodInfo method,
string idempotentKey,
IReadOnlyDictionary<string, object?> argumentsDictionary)
{
Target = target;
Method = method;
IdempotentKey = idempotentKey;
ArgumentsDictionary = argumentsDictionary;
}
}

61
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<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);
}
}
}

23
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<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;
}
}

30
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)
{
}
}

62
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;
}
}

7
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";
}

41
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();
}
}

39
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<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);
}
}

99
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<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());
}
}

22
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<string, object?> ArgumentsDictionary { get; }
public IdempotentKeyNormalizerContext(
Type target,
MethodInfo method,
IReadOnlyDictionary<string, object?> argumentsDictionary)
{
Target = target;
Method = method;
ArgumentsDictionary = argumentsDictionary;
}
}

8
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
{
}

6
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!"
}
}

6
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": "无法在限定时间内重复提交多次请求, 请稍后再试!"
}
}

30
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<AbpIdempotentOptions>(options =>
{
// 全局启用幂等检查
options.IsEnabled = true;
// 默认每个接口提供30秒超时
options.DefaultTimeout = 30000;
// 幂等token名称, 通过HttpHeader传递
options.IdempotentTokenName = "X-With-Idempotent-Token";
// 幂等校验失败时Http响应代码
options.HttpStatusCode = 429;
});
}
}
```
## 配置项说明
## 其他

3
aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/FodyWeavers.xml

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

30
aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/FodyWeavers.xsd

@ -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>

16
aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper.csproj

@ -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>

12
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
{
}

30
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<IOptions<AbpIdempotentOptions>>().Value;
if (!context.HttpContext.Items.ContainsKey(options.IdempotentTokenName))
{
context.HttpContext.Items.Add(options.IdempotentTokenName, deniedException.IdempotentKey);
}
}
await base.HandleAndWrapException(context);
}
}

30
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<IOptions<AbpIdempotentOptions>>().Value;
if (!context.HttpContext.Items.ContainsKey(options.IdempotentTokenName))
{
context.HttpContext.Items.Add(options.IdempotentTokenName, deniedException.IdempotentKey);
}
}
await base.HandleAndWrapException(context);
}
}

40
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<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);
}
}

17
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
{
}
```
## 配置项说明
## 其他

3
aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/FodyWeavers.xml

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

30
aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/FodyWeavers.xsd

@ -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>

19
aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Idempotent/LINGYUN.Abp.AspNetCore.Mvc.Idempotent.csproj

@ -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>

21
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<MvcOptions>(options =>
{
options.Filters.AddService(typeof(AbpIdempotentActionFilter));
});
}
}

17
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<string> SupportedMethods { get; }
public AbpAspNetCoreMvcIdempotentOptions()
{
SupportedMethods = new List<string>
{
"POST",
"PUT",
"PATCH",
// "DELETE"
};
}
}

77
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<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;
}
}

24
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<AbpMvcIdempotentOptions>(options =>
{
// 例如: 对 DELETE 请求方法进行幂等校验
options.SupportedMethods.Add("DELETE");
});
}
}
```
## 配置项说明
## 其他

29
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<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);
}
}
}
}

17
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<IHttpResponseWrapper>();
var statusCodFinder = context.GetRequiredService<IHttpExceptionStatusCodeFinder>();
var exceptionWrapHandler = context.GetRequiredService<IExceptionWrapHandlerFactory>();
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<string, string>()
{
{ 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!

17
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<IHttpResponseWrapper>();
var statusCodFinder = context.GetRequiredService<IHttpExceptionStatusCodeFinder>();
var exceptionWrapHandler = context.GetRequiredService<IExceptionWrapHandlerFactory>();
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<string, string>()
{
{ 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!

18
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<IOptions<AbpWrapperOptions>>().Value;
var httpResponseWrapper = context.GetRequiredService<IHttpResponseWrapper>();
var actionResultWrapperFactory = context.GetRequiredService<IActionResultWrapperFactory>();
actionResultWrapperFactory.CreateFor(context).Wrap(context);
context.HttpContext.Response.Headers.Add(AbpHttpWrapConsts.AbpWrapResult, "true");
context.HttpContext.Response.StatusCode = (int)options.HttpStatusCode;
var wrapperHeaders = new Dictionary<string, string>()
{
{ 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;
}

19
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<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>();
}
}

7
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);
}
Loading…
Cancel
Save