C#中如何为带校验特性的方法参数自动注入校验逻辑
基础背景与需求
我目前正在开展个人技术试验,开发了一批遵循IParameterValidator<in T>接口的参数校验器,接口定义如下:
public interface IParameterValidator<in T> { public void Validate(T param); }
以邮箱校验器为例,其实现代码如下:
public class EmailValidator: IParameterValidator<Email> { private static readonly Regex EmailRegex = new(@"^[\w!#$%&'*+\-/=?\^_`{|}~]+(\.[\w!#$%&'*+\-/=?\^_`{|}~]+)*((([\-\w]+\.)+[a-zA-Z]{2,4})|(([0-9]{1,3}\.){3}[0-9]{1,3}))$"); public void Validate(Email email) { if (!EmailRegex.IsMatch(email.ToString())) throw new InvalidParameterException(nameof(email)); } }
我的需求是:在任意代码位置为方法参数添加特性标注后,系统可自动匹配对应的校验器完成参数校验。为此我设计了如下ValidateParameterAttribute特性:
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)] public class ValidateParameterAttribute: Attribute { public Type ValidatorType { get; } public ValidateParameterAttribute(Type validatorType) { if (!validatorType.IsAssignableTo(typeof(IParameterValidator<>))) throw new("Must provide a validator!"); } }
据我所知,C#原生没有提供「识别方法/属性上的该特性即自动执行指定逻辑」的简易实现方式。我也知晓可以通过额外封装类扫描特性,实现类似MethodInfo.ValidatedInvoke(object[] params parms)的扩展方法完成调用,但我不希望采用这种侵入式的调用方案,希望该功能可以「开箱即用」。
我的预期效果是,如下标注了校验特性的方法:
public User CreateUser( [ValidateParameter(typeof(NameValidator))] Name name, [ValidateParameter(typeof(EmailValidator))] Email email, [ValidateParameter(typeof(AdultValidator))] DateOfBirth dateOfBirth) { User user = new(name, email, dateOfBirth); _userRepository.Add(user); return user; }
可以自动在方法执行起始位置注入对应的校验调用逻辑(校验器可存储在注册容器中,该部分实现暂不考虑),最终效果等价于如下代码:
public User CreateUser( [ValidateParameter(typeof(NameValidator))] Name name, [ValidateParameter(typeof(EmailValidator))] Email email, [ValidateParameter(typeof(AdultValidator))] DateOfBirth dateOfBirth) { Activator.CreateInstance<NameValidator>().Validate(name); Activator.CreateInstance<EmailValidator>().Validate(email); Activator.CreateInstance<AdultValidator>().Validate(dateOfBirth); User user = new(name, email, dateOfBirth); _userRepository.Add(user); return user; }
我查阅资料后了解到Source Generators(源生成器)可实现该需求,但我此前从未接触过源生成器开发,目前我已经编写了可生成校验代码片段的方法GetValidationString(MethodInfo method),代码如下:
private string GetValidationString(MethodInfo method) { StringBuilder builder = new(); ParameterInfo[] parameters = method.GetParameters(); if (parameters.Length == 0) return string.Empty; foreach (ParameterInfo info in parameters) { ValidateParameterAttribute? validationAttribute = info.GetCustomAttribute<ValidateParameterAttribute>(); if (validationAttribute is null) continue; builder.Append($"Activator.CreateInstance<{validationAttribute.ValidatorType.Name}>().Validate({info.Name});\n"); } return builder.ToString(); }
我的第一个疑问是:如何通过源生成器将上述方法生成的校验代码,正确注入到目标方法的对应位置?
更新:Someta.Fody 方案异常问题
根据评论区的建议,我尝试使用Someta.Fody提供的方法拦截器实现该功能,目前编写了如下拦截特性:
[AttributeUsage(AttributeTargets.Method)] public class ValidatedMethodAttribute: Attribute, IMethodInterceptor { public object Invoke(MethodInfo methodInfo, object instance, Type[] typeArguments, object[] arguments, Func<object[], object> invoker) { Attribute[]? attributes = methodInfo.GetCustomAttributes().Where(att => att.GetType().IsAssignableTo(typeof(IParameterValidator<>))).ToArray(); // 执行校验逻辑 return invoker.Invoke(arguments); } }
但构建项目时Fody抛出了未处理异常,我暂时无法定位问题原因,异常信息如下:
Fody: An unhandled exception occurred: Exception: Failed to execute weaver /home/james/.nuget/packages/someta.fody/1.2.1/build/../weaver/Someta.Fody.dll Type: System.Exception StackTrace: at InnerWeaver.ExecuteWeavers() in C:\projects\fody\FodyIsolated\InnerWeaver.cs:line 222 at InnerWeaver.Execute() in C:\projects\fody\FodyIsolated\InnerWeaver.cs:line 112 Source: FodyIsolated TargetSite: Void ExecuteWeavers() System.Type was somehow not found. Aborting. Type: System.InvalidOperationException StackTrace: at Someta.Fody.CecilExtensions.Initialize(ModuleDefinition moduleDefinition, TypeSystem typeSystem, AssemblyNameReference soMeta) at Someta.Fody.ModuleWeaver.Execute() at InnerWeaver.ExecuteWeavers() in C:\projects\fody\FodyIsolated\InnerWeaver.cs:line 186 Source: Someta.Fody TargetSite: Boolean Initialize(Mono.Cecil.ModuleDefinition, Fody.TypeSystem, Mono.Cecil.AssemblyNameReference)
我注意到错误堆栈中存在Linux环境下不存在的C盘路径引用,想确认Linux构建环境是否是导致该异常的原因。
内容的提问来源于stack exchange,提问作者ScottishTapWater

