You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何编写C#自定义属性实现属性赋值时的布尔逻辑校验?

实现可校验属性赋值的C#自定义Attribute

现有代码的核心问题

你当前的CheckAttribute实现完全无法达到预期效果,原因有两点:

  • Attribute的构造函数是在程序集加载阶段执行的,而非属性setter触发时,根本无法捕获属性的实际赋值
  • 构造函数中硬编码了val = 0,完全没有关联到属性的目标赋值,校验逻辑形同虚设

运行时校验方案:用Source Generator自动生成校验逻辑

要实现「无需手动编写setter校验代码」的需求,最原生的.NET方案是使用Source Generator,在编译阶段自动为标记了CheckAttribute的属性生成带校验逻辑的setter。

步骤1:定义标记用的Attribute

先做一个仅用于标记的轻量Attribute,无需包含校验逻辑:

[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class CheckAttribute : Attribute
{
    public int CheckValue { get; }
    public Operation Operation { get; }

    public CheckAttribute(int checkValue, Operation operation)
    {
        CheckValue = checkValue;
        Operation = operation;
    }
}

public enum Operation
{
    Equals,
    GtrThan,
    LssThan,
    GrtOrEquals,
    LssOrEquals,
    Neq
}

步骤2:编写Source Generator

创建一个Source Generator项目,扫描所有标记了CheckAttribute的属性,自动生成带校验的属性实现:

[Generator]
public class CheckPropertyGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context) {}

    public void Execute(GeneratorExecutionContext context)
    {
        // 遍历所有包含标记属性的类
        foreach (var classSymbol in context.Compilation.SyntaxTrees
                     .SelectMany(tree => tree.GetRoot().DescendantNodes()
                         .OfType<ClassDeclarationSyntax>())
                     .Select(syntax => context.Compilation.GetSemanticModel(syntax.SyntaxTree)
                         .GetDeclaredSymbol(syntax))
                     .OfType<INamedTypeSymbol>())
        {
            // 筛选标记了CheckAttribute的属性
            foreach (var propertySymbol in classSymbol.GetMembers()
                         .OfType<IPropertySymbol>()
                         .Where(p => p.GetAttributes()
                             .Any(a => a.AttributeClass?.Name == nameof(CheckAttribute))))
            {
                // 获取Attribute的配置参数
                var checkAttr = propertySymbol.GetAttributes()
                    .First(a => a.AttributeClass?.Name == nameof(CheckAttribute));
                var checkValue = (int)checkAttr.ConstructorArguments[0].Value!;
                var operation = (Operation)checkAttr.ConstructorArguments[1].Value!;

                // 生成带校验的属性代码
                var source = $@"
namespace {classSymbol.ContainingNamespace.ToDisplayString()}
{{
    partial class {classSymbol.Name}
    {{
        private int _{propertySymbol.Name.ToLowerInvariant()};
        public int {propertySymbol.Name}
        {{
            get => _{propertySymbol.Name.ToLowerInvariant()};
            set
            {{
                bool isValid = {GetValidationExpression(operation, "value", checkValue)};
                if (!isValid)
                {{
                    throw new InvalidOperationException(""Property {propertySymbol.Name} validation failed: {GetErrorMessage(operation, checkValue)}"");
                }}
                _{propertySymbol.Name.ToLowerInvariant()} = value;
            }}
        }}
    }}
}}";

                context.AddSource($"{classSymbol.Name}_{propertySymbol.Name}.g.cs", source);
            }
        }
    }

    // 生成校验表达式字符串
    private string GetValidationExpression(Operation op, string valueVar, int checkValue)
    {
        return op switch
        {
            Operation.Equals => $"{valueVar} == {checkValue}",
            Operation.GtrThan => $"{valueVar} > {checkValue}",
            Operation.LssThan => $"{valueVar} < {checkValue}",
            Operation.GrtOrEquals => $"{valueVar} >= {checkValue}",
            Operation.LssOrEquals => $"{valueVar} <= {checkValue}",
            Operation.Neq => $"{valueVar} != {checkValue}",
            _ => "false"
        };
    }

    // 生成错误提示文本
    private string GetErrorMessage(Operation op, int checkValue)
    {
        return op switch
        {
            Operation.Equals => $"must equal {checkValue}",
            Operation.GtrThan => $"must be greater than {checkValue}",
            Operation.LssThan => $"must be less than {checkValue}",
            Operation.GrtOrEquals => $"must be ≥ {checkValue}",
            Operation.LssOrEquals => $"must be ≤ {checkValue}",
            Operation.Neq => $"must not equal {checkValue}",
            _ => "invalid validation rule"
        };
    }
}

步骤3:使用方式

定义类时仅需标记属性,Source Generator会自动生成带校验逻辑的setter:

partial class MyClass 
{
    [Check(0, Operation.LssThan)]
    public int MinValue { get; set; }

    [Check(10, Operation.GtrThan)]
    public int MaxValue { get; set; }
}

测试时,非法赋值会触发异常:

var obj = new MyClass();
obj.MinValue = -1; // 抛出InvalidOperationException:MinValue必须小于0
obj.MaxValue = 11; // 抛出InvalidOperationException:MaxValue必须大于10

编译时校验方案:Roslyn分析器

如果需要在编译阶段就捕获非法赋值,可以编写Roslyn Diagnostic Analyzer,扫描代码中的属性赋值操作,提前给出错误提示。

核心实现片段

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CheckPropertyAnalyzer : DiagnosticAnalyzer
{
    public const string DiagnosticId = "CheckProperty001";
    private static readonly DiagnosticDescriptor Rule = new(
        DiagnosticId,
        "Invalid property assignment",
        "Assignment to {0} violates rule: {1}",
        "Usage",
        DiagnosticSeverity.Error,
        isEnabledByDefault: true,
        description: "Property assignment doesn't meet CheckAttribute validation rules");

    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

    public override void Initialize(AnalysisContext context)
    {
        context.RegisterSyntaxNodeAction(AnalyzeAssignment, SyntaxKind.SimpleAssignmentExpression);
    }

    private void AnalyzeAssignment(SyntaxNodeAnalysisContext context)
    {
        var assignment = (AssignmentExpressionSyntax)context.Node;
        var propertySymbol = context.SemanticModel.GetSymbolInfo(assignment.Left).Symbol as IPropertySymbol;
        if (propertySymbol == null) return;

        // 检查属性是否标记了CheckAttribute
        var checkAttr = propertySymbol.GetAttributes()
            .FirstOrDefault(a => a.AttributeClass?.Name == nameof(CheckAttribute));
        if (checkAttr == null) return;

        var checkValue = (int)checkAttr.ConstructorArguments[0].Value!;
        var operation = (Operation)checkAttr.ConstructorArguments[1].Value!;

        // 仅分析常量赋值的情况
        if (assignment.Right is not LiteralExpressionSyntax valueExpr 
            || !int.TryParse(valueExpr.Token.ValueText, out var assignedValue))
            return;

        // 校验常量值是否符合规则
        bool isValid = operation switch
        {
            Operation.Equals => assignedValue == checkValue,
            Operation.GtrThan => assignedValue > checkValue,
            Operation.LssThan => assignedValue < checkValue,
            Operation.GrtOrEquals => assignedValue >= checkValue,
            Operation.LssOrEquals => assignedValue <= checkValue,
            Operation.Neq => assignedValue != checkValue,
            _ => false
        };

        if (!isValid)
        {
            var message = string.Format(Rule.MessageFormat.ToString(), 
                propertySymbol.Name, 
                GetErrorMessage(operation, checkValue));
            context.ReportDiagnostic(Diagnostic.Create(Rule, assignment.GetLocation(), message));
        }
    }

    private string GetErrorMessage(Operation op, int checkValue)
    {
        // 复用之前的错误文本逻辑
        return op switch
        {
            Operation.Equals => $"must equal {checkValue}",
            Operation.GtrThan => $"must be greater than {checkValue}",
            Operation.LssThan => $"must be less than {checkValue}",
            Operation.GrtOrEquals => $"must be ≥ {checkValue}",
            Operation.LssOrEquals => $"must be ≤ {checkValue}",
            Operation.Neq => $"must not equal {checkValue}",
            _ => "invalid validation rule"
        };
    }
}

这个分析器会在编译时直接报错:

var obj = new MyClass();
obj.MinValue = -1; // 编译错误:Assignment to MinValue violates rule: must be less than 0

总结

  • 运行时校验:用Source Generator自动生成校验逻辑,完全无需手动编写重复的setter代码
  • 编译时校验:配合Roslyn分析器,提前捕获非法赋值,避免运行时异常
  • 彻底避开了「在Attribute构造函数中编写校验逻辑」的误区,因为Attribute的生命周期和属性赋值完全无关

内容的提问来源于stack exchange,提问作者solovyevuv

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 01:04:58