如何优雅地让 ASP.NET Core 支持异步模型验证
前言
在ASP.NET Core官方仓库中有个一直很受关注的问题Please reconsider allowing async model validation。FluentValidation
的作者也非常关心这个问题,因为FluentValidation
内置异步验证功能,但是由于MVC内置的模型验证管道是同步的,使可兼容的功能和集成都受到严重阻碍。每次MVC修改验证功能都有可能导致集成出问题。
不仅如此,FluentValidation
虽然是一个优秀的对象验证库,但其实现方式还是导致了一些与ASP.NET Core集成上的问题。例如他的验证消息本地化功能是通过全局对象管理的,这导致想要把消息本地化和依赖注入集成在一起的方式非常别扭。
最近在问题的评论中发现了一个复制并修改原始模型绑定系统得到的异步模型验证服务。这个修改版看上去还是比较直观的,使用自定义模型绑定器替换内置绑定器,然后用自定义的异步验证服务替换内置验证服务。同时提供了配套的异步验证特性和异步验证器助手。
但是使用过程中还是发现了一些不尽如人意的地方:
- 这个库直接面向ASP.NET Core框架,如果只想使用基本的异步验证功能,会被强制依赖无关内容。
- 异步验证特性的实现方式没有完全还原同步特性的使用方式,使用抽象方法强制要求实现返回
ValidationResult
的验证方法。内置特性只需要重写返回bool
或ValidationResult
的任意一个方法即可,对于大多数简单的验证需求而言返回bool
的版本完全足够。因此易用性不足。 - 如果为模型标记了异步验证特性且使用控制器提供的手动验证模型的方法,会导致手动验证发生异常。这导致在代码上调整模型后重新验证的功能不再可用。同时也没有提供对应的手动重新异步验证模型的方法。
- 模型验证是MVC的内部功能,无法在类似最小API的场合使用。内置的模型验证器又不会自动递归验证,反过来又导致可能需要换用
FluentValidation
。之前说过FluentValidation
和MVC集成是有瑕疵的。如果这么做,又可能出现同时使用多种验证方案使管理成本上升的情况。
总之就是无论如何都感觉别扭。因此笔者基于原始的改造重写了一个新版以期解决以上问题。
新书宣传
有关新书的更多介绍欢迎查看《C#与.NET6 开发从入门到实践》上市,作者亲自来打广告了!
正文
异步模型验证
解决第一个问题,首先就是分离基本验证器和MVC集成的类到不同项目,因此基本验证器的相关内容放到CoreDX.Extensions.Validation
中,MVC相关的内容放到CoreDX.Extensions.AspNetCore.Validation
中。
解决第二个问题则是简单的根据原版验证特性重新按套路添加异步方法。
AsyncValidationAttribute
public abstract class AsyncValidationAttribute : ValidationAttribute
{
private volatile bool _hasBaseIsValidAsync;
protected override sealed ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
throw new InvalidOperationException("Async validation called synchronously.");
}
public override sealed bool IsValid(object? value)
{
throw new InvalidOperationException("Async validation called synchronously.");
}
public virtual async ValueTask<bool> IsValidAsync(object? value, CancellationToken cancellationToken = default)
{
if (!_hasBaseIsValidAsync)
{
// track that this method overload has not been overridden.
_hasBaseIsValidAsync = true;
}
// call overridden method.
// The IsValid method without a validationContext predates the one accepting the context.
// This is theoretically unreachable through normal use cases.
// Instead, the overload using validationContext should be called.
return await IsValidAsync(value, null!, cancellationToken: cancellationToken) == ValidationResult.Success;
}
protected virtual async ValueTask<ValidationResult?> IsValidAsync(object? value, ValidationContext validationContext, CancellationToken cancellationToken = default)
{
if (_hasBaseIsValidAsync)
{
// this means neither of the IsValidAsync methods has been overridden, throw.
throw new NotImplementedException("IsValidAsync(object value, CancellationToken cancellationToken) has not been implemented by this class. The preferred entry point is GetValidationResultAsync() and classes should override IsValidAsync(object value, ValidationContext context, CancellationToken cancellationToken).");
}
// call overridden method.
return await IsValidAsync(value, cancellationToken)
? ValidationResult.Success
: CreateFailedValidationResult(validationContext);
}
public async ValueTask<ValidationResult?> GetValidationResultAsync(object? value, ValidationContext validationContext, CancellationToken cancellationToken = default)
{
if (validationContext == null)
{
throw new ArgumentNullException(nameof(validationContext));
}
ValidationResult? result = await IsValidAsync(value, validationContext, cancellationToken);
// If validation fails, we want to ensure we have a ValidationResult that guarantees it has an ErrorMessage
if (result != null)
{
if (string.IsNullOrEmpty(result.ErrorMessage))
{
var errorMessage = FormatErrorMessage(validationContext.DisplayName);
result = new ValidationResult(errorMessage, result?.MemberNames);
}
}
return result;
}
public async ValueTask ValidateAsync(object? value, string name, CancellationToken cancellationToken = default)
{
if (!(await IsValidAsync(value, cancellationToken)))
{
throw new ValidationException(FormatErrorMessage(name), this, value);
}
}
public async ValueTask ValidateAsync(object? value, ValidationContext validationContext, CancellationToken cancellationToken = default)
{
if (validationContext == null)
{
throw new ArgumentNullException(nameof(validationContext));
}
ValidationResult? result = await GetValidationResultAsync(value, validationContext, cancellationToken: cancellationToken);
if (result != null)
{
// Convenience -- if implementation did not fill in an error message,
throw new ValidationException(result, this, value);
}
}
}
基本思路就是继承原版验证特性,密封原版验证方法抛出异常,添加对应的异步验证方法。同时笔者也添加了对应的自定义异步验证特性CustomAsyncValidationAttribute
,代码较多,基本思路也没变,感兴趣的可以查看仓库代码CustomAsyncValidationAttribute.cs。当然用于直接在类型上实现验证功能的接口IValidatableObject
也添加了对应的异步版本IAsyncValidatableObject
,做戏做全套嘛。
然后就是实现对应的异步验证器,方法也非常简单,复制原版验证器的代码,把所有需要的方法改为异步方法,在内部添加对普通同步验证特性和异步验证特性的分别处理,最后加上自定义新接口的支持部分就大功告成了。详细代码可以查看AsyncValidator.cs。
根据命名规范,如果一个类型是明确的异步专用类型,内部的方法可以不使用Async
后缀,例如Task
类中的方法,因此笔者使用没有后缀的方法名,也方便修改使用验证器的代码,只需要修改类型便可。
解决第三个问题的关键是IModelValidator
接口的实现,评论提供的代码直接抛出异常,非常粗暴,因此这里直接修改成返回空白结果集即可。为了配合异步验证,添加一个新接口IAsyncModelValidator
实现IModelValidator
并添加对应的异步方法,实现同步接口的原因是为了能顺利注册到MVC框架中。这一套下来需要重新实现十个左右的服务,代码量较大,和原版服务的区别也仅限于同步和异步,因此不再展示,感兴趣的朋友可以查看仓库代码CoreDX.Extensions.AspNetCore.Validation。
其中值得关注的一点是,复刻版DefaultComplexObjectValidationStrategy
需要使用ModelMetadata
的内部成员,只能反射。好在 .NET8.0 添加了一个专门用来简化静态反射的功能,可以用起来简化代码提高性能。
#if NET8_0_OR_GREATER
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = nameof(ThrowIfRecordTypeHasValidationOnProperties))]
internal extern static void ThrowIfRecordTypeHasValidationOnProperties(ModelMetadata modelMetadata);
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_BoundProperties")]
internal extern static IReadOnlyList<ModelMetadata> GetBoundProperties(ModelMetadata modelMetadata);
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_BoundConstructorParameterMapping")]
internal extern static IReadOnlyDictionary<ModelMetadata, ModelMetadata> GetBoundConstructorParameterMapping(ModelMetadata modelMetadata);
#endif
最后在为服务注册和手动重新验证添加相应的辅助方法,即可正常使用。此时如果继续使用同步的手动模型验证,虽然不会发生异常,但是异步验证特性也会被忽略。
MVC异步模型验证服务注册扩展
namespace Microsoft.AspNetCore.Mvc;
public static class AsyncValidationExtension
{
public static IMvcBuilder AddAsyncDataAnnotations(this IMvcBuilder builder)
{
builder.Services.AddSingleton<IConfigureOptions<MvcOptions>, ConfigureMvcOptionsSetup>();
builder.Services.AddSingleton<ParameterBinder, AsyncParamterBinder>();
builder.Services.TryAddSingleton<IAsyncObjectModelValidator>(s =>
{
var options = s.GetRequiredService<IOptions<MvcOptions>>().Value;
var metadataProvider = s.GetRequiredService<IModelMetadataProvider>();
return new DefaultAsyncObjecValidator(metadataProvider, options.ModelValidatorProviders, options);
});
return builder;
}
public static IMvcCoreBuilder AddAsyncDataAnnotations(this IMvcCoreBuilder builder)
{
builder.Services.AddSingleton<IConfigureOptions<MvcOptions>, ConfigureMvcOptionsSetup>();
builder.Services.AddSingleton<ParameterBinder, AsyncParamterBinder>();
builder.Services.TryAddSingleton<IAsyncObjectModelValidator>(s =>
{
var options = s.GetRequiredService<IOptions<MvcOptions>>().Value;
var cache = s.GetRequiredService<ValidatorCache>();
var metadataProvider = s.GetRequiredService<IModelMetadataProvider>();
return new DefaultAsyncObjecValidator(metadataProvider, options.ModelValidatorProviders, options);
});
return builder;
}
internal sealed class ConfigureMvcOptionsSetup : IConfigureOptions<MvcOptions>
{
private readonly IStringLocalizerFactory? _stringLocalizerFactory;
private readonly IValidationAttributeAdapterProvider _validationAttributeAdapterProvider;
private readonly IOptions<MvcDataAnnotationsLocalizationOptions> _dataAnnotationLocalizationOptions;
public ConfigureMvcOptionsSetup(
IValidationAttributeAdapterProvider validationAttributeAdapterProvider,
IOptions<MvcDataAnnotationsLocalizationOptions> dataAnnotationLocalizationOptions)
{
ArgumentNullException.ThrowIfNull(validationAttributeAdapterProvider);
ArgumentNullException.ThrowIfNull(dataAnnotationLocalizationOptions);
_validationAttributeAdapterProvider = validationAttributeAdapterProvider;
_dataAnnotationLocalizationOptions = dataAnnotationLocalizationOptions;
}
public ConfigureMvcOptionsSetup(
IValidationAttributeAdapterProvider validationAttributeAdapterProvider,
IOptions<MvcDataAnnotationsLocalizationOptions> dataAnnotationLocalizationOptions,
IStringLocalizerFactory stringLocalizerFactory)
: this(validationAttributeAdapterProvider, dataAnnotationLocalizationOptions)
{
_stringLocalizerFactory = stringLocalizerFactory;
}
public void Configure(MvcOptions options)
{
ArgumentNullException.ThrowIfNull(options);
options.ModelValidatorProviders.Insert(0, new AsyncDataAnnotationsModelValidatorProvider(
_validationAttributeAdapterProvider,
_dataAnnotationLocalizationOptions,
_stringLocalizerFactory));
options.ModelValidatorProviders.Insert(0, new DefaultAsyncModelValidatorProvider());
}
}
}
public static class AsyncValidatiorExtension
{
public static Task<bool> TryValidateModelAsync(
this ControllerBase controller,
object model,
CancellationToken cancellationToken = default)
{
return TryValidateModelAsync(controller, model, null, cancellationToken);
}
public static async Task<bool> TryValidateModelAsync(
this ControllerBase controller,
object model,
string? prefix,
CancellationToken cancellationToken = default)
{
await TryValidateModelAsync(
controller.ControllerContext,
model: model,
prefix: prefix ?? string.Empty,
cancellationToken);
return controller.ModelState.IsValid;
}
public static Task<bool> TryValidateModelAsync(
this PageModel page,
object model,
CancellationToken cancellationToken = default)
{
return TryValidateModelAsync(page, model, null, cancellationToken);
}
public static async Task<bool> TryValidateModelAsync(
this PageModel page,
object model,
string? prefix,
CancellationToken cancellationToken = default)
{
await TryValidateModelAsync(
page.PageContext,
model: model,
prefix: prefix ?? string.Empty,
cancellationToken);
return page.ModelState.IsValid;
}
private static Task TryValidateModelAsync(
ActionContext context,
object model,
string? prefix,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(model);
var validator = context.HttpContext.RequestServices.GetRequiredService<IAsyncObjectModelValidator>();
return validator.ValidateAsync(
context,
validationState: null,
prefix: prefix ?? string.Empty,
model: model,
cancellationToken);
}
}
对象图递归验证
终于来到了最后一个问题,这个其实才是最困难的。Nuget上有一些别人写的递归验证器,但是笔者查看过代码和issues后发现这些验证器都有这样那样的问题。首先这些验证器都不支持异步验证,而且笔者有自己的异步验证基础类,就算这些验证器支持异步验证也和笔者提供的类型不兼容;其次这些验证器的API形状和内部运行机制没有完全对齐官方版本,这也意味着手动拆包对象后用官方验证器出来的结果可能对不上;再次这些验证器都存在没有解决的issue;作者也基本弃坑了。最后还是只能自己写一个。
在 Blazor 中曾经有一个实验性的递归表单模型验证器。但是这个验证器首先只能在 Blazor 中使用,其次需要一个专用特性表示模型的某个属性是复杂对象类型,需要继续深入验证他的内部属性,这种实现方式又会导致如果某个属性本身不需要验证,但内部的其他属性需要验证,就要链式地为整个属性链全部标记这个特性。这种半自动的用法还是不太方便。如果这个类型的源代码不归自己管无法修改,那就彻底没戏了。
在参考了这个已经不存在的表单验证器后,笔者实现了第一版对象图验证器,但是调试时发现一个极其麻烦的问题,循环引用对象的自动短路无论如何表现的都非常奇怪。要么是某些对象没有被验证,要么是某些对象被验证两次,要么干脆直接栈溢出,怎么调整短路条件都不对。而且此时还只是实现了同步验证,如果要再加上异步验证,一定会变成一个更麻烦的问题。
多次尝试无果后只能重新整理思路和代码。皇天不负有心人,在将近半个月的摸索后终于灵光一闪,想通了问题的关键。画龙点睛的一行代码写完后,一切终于如预期一样工作,甚至验证结果的出现顺序都和预想的完全一致。完成这个全自动对象图验证器后,最小API之类的其他场景也终于可以像MVC一样用验证特性验证整个对象模型了。并且这个验证器位于基础功能包,不依赖ASP.NET Core,可以在任意 .NET 项目中使用。
整个验证类代码较多,超过两千行,API形状和基础行为和官方验证器保持一致(笔者查看过的其他验证器代码基本在500行以内,几乎无法避免存在缺陷),因此只展示一下关键部分,完整代码请查看仓库ObjectGraphValidation。
private static bool TryValidateObjectRecursive(
object instance,
ValidationContext validationContext,
ValidationResultStore? validationResults,
AsyncValidationBehavior asyncValidationBehavior,
bool validateAllProperties,
Func<Type, bool>? predicate,
bool throwOnFirstError)
{
if (instance == null)
{
throw new ArgumentNullException(nameof(instance));
}
if (validationContext == null)
{
throw new ArgumentNullException(nameof(validationContext));
}
if (instance != validationContext.ObjectInstance)
{
throw new ArgumentException("The instance provided must match the ObjectInstance on the ValidationContext supplied.", nameof(instance));
}
// 这里就是关键,只要在这里记录访问历史,一切都会好起来的
if (!(validationContext.Items.TryGetValue(_validatedObjectsKey, out var item)
&& item is HashSet<object> visited
&& visited.Add(instance)))
{
return true;
}
bool isValid = true;
bool breakOnFirstError = (validationResults == null);
foreach (ValidationError err in GetObjectValidationErrors(
instance,
validationContext,
asyncValidationBehavior,
validateAllProperties,
breakOnFirstError))
{
if (throwOnFirstError) err.ThrowValidationException();
isValid = false;
if (breakOnFirstError) break;
TransferErrorToResult(validationResults!, err);
}
if (!isValid && breakOnFirstError) return isValid;
var propertyObjectsAreValid = TryValidatePropertyObjects(
instance,
validationContext,
validationResults,
asyncValidationBehavior,
validateAllProperties,
predicate,
throwOnFirstError);
if (isValid && !propertyObjectsAreValid) isValid = false;
return isValid;
}
这个方法是一切递归的开始,因此对象的访问记录也应该从这里开始,只是当时被 Blazor 的代码干扰了一下,老想着在别处处理这个问题被坑了半个月。参数里的委托是一个自定义判定条件,用于决定是否要验证这个类型的对象,如果你很清楚某个类型内部不会再有验证特性,可以在这里阻止无用的递归。笔者已经在内部排除了大部分已知的无需深入验证的内置类型,例如int
、List<T>
之类的基本类型和内部不会再有其他直接或间接存在验证特性标记的属性的复杂类型。这里的List<T>
中不会继续深入验证是指这个类型本身的属性,例如Count
,如果T
类型是可能有验证标记的类型是会正常验证的,如果直接继承List<T>
再添加自己的新属性也可以正常验证。
对象图的验证结果可能来自深层对象,因此需要一种方法来保留这种结构信息以提供更有价值的验证结果。此处笔者参考Blazor的验证器做了一个更符合这里的需求的版本。如果要重写值类型的相等性判断,则需要谨慎,否则可能出现问题。
public sealed class FieldIdentifier : IEquatable<FieldIdentifier>
{
private static readonly object TopLevelObjectFaker = new();
public static FieldIdentifier GetFakeTopLevelObjectIdentifier(string fieldName)
{
return new(TopLevelObjectFaker, fieldName, null);
}
public FieldIdentifier(object model, string fieldName, FieldIdentifier? modelOwner)
{
Model = model ?? throw new ArgumentNullException(nameof(model));
CheckTopLevelObjectFaker(model, modelOwner);
// Note that we do allow an empty string. This is used by some validation systems
// as a place to store object-level (not per-property) messages.
FieldName = fieldName ?? throw new ArgumentNullException(nameof(fieldName));
ModelOwner = modelOwner;
}
public FieldIdentifier(object model, int enumerableElementIndex, FieldIdentifier? modelOwner)
{
Model = model ?? throw new ArgumentNullException(nameof(model));
CheckTopLevelObjectFaker(model, modelOwner);
if (enumerableElementIndex < 0)
{
throw new ArgumentOutOfRangeException(nameof(enumerableElementIndex), "The index must be great than or equals 0.");
}
EnumerableElementIndex = enumerableElementIndex;
ModelOwner = modelOwner;
}
private static void CheckTopLevelObjectFaker(object model, FieldIdentifier? modelOwner)
{
if (model == TopLevelObjectFaker && modelOwner is not null)
{
throw new ArgumentException($"{nameof(modelOwner)} must be null when {nameof(model)} is {nameof(TopLevelObjectFaker)}", nameof(modelOwner));
}
}
public object Model { get; }
public bool ModelIsCopiedInstanceOfValueType => Model.GetType().IsValueType;
public bool ModelIsTopLevelFakeObject => Model == TopLevelObjectFaker;
public string? FieldName { get; }
public int? EnumerableElementIndex { get; }
public FieldIdentifier? ModelOwner { get; }
/// <inheritdoc />
public override int GetHashCode()
{
// We want to compare Model instances by reference. RuntimeHelpers.GetHashCode returns identical hashes for equal object references (ignoring any `Equals`/`GetHashCode` overrides) which is what we want.
var modelHash = RuntimeHelpers.GetHashCode(Model);
var fieldHash = FieldName is null ? 0 : StringComparer.Ordinal.GetHashCode(FieldName);
var indexHash = EnumerableElementIndex ?? 0;
var ownerHash = RuntimeHelpers.GetHashCode(ModelOwner);
return (modelHash, fieldHash, indexHash, ownerHash).GetHashCode();
}
/// <inheritdoc />
public override bool Equals(object? obj)
=> obj is FieldIdentifier otherIdentifier
&& Equals(otherIdentifier);
/// <inheritdoc />
public bool Equals(FieldIdentifier? otherIdentifier)
{
return (ReferenceEquals(otherIdentifier?.Model, Model) || Equals(otherIdentifier?.Model, Model))
&& string.Equals(otherIdentifier?.FieldName, FieldName, StringComparison.Ordinal)
&& Nullable.Equals(otherIdentifier?.EnumerableElementIndex, EnumerableElementIndex)
&& ReferenceEquals(otherIdentifier?.ModelOwner, ModelOwner);
}
/// <inheritdoc/>
public static bool operator ==(FieldIdentifier? left, FieldIdentifier? right)
{
if (left is not null) return left.Equals(right);
if (right is not null) return right.Equals(left);
return Equals(left, right);
}
/// <inheritdoc/>
public static bool operator !=(FieldIdentifier? left, FieldIdentifier? right) => !(left == right);
/// <inheritdoc/>
public override string? ToString()
{
if (ModelIsTopLevelFakeObject) return FieldName;
var sb = new StringBuilder();
var fieldIdentifier = this;
var chainHasTopLevelFaker = false;
do
{
sb.Insert(0, fieldIdentifier.FieldName is not null ? $".{fieldIdentifier.FieldName}" : $"[{fieldIdentifier.EnumerableElementIndex}]");
if (chainHasTopLevelFaker is false && fieldIdentifier.ModelIsTopLevelFakeObject) chainHasTopLevelFaker = true;
fieldIdentifier = fieldIdentifier.ModelOwner;
} while (fieldIdentifier != null && !fieldIdentifier.ModelIsTopLevelFakeObject);
if (fieldIdentifier is null && !chainHasTopLevelFaker) sb.Insert(0, "$");
else if (fieldIdentifier is { ModelIsTopLevelFakeObject: true }) sb.Insert(0, fieldIdentifier.FieldName);
return sb.ToString();
}
}
这里有一个专门用来表示函数参数名或者本地变量名的特殊对象,这个特殊对象只能是根对象,如果没有这个特殊对象做根,根对象会用$
符号表示。对于值类型的对象,可以通过属性知道这里保存的对象是复制版,修改这里保存的对象可能无法反馈到原始数据。Blazor则是简单粗暴地在发现传入对象是结构体时直接抛出异常。
最小API自动验证
对于.NET6.0 来说,自己在API端点用对象图验证器验证一下就可以了,也只能这么做。但是 .NET7.0 为最小API也添加了过滤器功能,这使得模拟MVC的自动验证机制成为可能。本来这篇文章应该在将近半个月前发布的,就是因为偶然看见 .NET7.0 增加了最小API过滤器,笔者临时决定一不做二不休,把自动验证的管道过滤器也一并做完再发。
MinimalApis.Extensions
是官方文档引用的过滤器应用示例,因此也是下载量比较大的扩展库,另外也有一个基于FluentValidation
的。但是笔者的验证功能已经完全自己实现,想要接入进去也不可能了,而且这些库都没有还原出MVC的使用体验。既然如此,干脆完全重写一个更接近MVC体验的算了。
在此,笔者规划实现以下MVC功能:
- 自动验证绑定参数:包括复杂对象、简单值和多参数。
- 可选的自动验证错误响应:普通控制器在验证失败时会继续执行控制器动作,而API控制器会自动返回验证错误。
- 手动通过代码获取验证结果。
- 手动重新验证参数。
- 本地化验证错误信息。
可能是这个野心太大,本来以为两三天能搞定的,结果在被无尽的bug折磨中将近半个月就过去了。好在这些时间没有白花,预定目标全部实现。
类似MVC,这个验证过滤器也是基于模型元数据的,但是是笔者自己写的独立版本。因为MVC的元数据系统太复杂,而且最小API本来就是为不想用MVC这种复杂框架的人准备的精简版功能,现在回过去依赖MVC的功能也不好。这个元数据系统的核心是保存绑定参数的名字,附加的验证特性和本地化特性等信息。
端点参数验证元数据
internal sealed class EndpointBindingParametersValidationMetadata : IReadOnlyDictionary<string, ParameterValidationMetadata>
{
private readonly MethodInfo _endpointMethod;
private readonly IReadOnlyDictionary<string, ParameterValidationMetadata> _metadatas;
public MethodInfo EndpointMethod => _endpointMethod;
public EndpointBindingParametersValidationMetadata(MethodInfo endpointMethod, params IEnumerable<ParameterValidationMetadata> metadatas)
{
ArgumentNullException.ThrowIfNull(endpointMethod);
Dictionary<string, ParameterValidationMetadata> tempMetadatas = [];
HashSet<string> names = [];
foreach (var metadata in metadatas)
{
if (!names.Add(metadata.ParameterName)) throw new ArgumentException("metadata's parameter name must be unique.", nameof(metadatas));
tempMetadatas.Add(metadata.ParameterName, metadata);
}
_metadatas = tempMetadatas.AsReadOnly();
_endpointMethod = endpointMethod;
}
public async ValueTask<Dictionary<string, ValidationResultStore>?> ValidateAsync(IDictionary<string, object?> arguments, CancellationToken cancellationToken = default)
{
Dictionary<string, ValidationResultStore> result = [];
foreach (var argument in arguments)
{
if (!_metadatas.TryGetValue(argument.Key, out var metadata))
{
throw new InvalidOperationException($"Parameter named {argument.Key} does not exist.");
}
var argumentResults = await metadata.ValidateAsync(argument.Value, cancellationToken);
if (argumentResults is not null) result.TryAdd(metadata.ParameterName, argumentResults);
}
return result.Count > 0 ? result : null;
}
public IEnumerable<string> Keys => _metadatas.Keys;
public IEnumerable<ParameterValidationMetadata> Values => _metadatas.Values;
public int Count => _metadatas.Count;
public ParameterValidationMetadata this[string key] => _metadatas[key];
public bool ContainsKey(string key) => _metadatas.ContainsKey(key);
public bool TryGetValue(
string key,
[MaybeNullWhen(false)] out ParameterValidationMetadata value)
=> _metadatas.TryGetValue(key, out value);
public IEnumerator<KeyValuePair<string, ParameterValidationMetadata>> GetEnumerator() => _metadatas.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
internal sealed class ParameterValidationMetadata
{
private ParameterInfo _parameterInfo;
private string? _displayName;
private RequiredAttribute? _requiredAttribute;
private ImmutableList<ValidationAttribute> _otherValidationAttributes;
public ParameterValidationMetadata(ParameterInfo parameterInfo)
{
_parameterInfo = parameterInfo ?? throw new ArgumentNullException(nameof(parameterInfo));
if (string.IsNullOrEmpty(parameterInfo.Name)) throw new ArgumentException("Parameter must be have name.", nameof(parameterInfo));
_displayName = parameterInfo.GetCustomAttribute<DisplayAttribute>()?.Name
?? parameterInfo.GetCustomAttribute<DisplayNameAttribute>()?.DisplayName;
_requiredAttribute = parameterInfo.GetCustomAttribute<RequiredAttribute>();
_otherValidationAttributes = parameterInfo
.GetCustomAttributes<ValidationAttribute>()
.Where(attr => attr is not RequiredAttribute)
.ToImmutableList();
}
public string ParameterName => _parameterInfo.Name!;
public string? DisplayName => _displayName;
public ParameterInfo Parameter => _parameterInfo;
public async ValueTask<ValidationResultStore?> ValidateAsync(object? argument, CancellationToken cancellationToken = default)
{
if (argument is not null && !argument.GetType().IsAssignableTo(_parameterInfo.ParameterType))
{
throw new InvalidCastException($"Object cannot assign to {ParameterName} of type {_parameterInfo.ParameterType}.");
}
var topName = ParameterName ?? $"<argumentSelf({argument?.GetType()?.Name})>";
ValidationResultStore resultStore = new();
List<ValidationResult> results = [];
var validationContext = new ValidationContext(argument ?? new())
{
MemberName = ParameterName
};
if (DisplayName is not null) validationContext.DisplayName = DisplayName;
// 验证位于参数上的特性
if (argument is null && _requiredAttribute is not null)
{
var result = _requiredAttribute.GetValidationResult(argument, validationContext)!;
result = new LocalizableValidationResult(result.ErrorMessage, result.MemberNames, _requiredAttribute, validationContext);
results.Add(result);
}
if (argument is not null)
{
foreach (var validation in _otherValidationAttributes)
{
if (validation is AsyncValidationAttribute asyncValidation)
{
var result = await asyncValidation.GetValidationResultAsync(argument, validationContext, cancellationToken);
if (result != ValidationResult.Success)
{
result = new LocalizableValidationResult(result!.ErrorMessage, result.MemberNames, validation, validationContext);
results.Add(result);
}
}
else
{
var result = validation.GetValidationResult(argument, validationContext);
if (result != ValidationResult.Success)
{
result = new LocalizableValidationResult(result!.ErrorMessage, result.MemberNames, validation, validationContext);
results.Add(result);
}
}
}
// 验证对象内部的特性
await ObjectGraphValidator.TryValidateObjectAsync(
argument,
new ValidationContext(argument),
resultStore,
true,
static type => !IsRequestDelegateFactorySpecialBoundType(type),
topName,
cancellationToken);
}
if (results.Count > 0)
{
var id = FieldIdentifier.GetFakeTopLevelObjectIdentifier(topName);
resultStore.Add(id, results);
}
return resultStore.Any() ? resultStore : null;
}
}
}
internal static bool IsRequestDelegateFactorySpecialBoundType(Type type) =>
type.IsAssignableTo(typeof(HttpContext))
|| type.IsAssignableTo(typeof(HttpRequest))
|| type.IsAssignableTo(typeof(HttpResponse))
|| type.IsAssignableTo(typeof(ClaimsPrincipal))
|| type.IsAssignableTo(typeof(CancellationToken))
|| type.IsAssignableTo(typeof(IFormFile))
|| type.IsAssignableTo(typeof(IEnumerable<IFormFile>))
|| type.IsAssignableTo(typeof(Stream))
|| type.IsAssignableTo(typeof(PipeReader));
端点过滤器工厂
需要在端点构建阶段使用端点信息生成参数验证元数据并保存到端点元数据备用,再通过元数据生成结果决定是否需要添加验证过滤器,端点过滤器工厂刚好能实现这个目的。
public static EndpointParameterDataAnnotationsRouteHandlerBuilder<TBuilder> AddEndpointParameterDataAnnotations<TBuilder>(
this TBuilder endpointConvention)
where TBuilder : IEndpointConventionBuilder
{
endpointConvention.Add(static endpointBuilder =>
{
var loggerFactory = endpointBuilder.ApplicationServices.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger(_filterLoggerName);
// 排除MVC端点
if (endpointBuilder.Metadata.Any(static md => md is ActionDescriptor))
{
logger.LogDebug("Cannot add parameter data annotations validation filter to MVC controller or Razor pages endpoint {actionName}.", endpointBuilder.DisplayName);
return;
}
// 检查重复注册自动验证过滤器
if (endpointBuilder.Metadata.Any(static md => md is EndpointBindingParametersValidationMetadata))
{
logger.LogDebug("Already has a parameter data annotations validation filter on endpoint {actionName}.", endpointBuilder.DisplayName);
return;
}
if (endpointBuilder.Metadata.Any(static md => md is EndpointBindingParametersValidationMetadataMark))
{
logger.LogDebug("Already called method AddEndpointParameterDataAnnotations before on endpoint {actionName}.", endpointBuilder.DisplayName);
return;
}
// 标记自动验证过滤器已经注册
endpointBuilder.Metadata.Add(new EndpointBindingParametersValidationMetadataMark());
endpointBuilder.FilterFactories.Add((filterFactoryContext, next) =>
{
var loggerFactory = filterFactoryContext.ApplicationServices.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger(_filterLoggerName);
var parameters = filterFactoryContext.MethodInfo.GetParameters();
// 查找绑定参数,记录索引备用
var isServicePredicate = filterFactoryContext.ApplicationServices.GetService<IServiceProviderIsService>();
List<int> bindingParameterIndexs = new(parameters.Length);
for (int i = 0; i < parameters.Length; i++)
{
ParameterInfo? parameter = parameters[i];
if (IsRequestDelegateFactorySpecialBoundType(parameter.ParameterType)) continue;
if (parameter.GetCustomAttribute<FromServicesAttribute>() is not null) continue;
#if NET8_0_OR_GREATER
if (parameter.GetCustomAttribute<FromKeyedServicesAttribute>() is not null) continue;
#endif
if (isServicePredicate?.IsService(parameter.ParameterType) is true) continue;
bindingParameterIndexs.Add(i);
}
if (bindingParameterIndexs.Count is 0)
{
logger.LogDebug("Route handler method '{methodName}' does not contain any validatable parameters, skipping adding validation filter.", filterFactoryContext.MethodInfo.Name);
}
// 构建参数模型验证元数据添加到端点元数据集合
EndpointBindingParametersValidationMetadata? validationMetadata;
try
{
List<ParameterValidationMetadata> bindingParameters = new(bindingParameterIndexs.Count);
foreach (var argumentIndex in bindingParameterIndexs)
{
bindingParameters.Add(new(parameters[argumentIndex]));
}
validationMetadata = new(filterFactoryContext.MethodInfo, bindingParameters);
}
catch (Exception e)
{
validationMetadata = null;
logger.LogError(e, "Build parameter validation metadate failed for route handler method '{methodName}', skipping adding validation filter.", filterFactoryContext.MethodInfo.Name);
}
if (validationMetadata?.Any() is not true) return invocationContext => next(invocationContext);
endpointBuilder.Metadata.Add(validationMetadata);
// 一切顺利,注册验证过滤器
return async invocationContext =>
{
var endpoint = invocationContext.HttpContext.GetEndpoint();
var metadata = endpoint?.Metadata
.FirstOrDefault(static md => md is EndpointBindingParametersValidationMetadata) as EndpointBindingParametersValidationMetadata;
if (metadata is null) return await next(invocationContext);
Dictionary<string, object?> arguments = new(bindingParameterIndexs.Count);
foreach (var argumentIndex in bindingParameterIndexs)
{
arguments.Add(parameters[argumentIndex].Name!, invocationContext.Arguments[argumentIndex]);
}
try
{
var results = await metadata.ValidateAsync(arguments);
if (results != null) invocationContext.HttpContext.Items.Add(_validationResultItemName, results);
}
catch (Exception e)
{
logger.LogError(e, "Validate parameter failed for route handler method '{methodName}'.", filterFactoryContext.MethodInfo.Name);
}
return await next(invocationContext);
};
});
});
return new(endpointConvention);
}
public sealed class EndpointBindingParametersValidationMetadataMark;
这里返回特殊类型的构造器并用作错误结果过滤器的参数,确保错误结果过滤器只能在参数验证过滤器之后注册。
自动验证错误返回过滤器
public static TBuilder AddValidationProblemResult<TBuilder>(
this EndpointParameterDataAnnotationsRouteHandlerBuilder<TBuilder> validationEndpointBuilder,
int statusCode = StatusCodes.Status400BadRequest)
where TBuilder : IEndpointConventionBuilder
{
validationEndpointBuilder.InnerBuilder.Add(endpointBuilder =>
{
var loggerFactory = endpointBuilder.ApplicationServices.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger(_filterLoggerName);
// 检查 OpenAPI 元数据是否存在
if (!endpointBuilder.Metadata.Any(static md =>
md is IProducesResponseTypeMetadata pr
&& (pr.Type?.IsAssignableTo(typeof(HttpValidationProblemDetails))) is true)
)
{
// 添加 OpenAPI 元数据
endpointBuilder.Metadata.Add(
new ProducesResponseTypeMetadata(
statusCode,
typeof(HttpValidationProblemDetails),
["application/problem+json", "application/json"]
)
);
}
// 检查重复注册自动验证错误返回过滤器
if (endpointBuilder.Metadata.Any(static md => md is EndpointParameterDataAnnotationsValidationProblemResultMark))
{
logger.LogDebug("Already has a parameter data annotations validation problem result filter on endpoint {actionName}.", endpointBuilder.DisplayName);
return;
}
// 标记自动验证错误返回过滤器已经注册
endpointBuilder.Metadata.Add(new EndpointParameterDataAnnotationsValidationProblemResultMark());
endpointBuilder.FilterFactories.Add(static (filterFactoryContext, next) =>
{
return async invocationContext =>
{
var errors = invocationContext.HttpContext.GetEndpointParameterDataAnnotationsProblemDetails();
if (errors is { Count: > 0 }) return Results.ValidationProblem(errors);
else return await next(invocationContext);
};
});
});
return validationEndpointBuilder.InnerBuilder;
}
public static TBuilder RestoreToOriginalBuilder<TBuilder>(this EndpointParameterDataAnnotationsRouteHandlerBuilder<TBuilder> validationEndpointBuilder)
where TBuilder : IEndpointConventionBuilder
{
return validationEndpointBuilder.InnerBuilder;
}
public sealed class EndpointParameterDataAnnotationsValidationProblemResultMark;
注册错误结果过滤器后返回原始构造器,可用于继续注册其他东西。或者用专门的辅助方法直接还原,跳过注册错误结果过滤器。
手动重新验证参数
public static async Task<bool> TryValidateEndpointParametersAsync(
this HttpContext httpContext,
params IEnumerable<KeyValuePair<string, object?>> arguments)
{
ArgumentNullException.ThrowIfNull(httpContext);
var metadata = httpContext.GetEndpointBindingParameterValidationMetadata();
if (metadata is null) return false;
if (!arguments.Any()) throw new ArgumentException("There are no elements in the sequence.", nameof(arguments));
HashSet<string> names = [];
foreach (var name in arguments.Select(arg => arg.Key))
{
if (string.IsNullOrEmpty(name)) throw new ArgumentException("Argument's name cannot be null or empty.", nameof(arguments));
if (!names.Add(name)) throw new ArgumentException("Argument's name must be unique.", nameof(arguments));
}
var currentResults = httpContext.GetEndpointParameterDataAnnotationsValidationResultsCore();
var newResults = await metadata.ValidateAsync(arguments.ToDictionary(arg => arg.Key, arg => arg.Value));
if (newResults is null) // 本次验证结果没有任何错误
{
if (currentResults != null)
{
// 移除本次验证结果中没有验证错误数据的参数项
foreach (var argument in arguments) currentResults.Remove(argument.Key);
// 如果移除后变成空集,直接清除结果集
if (currentResults.Count is 0) httpContext.Items.Remove(_validationResultItemName);
}
}
else
{
if (currentResults != null)
{
// 如果上次的验证结果中有同名参数的数据,但本次验证结果中没有,移除该参数的过时的旧结果数据
foreach (var argument in arguments)
{
if (!newResults.Keys.Any(key => key == argument.Key)) currentResults.Remove(argument.Key);
}
}
else
{
// 上次验证结果显示没有任何错误,新建错误结果集
httpContext.Items.Remove(_validationResultItemName);
currentResults = [];
httpContext.Items.Add(_validationResultItemName, currentResults);
}
// 添加上次验证中没有错误数据的参数项,或者更新同名参数项的验证错误数据
foreach (var newResult in newResults) currentResults[newResult.Key] = newResult.Value;
}
return true;
}
可以说构建元数据的根本目的就是为了支持手动重新验证,MVC的手动重新验证同样也是依靠元数据系统。确实是个好想法,借鉴过来用。
获取验证结果
internal static Dictionary<string, ValidationResultStore>? GetEndpointParameterDataAnnotationsValidationResultsCore(this HttpContext httpContext)
{
ArgumentNullException.ThrowIfNull(httpContext);
httpContext.Items.TryGetValue(_validationResultItemName, out var result);
return result as Dictionary<string, ValidationResultStore>;
}
public static EndpointArgumentsValidationResults? GetEndpointParameterDataAnnotationsValidationResults(this HttpContext httpContext)
{
var results = httpContext.GetEndpointParameterDataAnnotationsValidationResultsCore();
if (results is null) return null;
return new(results
.ToDictionary(
static r => r.Key,
static r => new ArgumentPropertiesValidationResults(r.Value.ToDictionary(
static fr => fr.Key.ToString()!,
static fr => fr.Value.ToImmutableList())
)
)
);
}
获取本地化的验证错误消息
我们知道MVC的本地化功能非常灵活,支持消息模板,能在最大程度上使本地化资源通用。但是不同的验证特性的消息模板占位符不尽相同,除了0号占位符统一表示属性名以外,其他占位符的数量是不确定的。因此MVC框架使用了一套适配器来允许开发者自行开有发针对性的消息生成,同时为内置验证特性准备了一组适配器。笔者也借鉴这套适配器系统开发了一个简易版,并内置了官方现有验证特性的适配器。主要区别是这套适配器没有客户端验证相关的功能。
public interface IAttributeAdapter
{
Type CanProcessAttributeType { get; }
object[]? GetLocalizationArguments(ValidationAttribute attribute);
}
public abstract class AttributeAdapterBase<TAttribute> : IAttributeAdapter
where TAttribute : ValidationAttribute
{
public Type CanProcessAttributeType => typeof(TAttribute);
public object[]? GetLocalizationArguments(ValidationAttribute attribute)
{
return GetLocalizationArgumentsInternal((TAttribute)attribute);
}
protected abstract object[]? GetLocalizationArgumentsInternal(TAttribute attribute);
}
public sealed class RangeAttributeAdapter : AttributeAdapterBase<RangeAttribute>
{
protected override object[]? GetLocalizationArgumentsInternal(RangeAttribute attribute)
{
return [attribute.Minimum, attribute.Maximum];
}
}
有了消息模板占位符参数后,剩下的就好办了。
public static Dictionary<string, string[]>? GetEndpointParameterDataAnnotationsProblemDetails(this HttpContext httpContext)
{
Dictionary<string, string[]>? result = null;
var validationResult = httpContext.GetEndpointParameterDataAnnotationsValidationResultsCore();
if (validationResult?.Any(vrp => vrp.Value.Any()) is true)
{
var localizerFactory = httpContext.RequestServices.GetService<IStringLocalizerFactory>();
EndpointParameterValidationLocalizationOptions? localizationOptions = null;
AttributeLocalizationAdapters? adapters = null;
if (localizerFactory != null)
{
localizationOptions = httpContext.RequestServices
.GetService<IOptions<EndpointParameterValidationLocalizationOptions>>()
?.Value;
adapters = localizationOptions?.Adapters;
}
var metadatas = httpContext.GetEndpointBindingParameterValidationMetadata();
Debug.Assert(metadatas != null);
var endpointHandlerType = metadatas.EndpointMethod.ReflectedType;
Debug.Assert(endpointHandlerType != null);
var errors = validationResult.SelectMany(vrp => vrp.Value);
result = localizerFactory is null || !(adapters?.Count > 0)
? errors
.ToDictionary(
static fvr => fvr.Key.ToString()!,
static fvr => fvr.Value.Select(ToErrorMessage).ToArray()
)
: errors
.ToDictionary(
static fvr => fvr.Key.ToString()!,
fvr => fvr.Value
.Select(vr =>
ToLocalizedErrorMessage(
vr,
fvr.Key.ModelIsTopLevelFakeObject
? new KeyValuePair<Type, ParameterValidationMetadata>(
endpointHandlerType,
(metadatas?.TryGetValue(fvr.Key.FieldName!, out var metadata)) is true
? metadata
: null! /* never null */)
: null,
adapters,
localizerFactory
)
)
.ToArray()
);
}
return result;
static string ToErrorMessage(ValidationResult result)
{
return result.ErrorMessage!;
}
string ToLocalizedErrorMessage(
ValidationResult result,
KeyValuePair<Type, ParameterValidationMetadata>? parameterMetadata,
AttributeLocalizationAdapters adapters,
IStringLocalizerFactory localizerFactory)
{
if (result is LocalizableValidationResult localizable)
{
var localizer = localizerFactory.Create(localizable.InstanceObjectType);
string displayName;
if (!string.IsNullOrEmpty(parameterMetadata?.Value.DisplayName))
{
var parameterLocalizer = localizerFactory.Create(parameterMetadata.Value.Key);
displayName = parameterLocalizer[parameterMetadata.Value.Value.DisplayName];
}
else displayName = GetDisplayName(localizable, localizer);
var adapter = adapters.FirstOrDefault(ap => localizable.Attribute.GetType().IsAssignableTo(ap.CanProcessAttributeType));
if (adapter != null
&& !string.IsNullOrEmpty(localizable.Attribute.ErrorMessage)
&& string.IsNullOrEmpty(localizable.Attribute.ErrorMessageResourceName)
&& localizable.Attribute.ErrorMessageResourceType == null)
{
return localizer
[
localizable.Attribute.ErrorMessage,
[displayName, .. adapter.GetLocalizationArguments(localizable.Attribute) ?? []]
];
}
return localizable.Attribute.FormatErrorMessage(displayName);
}
return result.ErrorMessage!;
static string GetDisplayName(LocalizableValidationResult localizable, IStringLocalizer localizer)
{
string? displayName = null;
ValidationAttributeStore store = ValidationAttributeStore.Instance;
DisplayAttribute? displayAttribute = null;
DisplayNameAttribute? displayNameAttribute = null;
if (string.IsNullOrEmpty(localizable.MemberName))
{
displayAttribute = store.GetTypeDisplayAttribute(localizable.Context);
displayNameAttribute = store.GetTypeDisplayNameAttribute(localizable.Context);
}
else if (store.IsPropertyContext(localizable.Context))
{
displayAttribute = store.GetPropertyDisplayAttribute(localizable.Context);
displayNameAttribute = store.GetPropertyDisplayNameAttribute(localizable.Context);
}
if (displayAttribute != null)
{
displayName = displayAttribute.GetName();
}
else if (displayNameAttribute != null)
{
displayName = displayNameAttribute.DisplayName;
}
return string.IsNullOrEmpty(displayName)
? localizable.DisplayName
: localizer[displayName];
}
}
}
本地化错误消息可以说是整个工程里最麻烦的部分,为此甚至改造了一番对象图验证器。因为MVC根本没有使用验证器,而是直接使用原始验证特性,这意味着MVC可以获得最原始的信息进行任何自定义处理。为了做到同样的效果,必须让对象图验证器也返回包含原始信息的验证结果。其中的关键就是LocalizableValidationResult
,这个新类型保存了本地化所需的一切原始信息。对象图验证器会在获得验证结果后将原始结果重新包装一次,因此这些信息在非MVC环境下同样可用。
还有就是针对参数本身的特殊处理,参数名和参数上的特性和对象内部的特性无法通用一套逻辑,必须分别处理。为此又把事情给弄麻烦了一些,也算是再次感受到MVC的元数据系统的强大。
基本使用方法
Nuget包中有一个比较完整的示例说明,也比较长,就不赘述了,可以到这里查看使用说明。
结语
这真是一次漫长的开发之旅,一开始就对开发难度估计不足,把自己坑进去一个多星期,然后半路临时追加新功能,又被坑进去一个多星期。还好前期开发的功能比较严谨,为临时功能做改造还算顺利。也算是好好体会了一把严谨设计的威力,跟着微软的代码照猫画虎是真的能提前避开很多坑啊。
QQ群
读者交流QQ群:540719365
欢迎读者和广大朋友一起交流,如发现本书错误也欢迎通过博客园、QQ群等方式告知笔者。
本文地址:如何优雅地让 ASP.NET Core 支持异步模型验证
如何优雅地让 ASP.NET Core 支持异步模型验证的更多相关文章
- 从零写一个Asp.net core手脚架(模型验证)
一个asp.net core项目,一定包含了各种的实体,在RESTful api里面,有很多的参数传递,不建立实体则大量的参数需要自定验证正确性,并且Action上面会写的密密麻麻的参数 在asp.n ...
- ASP.NET Core 添加统一模型验证处理机制
一.前言 模型验证自ASP.NET MVC便有提供,我们可以在Model(DTO)的属性上加上数据注解(Data Annotations)特性,在进入Action之前便会根据数据注解,来验证输入的数据 ...
- asp.net core的输入模型验证
数据验证特性RequiredAttribute:表示数据不能为空RegularExpressionAttribute:正则校验CompareAttribute:和某个属性比较RangeAttribut ...
- 使用ASP.NET Core支持GraphQL -- 较为原始的方法
GraphQL简介 下面是GraphQL的定义: GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时. GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述 ...
- 使用ASP.NET Core支持GraphQL( restful 配套)
https://github.com/graphql-dotnet https://github.com/graphql GraphQL简介 官网:https://graphql.cn/code/ 下 ...
- asp dotnet core 支持客户端上传文件
本文告诉大家如何在 asp dotnet core 支持客户端上传文件 新建一个 asp dotnet core 程序,创建一个新的类,用于给客户端上传文件的信息 public class Kanaj ...
- asp.net core 3.x 身份验证-2启动阶段的配置
注册服务.配置选项.添加身份验证方案 在Startup.ConfigureServices执行services.AddAuthentication() 注册如下服务(便于理解省略了部分辅助服务): s ...
- asp.net core 3.x 身份验证-3cookie身份验证原理
概述 上两篇(asp.net core 3.x 身份验证-1涉及到的概念.asp.net core 3.x 身份验证-2启动阶段的配置)介绍了身份验证相关概念以及启动阶段的配置,本篇以cookie身份 ...
- 从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
OpenIDConnect是一个身份验证服务,而Oauth2.0是一个授权框架,在前面几篇文章里通过IdentityServer4实现了基于Oauth2.0的客户端证书(Client_Credenti ...
- 用.Net Core控制台模拟一个ASP.Net Core的管道模型
在我的上几篇文章中降到了asp.net core的管道模型,为了更清楚地理解asp.net core的管道,再网上学习了.Net Core控制台应用程序对其的模拟,以加深映像,同时,供大家学习参考. ...
随机推荐
- Spring —— bean实例化
bean 实例化 bean本质上就是对象,创建bean使用构造方法完成(反射) 构造方法(常用) 静态工厂* 实例工厂* FactoryBean(实 ...
- Servlet——xml配置Servlet
XML配置方式编写 Servlet 步骤: 1.编写 Servlet 类 2.在 web.xml 中配置该Servlet
- HTTP——响应数据格式
HTTP响应数据格式 状态码分类: 常见的状态响应码:
- 控制请求并发数量:p-limit 源码解读
p-limit 是一个控制请求并发数量的库,他的整体代码不多,思路挺好的,很有学习价值: 举例 当我们同时发起多个请求时,一般是这样做的 Promise.all([ requestFn1, reque ...
- [OI] Testlib
Testlib 是用于实现 SpecialJudge 的一种方式 为了使用 Testlib,你需要在你的文件中引用 Testlib.h testlib.h 下载 使用 Testlib 程序 以 Tes ...
- C# ASP.NET Core Web API 框架 实现向手机发送验证码短信
本文章主要是在C# ASP.NET Core Web API框架实现向手机发送验证码短信功能.这里我选择是一个互亿无线短信验证码平台,其实像阿里云,腾讯云上面也可以. 首先我们先去 互亿无线 http ...
- threejs 父元素 相对位置 position 网格对象
设置position都是相对于父元素的位置设置的 // 导入 threejs import * as THREE from "three"; import { OrbitContr ...
- 65.说下vue3的使用感想(说些vue3对比vue3的方便之处)
vue3 使用了组合式API,setup 替换了选项式api ,不需要在多个api里面写代码了,而且使用了setup的语法糖,可以更加方便写代码 : vue3使用proxy替代了Object.defi ...
- 43.v-if和v-for的优先级
v-for 的优先级高 延申问题:v-for 和 v-if 为什么不能在一起使用 ? 会造成性能的浪费,因为v-for 的优先级高,所以每次渲染都会执行v-if 判断条件,浪费时间 :比如 渲染 10 ...
- Kubernetes 边缘节点抓不到监控指标?试试这个方法!
KubeSphere v3.1.0 通过集成 KubeEdge,将节点和资源的管理延伸到了边缘,也是 KubeSphere 正式支持边缘计算的第一个版本. 笔者也第一时间搭建和试用了边缘节点相关的功能 ...