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