如何编写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
相关产品推荐
相关产品推荐

