惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hacker News: Front Page
P
Palo Alto Networks Blog
T
ThreatConnect
Apple Machine Learning Research
Apple Machine Learning Research
博客园_首页
T
True Tiger Recordings
P
Privacy & Cybersecurity Law Blog
B
Blog
IT之家
IT之家
Last Week in AI
Last Week in AI
F
Full Disclosure
Hacker News: Ask HN
Hacker News: Ask HN
C
Comments on: Blog
Microsoft Azure Blog
Microsoft Azure Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Microsoft Security Blog
Microsoft Security Blog
博客园 - 【当耐特】
N
News and Events Feed by Topic
NISL@THU
NISL@THU
腾讯CDC
雷峰网
雷峰网
Security Latest
Security Latest
李成银的技术随笔
M
Microsoft Research Blog - Microsoft Research
L
LangChain Blog
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Check Point Blog
Y
Y Combinator Blog
Recent Announcements
Recent Announcements
博客园 - Franky
N
News | PayPal Newsroom
V
V2EX
A
About on SuperTechFans
The Register - Security
The Register - Security
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google Online Security Blog
Google Online Security Blog
MyScale Blog
MyScale Blog
Cisco Talos Blog
Cisco Talos Blog
Vercel News
Vercel News
WordPress大学
WordPress大学
C
Cyber Attacks, Cyber Crime and Cyber Security
The Hacker News
The Hacker News
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
爱范儿
爱范儿
A
Arctic Wolf
L
LINUX DO - 最新话题
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

博客园 - 陈大欠

Step Builder:让对象构建有顺序、有边界、有校验 浅析 .NET 响应式编程 IObservable与ReactiveX Cron表达式简明教程 正则表达式拾遗 快速批量升级 NugetPackage 版本 C# IEquatable和IEqualityComparer 最佳实践 使用XDT提高开发效率 如何保证XML正确性 分析 Dump 入门简明教程 Git commit emoji 对照表 ELK Stack 笔记 HTML5/CSS3(PrefixFree.js) 3D文字特效 Jquery实现图片上下一张 js css 构建滚动边框 使用jquery构建Metro style 返回顶部 使用js实现移动设备访问跳转到指定目录 从一幅图中了解开源世界 Jquery ajax 学习笔记 工欲善其事必先利其器系列之:在VS里面折叠js代码
基于 Attribute 的 AOP 字段校验
陈大欠 · 2026-03-26 · via 博客园 - 陈大欠

写在前面

如何把字段校验从业务代码里抽离出来,做成一条可复用、可扩展、可缓存的“校验切面”。

适用场景:

  • 业务模型字段很多,校验规则重复
  • 想让校验逻辑统一收口,而不是散落在各个 Service
  • 希望兼顾性能(表达式树编译 + 缓存)和可维护性(Attribute 声明式)

核心思路

核心链路可以概括为:

  1. 在模型(接口)属性上声明 ValidationAttribute
  2. 在规则基类 RuleBase.Apply 里统一触发字段校验
  3. ValidationContainer 负责获取/缓存校验委托
  4. ValidationBuilder 扫描属性特性并构建表达式树
  5. 编译成 Validation 委托后执行,返回 Result

这本质上就是 AOP 思想:
把“字段校验”作为横切关注点,从业务规则中切出来,通过统一入口织入。

类图

classDiagram class Program { +Main() } class IUserProfile { <<interface>> +string Name +string EnglishName +string Position } class DemoProfileModel { +string Name +string EnglishName +string Position } class RuleBase~TModel~ { +bool IgnoreFieldCheck +Result Apply(TModel model) #Result OnApply(TModel model) } class ProfileUpdateRule { +Result OnApply(IUserProfile model) } class ValidationContainer { +Validation Get(Type type) } class ValidationBuilder { +Validation Build(Type type) } class StringLengthExAttribute { +string Name +bool Trim +bool IsValid(object value) +string FormatErrorMessage(string name) } class Result { +bool Ok +Exception Error +string Message +Success() +Failure(Exception error) } Program --> ProfileUpdateRule : 调用 ProfileUpdateRule --|> RuleBase~IUserProfile~ : 继承 DemoProfileModel ..|> IUserProfile : 实现 RuleBase~TModel~ --> ValidationContainer : 获取校验委托 ValidationContainer --> ValidationBuilder : Build(type) ValidationBuilder --> StringLengthExAttribute : 调用 IsValid RuleBase~TModel~ --> Result : 返回

关键类职责

RuleBase

  • 统一规则入口
  • 在业务执行前触发字段校验
  • 校验失败直接返回,校验通过再进入 OnApply

ValidationContainer

  • ConcurrentDictionary<Type, Validation> 缓存委托
  • GetOrAdd 简化并发逻辑,避免重复构建

ValidationBuilder

  • 反射读取属性上的 ValidationAttribute
  • 按规则拼装表达式树
  • 编译为可复用委托,减少运行期开销

StringLengthExAttribute

  • StringLengthAttribute 基础上增强
  • 支持 Trim
  • 支持中文错误文案
  • 支持显式 token 替换,避免贪婪正则误匹配

每一步对应代码片段

pe82Nex.png

在接口属性上声明校验特性

public interface IUserProfile
{
    [StringLengthEx("姓名", 10)]
    string Name { get; set; }

    [StringLengthEx("英文名", 8)]
    string EnglishName { get; set; }
}

在规则基类里统一触发校验

public Result Apply(TModel model)
{
    if (!IgnoreFieldCheck)
    {
        if (model == null)
            return Result.Failure(new ArgumentNullException(nameof(model)));

        Validation func = ValidationContainer.Instance.Get(typeof(TModel));
        Result checkResult = func(model);
        if (!checkResult.Ok)
            return checkResult;
    }

    return OnApply(model);
}

容器中并发缓存委托

private readonly ConcurrentDictionary<Type, Validation> _container =
    new ConcurrentDictionary<Type, Validation>();

public Validation Get(Type type)
{
    return _container.GetOrAdd(type, x => ValidationBuilder.Instance.Build(x));
}

通过表达式树构建校验委托

public Validation Build(Type type)
{
    if (!type.IsCustomClassType())
        return _ => Result.Success();

    ParameterExpression modelExpr = Expression.Parameter(typeof(object), "model");
    ParameterExpression resultExpr = Expression.Variable(typeof(Result), "result");
    LabelTarget returnLabel = Expression.Label(typeof(Result));

    BlockExpression body = Expression.Block(
        new[] { resultExpr },
        BuildModelValidateExpr(type, modelExpr, returnLabel),
        Expression.Return(returnLabel, Expression.Call(typeof(Result), "Success", Type.EmptyTypes)),
        Expression.Label(returnLabel, resultExpr));

    return Expression.Lambda<Validation>(body, modelExpr).Compile();
}

表达式树构建后的代码

.Lambda #Lambda1<ValidationDemo.Validation>(System.Object $model) {
    .Block(ValidationDemo.Result $result) {
        .Block() {
            .Block() {
                .Block() {
                    .If (
                        .IsFalse(.Call .Constant<ValidationDemo.StringLengthExAttribute>(ValidationDemo.StringLengthExAttribute).IsValid((System.Object)((ValidationDemo.IUserProfile)$model).Name)
                        )
                    ) {
                        .Return #Label1 { .Call ValidationDemo.Result.Failure(.New ValidationDemo.FieldException(
                                "Name",
                                "姓名长度不能超过10")) }
                    } .Else {
                        .Default(System.Void)
                    }
                };
                .Block() {
                    .If (
                        .IsFalse(.Call .Constant<ValidationDemo.StringLengthExAttribute>(ValidationDemo.StringLengthExAttribute).IsValid((System.Object)((ValidationDemo.IUserProfile)$model).EnglishName)
                        )
                    ) {
                        .Return #Label1 { .Call ValidationDemo.Result.Failure(.New ValidationDemo.FieldException(
                                "EnglishName",
                                "英文名长度不能超过8")) }
                    } .Else {
                        .Default(System.Void)
                    }
                };
                .Block() {
                    .If (
                        .IsFalse(.Call .Constant<ValidationDemo.StringLengthExAttribute>(ValidationDemo.StringLengthExAttribute).IsValid((System.Object)((ValidationDemo.IUserProfile)$model).Position)
                        )
                    ) {
                        .Return #Label1 { .Call ValidationDemo.Result.Failure(.New ValidationDemo.FieldException(
                                "Position",
                                "职位长度不能超过20")) }
                    } .Else {
                        .Default(System.Void)
                    }
                }
            }
        };
        .Return #Label1 { .Call ValidationDemo.Result.Success() };
        .Label
            $result
        .LabelTarget #Label1:
    }
}


Result result;
if (!StringLengthExAttribute.IsValid(((IUserProfile)model).Name))
{
    return Result.Failure(new FieldException("Name", "姓名长度不能超过10"));
}
if (!StringLengthExAttribute.IsValid(((IUserProfile)model).EnglishName))
{
    return Result.Failure(new FieldException("EnglishName", "英文名长度不能超过8"));
}
if (!StringLengthExAttribute.IsValid(((IUserProfile)model).Position))
{
    return Result.Failure(new FieldException("Position", "职位长度不能超过20"));
}

return Result.Success();

特性里执行具体规则

public override bool IsValid(object value)
{
    if (Trim && value is string stringValue)
        value = stringValue.Trim();

    return base.IsValid(value);
}

错误模板用显式 token 替换

private static readonly Regex TokenRegex =
    new Regex(@"\{([A-Za-z_][A-Za-z0-9_]*)\}", RegexOptions.Compiled);

string message = TokenRegex.Replace(attr.ErrorMessage, match =>
{
    string token = match.Groups[1].Value;
    if (dict.TryGetValue(token, out object tokenValue))
        return Convert.ToString(tokenValue);

    return match.Value;
});

完整代码

在线运行地址

using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text.RegularExpressions;

namespace ValidationDemo
{
    /// <summary>
    /// 入口
    /// </summary>
    /// <remarks>
    /// 演示规则触发、Attribute 校验和统一结果返回这条链路
    /// </remarks>
    public static class Program
    {
        public static void Main()
        {
            ProfileUpdateRule rule = new ProfileUpdateRule(); // 规则对象:负责触发字段校验

            DemoProfileModel successModel = new DemoProfileModel
            {
                Name = "  张三  ", // 前后空格会在特性中 Trim 后校验
                EnglishName = "Tom", // 合法长度
                Position = "Engineer" // 合法长度
            };

            Result success = rule.Apply(successModel); // 走 RuleBase.Apply -> ValidationContainer -> ValidationBuilder
            Console.WriteLine($"Success Case => Ok:{success.Ok}, Message:{success.Message}");

            DemoProfileModel failedModel = new DemoProfileModel
            {
                Name = "  这是一个超长姓名这是一个超长姓名  ", // 超出 Name 最大长度 10
                EnglishName = "ThisEnglishNameIsTooLong", // 该字段也超长,但会被 Name 先拦截(当前实现失败即返回)
                Position = "Senior Engineer" // 不会执行到此字段校验
            };

            Result failed = rule.Apply(failedModel); // 预期失败并返回 FieldException
            Console.WriteLine($"Failed Case => Ok:{failed.Ok}, Message:{failed.Message}");
            if (failed.Error != null)
                Console.WriteLine($"Failed Error Type => {failed.Error.GetType().FullName}");

            Console.WriteLine("Done.");
        }
    }

    /// <summary>
    /// 扩展版长度特性
    /// </summary>
    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)]
    public class StringLengthExAttribute : StringLengthAttribute
    {
        /// <summary>
        /// 字段显示名
        /// </summary>
        public string Name { get; }

        /// <summary>
        /// 是否先 Trim
        /// </summary>
        public bool Trim { get; set; } = true;

        /// <summary>
        /// 初始化特性
        /// </summary>
        public StringLengthExAttribute(string name, int maximumLength)
            : base(maximumLength)
        {
            Name = name;
        }

        /// <summary>
        /// 执行校验
        /// </summary>
        public override bool IsValid(object value)
        {
            if (Trim)
            {
                if (value is string stringValue)
                    value = stringValue.Trim();
            }

            return base.IsValid(value);
        }

        /// <summary>
        /// 格式化错误消息
        /// </summary>
        /// <remarks>
        /// 如果没有显式配置 ErrorMessage,就使用默认中文模板
        /// </remarks>
        public override string FormatErrorMessage(string name)
        {
            if (string.IsNullOrWhiteSpace(ErrorMessage))
            {
                ErrorMessage = MinimumLength == 0
                    ? $"{Name}长度不能超过{{1}}"
                    : $"{Name}长度不能少于{{2}}或超过{{1}}";
            }

            return base.FormatErrorMessage(name);
        }
    }

    /// <summary>
    /// 字段校验异常
    /// </summary>
    public class FieldException : Exception
    {
        /// <summary>
        /// 失败字段名
        /// </summary>
        public string Field { get; private set; }

        public FieldException(string field, string message)
            : base(message)
        {
            Field = field;
        }

        public FieldException() : base()
        {
        }

        public FieldException(string message) : base(message)
        {
        }

        public FieldException(string message, Exception innerException) : base(message, innerException)
        {
        }
    }

    /// <summary>
    /// 统一返回结果
    /// </summary>
    public class Result
    {
        /// <summary>
        /// 是否成功
        /// </summary>
        public bool Ok { get; private set; }

        /// <summary>
        /// 错误对象
        /// </summary>
        public Exception Error { get; private set; }

        /// <summary>
        /// 可读消息
        /// </summary>
        public string Message { get; private set; }

        /// <summary>
        /// 创建成功结果
        /// </summary>
        public static Result Success()
        {
            return new Result
            {
                Ok = true,
                Message = "OK"
            };
        }

        /// <summary>
        /// 创建失败结果
        /// </summary>
        public static Result Failure(Exception error)
        {
            return new Result
            {
                Ok = false,
                Error = error,
                Message = error?.Message
            };
        }
    }

    /// <summary>
    /// 验证委托
    /// </summary>
    public delegate Result Validation(object model);

    /// <summary>
    /// 字段校验入口
    /// </summary>
    public static class FieldCheckHelper
    {
        /// <summary>
        /// 执行字段校验
        /// </summary>
        /// <remarks>
        /// 按运行时类型取缓存委托并执行
        /// </remarks>
        public static Result CheckFields(object model)
        {
            Validation func = ValidationContainer.Instance.Get(model.GetType());
            return func(model);
        }
    }

    /// <summary>
    /// 校验委托容器
    /// </summary>
    /// <remarks>
    /// 按类型缓存委托,首次构建,后续复用
    /// </remarks>
    public class ValidationContainer
    {
        private readonly ConcurrentDictionary<Type, Validation> _container = new ConcurrentDictionary<Type, Validation>();

        public static ValidationContainer Instance { get; } = new ValidationContainer();

        /// <summary>
        /// 获取校验委托
        /// </summary>
        public Validation Get(Type type)
        {
            return _container.GetOrAdd(type, x => ValidationBuilder.Instance.Build(x));
        }
    }

    /// <summary>
    /// 校验表达式构建器
    /// </summary>
    /// <remarks>
    /// 扫描属性上的 ValidationAttribute,拼表达式树并编译成委托
    /// </remarks>
    public class ValidationBuilder
    {
        private static readonly Regex TokenRegex = new Regex(@"\{([A-Za-z_][A-Za-z0-9_]*)\}", RegexOptions.Compiled);

        public static ValidationBuilder Instance { get; } = new ValidationBuilder();

        /// <summary>
        /// 构建校验委托
        /// </summary>
        public Validation Build(Type type)
        {
            if (!type.IsCustomClassType())
                return _ => Result.Success(); // 非对象类型直接放行

            ParameterExpression modelExpr = Expression.Parameter(typeof(object), "model");
            ParameterExpression resultExpr = Expression.Variable(typeof(Result), "result");
            LabelTarget returnLabel = Expression.Label(typeof(Result));
            BlockExpression body = Expression.Block(
                new[] { resultExpr },
                BuildModelValidateExpr(type, modelExpr, returnLabel),
                Expression.Return(returnLabel, Expression.Call(typeof(Result), "Success", Type.EmptyTypes)),
                Expression.Label(returnLabel, resultExpr));

            return Expression.Lambda<Validation>(body, modelExpr).Compile();
        }

        /// <summary>
        /// 生成模型校验表达式
        /// </summary>
        /// <remarks>
        /// 类只校验自身属性;接口会把继承链上的接口属性一并校验
        /// </remarks>
        private Expression BuildModelValidateExpr(Type type, Expression modelExpr, LabelTarget returnLabel)
        {
            Expression validateExpr = BuildSimpleModelValidateExpr(type, modelExpr, returnLabel);
            if (!type.IsInterface)
                return validateExpr;

            IEnumerable<Type> interfaces = type.GetInterfaces();
            IEnumerable<Expression> expressions = interfaces.Select(x => BuildSimpleModelValidateExpr(x, modelExpr, returnLabel));

            return Expression.Block(expressions.Concat(new Expression[] { validateExpr }));
        }

        /// <summary>
        /// 生成类型属性校验表达式
        /// </summary>
        private Expression BuildSimpleModelValidateExpr(Type type, Expression modelExpr, LabelTarget returnLabel)
        {
            PropertyInfo[] properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public);
            if (properties.Length == 0)
                return Expression.Empty();

            IEnumerable<Expression> expressions = properties.Select(property =>
            {
                Attribute[] attrs = Attribute.GetCustomAttributes(property, typeof(ValidationAttribute), true);
                MemberExpression propertyExpr = Expression.Property(Expression.Convert(modelExpr, type), property.Name);

                return BuildPropValidateExpr(attrs, property, propertyExpr, returnLabel);
            });

            return Expression.Block(expressions);
        }

        /// <summary>
        /// 生成属性校验表达式
        /// </summary>
        /// <remarks>
        /// 把一个属性上的多个 ValidationAttribute 依次拼成表达式
        /// </remarks>
        private Expression BuildPropValidateExpr(Attribute[] attrs, PropertyInfo property, Expression propertyExpr, LabelTarget returnLabel)
        {
            if (attrs == null || attrs.Length == 0)
                return Expression.Empty();

            IEnumerable<Expression> expressions = attrs.Select(attr =>
            {
                ValidationAttribute validationAttr = attr as ValidationAttribute;
                Expression isValidExpr = Expression.Call(
                    Expression.Constant(validationAttr),
                    "IsValid",
                    Type.EmptyTypes,
                    Expression.Convert(propertyExpr, typeof(object)));

                // 校验失败时立即 Return
                return Expression.IfThen(
                    Expression.IsFalse(isValidExpr),
                    Expression.Return(
                        returnLabel,
                        Expression.Call(
                            typeof(Result),
                            "Failure",
                            Type.EmptyTypes,
                            Expression.New(
                                typeof(FieldException).GetConstructor(new Type[] { typeof(string), typeof(string) }),
                                Expression.Constant(property.Name),
                                BuildErrorMsgExpr(validationAttr, property)))));
            });

            return Expression.Block(expressions);
        }

        /// <summary>
        /// 构造错误消息
        /// </summary>
        /// <remarks>
        /// 优先用默认格式化文案;若配置了模板则做占位符替换
        /// </remarks>
        private Expression BuildErrorMsgExpr(ValidationAttribute attr, PropertyInfo property)
        {
            if (string.IsNullOrWhiteSpace(attr.ErrorMessage))
                return Expression.Constant(attr.FormatErrorMessage(property.Name));

            Dictionary<string, object> dict = attr.GetType()
                .GetProperties(BindingFlags.Instance | BindingFlags.Public)
                .ToDictionary(x => x.Name, x => x.GetValue(attr), StringComparer.OrdinalIgnoreCase);

            string message = TokenRegex.Replace(attr.ErrorMessage, match =>
            {
                string token = match.Groups[1].Value;
                if (dict.TryGetValue(token, out object tokenValue))
                    return Convert.ToString(tokenValue);

                return match.Value;
            });

            return Expression.Constant(message);
        }
    }

    /// <summary>
    /// 类型扩展
    /// </summary>
    public static class TypeExtensions
    {
        /// <summary>
        /// 判断是否可做对象校验
        /// </summary>
        /// <remarks>
        /// 支持类和接口,排除 string
        /// </remarks>
        public static bool IsCustomClassType(this Type type)
        {
            return type != null
                && (type.IsClass || type.IsInterface)
                && type != typeof(string);
        }

        /// <summary>
        /// 判断是否可枚举类型
        /// </summary>
        /// <remarks>
        /// 当前 demo 未使用,预留给集合校验场景
        /// </remarks>
        public static bool IsEnumerableType(this Type type)
        {
            if (type == null || type == typeof(string))
                return false;

            return typeof(IEnumerable).IsAssignableFrom(type);
        }
    }

    /// <summary>
    /// 规则基类
    /// </summary>
    /// <remarks>
    /// 在 Apply 里先做字段校验,校验通过后再执行具体业务
    /// </remarks>
    public abstract class RuleBase<TModel>
    {
        /// <summary>
        /// 是否跳过字段校验
        /// </summary>
        public bool IgnoreFieldCheck { get; set; }

        protected RuleBase(bool ignoreFieldCheck = false)
        {
            IgnoreFieldCheck = ignoreFieldCheck;
        }

        /// <summary>
        /// 执行规则
        /// </summary>
        public Result Apply(TModel model)
        {
            if (!IgnoreFieldCheck)
            {
                if (model == null)
                    return Result.Failure(new ArgumentNullException(nameof(model))); // 空模型直接失败

                Validation func = ValidationContainer.Instance.Get(typeof(TModel)); // 按声明模型类型获取委托(与主工程一致)
                Result checkResult = func(model);
                if (!checkResult.Ok)
                    return checkResult; // 字段校验失败:不中断返回业务层,直接退出
            }

            return OnApply(model);
        }

        /// <summary>
        /// 业务扩展点
        /// </summary>
        protected abstract Result OnApply(TModel model);
    }

    /// <summary>
    /// 接口模型
    /// </summary>
    public interface IUserProfile
    {
        [StringLengthEx("姓名", 10)]
        string Name { get; set; }

        [StringLengthEx("英文名", 8)]
        string EnglishName { get; set; }

        [StringLengthEx("职位", 20)]
        string Position { get; set; }
    }

    /// <summary>
    /// 实现模型
    /// </summary>
    public class DemoProfileModel : IUserProfile
    {
        public string Name { get; set; }

        public string EnglishName { get; set; }

        public string Position { get; set; }
    }

    /// <summary>
    /// 更新规则示例
    /// </summary>
    /// <remarks>
    /// 继承 RuleBase 后,字段校验会在 Apply 里自动执行
    /// </remarks>
    public class ProfileUpdateRule : RuleBase<IUserProfile>
    {
        protected override Result OnApply(IUserProfile model)
        {
            return Result.Success(); // demo 中业务处理省略
        }
    }
}