- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
在ASP.NET Core官方仓库中有个一直很受关注的问题Please reconsider allowing async model validation。FluentValidation的作者也非常关心这个问题,因为FluentValidation内置异步验证功能,但是由于MVC内置的模型验证管道是同步的,使可兼容的功能和集成都受到严重阻碍。每次MVC修改验证功能都有可能导致集成出问题.
不仅如此,FluentValidation虽然是一个优秀的对象验证库,但其实现方式还是导致了一些与ASP.NET Core集成上的问题。例如他的验证消息本地化功能是通过全局对象管理的,这导致想要把消息本地化和依赖注入集成在一起的方式非常别扭.
最近在问题的评论中发现了一个复制并修改原始模型绑定系统得到的异步模型验证服务。这个修改版看上去还是比较直观的,使用自定义模型绑定器替换内置绑定器,然后用自定义的异步验证服务替换内置验证服务。同时提供了配套的异步验证特性和异步验证器助手.
但是使用过程中还是发现了一些不尽如人意的地方:
ValidationResult
的验证方法。内置特性只需要重写返回bool
或ValidationResult
的任意一个方法即可,对于大多数简单的验证需求而言返回bool
的版本完全足够。因此易用性不足。FluentValidation
。之前说过FluentValidation
和MVC集成是有瑕疵的。如果这么做,又可能出现同时使用多种验证方案使管理成本上升的情况。总之就是无论如何都感觉别扭。因此笔者基于原始的改造重写了一个新版以期解决以上问题.
有关新书的更多介绍欢迎查看《C#与.NET6 开发从入门到实践》上市,作者亲自来打广告了! 。
解决第一个问题,首先就是分离基本验证器和MVC集成的类到不同项目,因此基本验证器的相关内容放到CoreDX.Extensions.Validation中,MVC相关的内容放到CoreDX.Extensions.AspNetCore.Validation中.
解决第二个问题则是简单的根据原版验证特性重新按套路添加异步方法.
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
最后在为服务注册和手动重新验证添加相应的辅助方法,即可正常使用。此时如果继续使用同步的手动模型验证,虽然不会发生异常,但是异步验证特性也会被忽略.
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则是简单粗暴地在发现传入对象是结构体时直接抛出异常.
对于.NET6.0 来说,自己在API端点用对象图验证器验证一下就可以了,也只能这么做。但是 .NET7.0 为最小API也添加了过滤器功能,这使得模拟MVC的自动验证机制成为可能。本来这篇文章应该在将近半个月前发布的,就是因为偶然看见 .NET7.0 增加了最小API过滤器,笔者临时决定一不做二不休,把自动验证的管道过滤器也一并做完再发.
MinimalApis.Extensions是官方文档引用的过滤器应用示例,因此也是下载量比较大的扩展库,另外也有一个基于FluentValidation的。但是笔者的验证功能已经完全自己实现,想要接入进去也不可能了,而且这些库都没有还原出MVC的使用体验。既然如此,干脆完全重写一个更接近MVC体验的算了.
在此,笔者规划实现以下MVC功能:
可能是这个野心太大,本来以为两三天能搞定的,结果在被无尽的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群:540719365 。
欢迎读者和广大朋友一起交流,如发现本书错误也欢迎通过博客园、QQ群等方式告知笔者.
本文地址:如何优雅地让 ASP.NET Core 支持异步模型验证 。
最后此篇关于如何优雅地让ASP.NETCore支持异步模型验证的文章就讲到这里了,如果你想了解更多关于如何优雅地让ASP.NETCore支持异步模型验证的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我的 processmaker 安装遇到了一些问题。我正在尝试使用本指南 [url]http://wiki.processmaker.com/index.php/ProcessMaker_Ubuntu
我正在使用 ShareKit。发送 SMS 消息使用 MFMessageComposeViewController,用户看到标题“文本”。我想将该标题更改为更能反射(reflect)实际可用内容的内容
我需要在我的一个针对 Gingerbread 的 Android 应用程序中使用操作栏和 fragment 的组合。所以我使用了 v7 支持库中的操作栏和 v4 支持库中的 fragment ,并使用
我明白为什么浏览器 vendor 不想帮助我阻止他们的 UI 线程。但是,我不明白为什么会有: Web Workers 中没有 sleep (2) 没有同步 WebSockets API 有一个syn
最近我的组织正在考虑使用 Docker。我们组使用的是cloudera CDH 5.1.2。 1) cloudera 是否与 Docker 容器兼容?2) docker 和cloudera 组合是否存
我正在尝试通过编译在 Mac 上安装 rsync 3.2.3。但是,我想安装所有功能。为此,它需要一些库,此处 ( https://download.samba.org/pub/rsync/INSTA
我一直在使用 PyDev 成功运行 nose 测试,并想试试 nose2。 所以我安装了它 pip install nose2 复制/粘贴来自 http://nose2.info/ 的示例代码进入名为
我想知道 LLVM 中是否有任何函数/方法可以在 LLVM IR 中添加 Open-MP 构造。 llvm-3.0 是否仍然支持 OpenMP 指令? 最佳答案 OpenMP 是一种高级语言扩展。因此
我对 CUDA 编程非常陌生。我正在浏览 SDK 附带的示例。我能够编译代码,但是当我运行它时,出现以下错误: "clock.cu(177) : CUDA Runtime API error 38:
RStudio 是用于 R 开发的出色 IDE。我想知道是否有任何方法可以很好地支持 HiDPI 分辨率? 我目前有 13 英寸显示器和 3200x1800 分辨率,甚至很难阅读 RStudio 选项
我正在寻找一种有助于为 Django 项目提供 RDF 支持的工具。 到目前为止,我发现了两个: django-rdf - 最后一次修改是在 4 年前,所以它看起来像是一个死项目。 djubby -
我刚刚尝试了一些 JS 核心原则,发现引擎评估链接的关系运算符而不会引发错误。相反,他们以我自己无法理解的方式进行评估。 console.log(1 4 > 3 > 2 > 1); //false,
我知道 etexteditor 和 vim/emacs。 是否有任何其他 Windows 编辑器支持类似 textmate 的片段(例如,您编写触发词,按 Tab,它更改为某些内容,再次按 Tab,它
我正在尝试找出验证给定集群的网络策略配置的最佳方法。 According to the documentation Network policies are implemented by the ne
很难说出这里问的是什么。这个问题是含糊的、模糊的、不完整的、过于宽泛的或修辞性的,无法以目前的形式得到合理的回答。如需帮助澄清此问题以便重新打开它,visit the help center 。 已关
Z3 会支持 AUFBV 吗? 对于以下脚本: (set-logic AUFBV) (declare-fun x () (_ BitVec 16)) (declare-const t (Array (
使用分部类编写 NUnit 测试的优缺点是什么? 我要开始了: 亲:可以测试私有(private)方法 缺点:TDD 不再可能了 还有什么? 最佳答案 缺点:要么您必须测试与您发布的版本不同的构建,要
它很容易(对于 90% 的 aop 特性)在没有任何语言本身支持的情况下做到这一点,就像大多数动态语言如 python 和 ruby 一样。然而,Dojo在 1.3.2 上直接支持它.最新版本发生
我在我的 android 应用程序中使用亚洲字符,我已经了解到某些字符无法显示,因为系统字体不支持它们。我查询了一个包含亚洲字符的数据库,并且经常检索到无法显示的标志。这些情况对我的应用程序来说通常不
你好,我想实现一个控件,我想在用户键入@字符时启用该控件,直到未填充运行文本中的空格为止,它应该显示用户列表,@符号后键入的文本应该显示基于键盘字符的建议,就像我们在上面看到的那样Twitter 或
我是一名优秀的程序员,十分优秀!