使用属性选择器时如何区分string与string?可空类型
问题描述
现有一个Anonymizer类实现了IAnonymizer接口,其泛型Anonymize方法通过属性选择器匿名化实体的指定属性。但目前存在问题:无法区分**非可空字符串(string)和可空字符串(string?)**属性——两种场景下泛型参数TProperty的类型均为string,导致可空字符串属性无法被正确设置为null,而是被设置为<Anonymized>。
现有实现代码
Anonymizer类
public class Anonymizer : IAnonymizer { public void Anonymize<TEntity, TProperty>(TEntity entity, Expression<Func<TEntity, TProperty>> propertySelector) { var value = typeof(TProperty) switch { { } t when IsNullable(t) => (TProperty?)(object) null!, { } t when t == typeof(string) => (TProperty) (object) "<Anonymized>", { } t when t == typeof(int) => (TProperty) (object) -1, _ => default, }; var memberExpression = (MemberExpression)propertySelector.Body; var property = (PropertyInfo)memberExpression.Member; property.SetValue(entity, value); } private static bool IsNullable(Type t) { return !t.IsValueType || Nullable.GetUnderlyingType(t) != null; } }
测试用例
public class AnonymizerTests { [Fact] public void Test_string() { var sut = new Anonymizer(); var myObject = new MyClass {MyText = "Hello World", MyNumber = 42}; sut.Anonymize(myObject, e => e.MyText); myObject.MyText.Should().Be("<Anonymized>"); myObject.MyNumber.Should().Be(42); } [Fact] public void Test_int() { var sut = new Anonymizer(); var myObject = new MyClass {MyText = "Hello World", MyNumber = 42}; sut.Anonymize(myObject, e => e.MyNumber); myObject.MyText.Should().Be("Hello World"); myObject.MyNumber.Should().Be(-1); } [Fact] public void Test_NullableString() { var sut = new Anonymizer(); var myObject = new MyClass { MyText = "Hello World", MyNumber = 42, MyNullableString = "Hello World" }; sut.Anonymize(myObject, e => e.MyNullableString); myObject.MyNumber.Should().Be(42); myObject.MyNullableString.Should().BeNull(); } class MyClass { public string MyText { get; set; } public int MyNumber { get; set; } public string? MyNullableString { get; set; } } }
解决建议
核心问题是泛型参数TProperty会被C#的nullable特性“抹平”可空引用类型的标记——string?作为泛型参数传入时,编译器会将TProperty解析为string,导致无法通过typeof(TProperty)区分可空与非可空字符串。
解决思路是:直接从属性本身的元数据判断可空性,而非依赖泛型参数的类型。具体修改如下:
修改后的Anonymizer类
public class Anonymizer : IAnonymizer { public void Anonymize<TEntity, TProperty>(TEntity entity, Expression<Func<TEntity, TProperty>> propertySelector) { // 先获取目标属性的元数据 var memberExpression = (MemberExpression)propertySelector.Body; var property = (PropertyInfo)memberExpression.Member; var propertyType = property.PropertyType; // 根据属性的实际类型和可空性确定匿名化值 object? value = propertyType switch { // 可空值类型(如int?)或可空引用类型(如string?)设为null { } t when IsPropertyNullable(property) => null, // 非可空字符串设为固定标记 { } t when t == typeof(string) => "<Anonymized>", // 非可空int设为-1 { } t when t == typeof(int) => -1, // 其他类型返回默认值 _ => default(TProperty) }; property.SetValue(entity, value); } /// <summary> /// 判断属性是否为可空类型(包括可空值类型和可空引用类型) /// </summary> private static bool IsPropertyNullable(PropertyInfo property) { // 1. 处理可空值类型(如int?) if (Nullable.GetUnderlyingType(property.PropertyType) != null) return true; // 2. 处理可空引用类型(如string?):检查属性上的NullableAttribute var nullableAttr = property.GetCustomAttribute<NullableAttribute>(); if (nullableAttr != null) { // NullableAttribute.Argument数组中,1表示可空 return nullableAttr.Argument.Length > 0 && nullableAttr.Argument[0] == 1; } // 3. 检查类级别的NullableContextAttribute,判断默认可空上下文 var contextAttr = property.DeclaringType?.GetCustomAttribute<NullableContextAttribute>(); if (contextAttr != null) { // Flag=1表示默认可空上下文 return contextAttr.Flag == 1; } // 4. 兜底:引用类型在未启用nullable的项目中默认视为可空 return !property.PropertyType.IsValueType; } }
修改说明
- 优先获取属性元数据:不再依赖
typeof(TProperty),而是直接从PropertyInfo获取属性的实际类型和可空标记。 - 完善可空判断逻辑:
- 先判断是否为可空值类型(通过
Nullable.GetUnderlyingType)。 - 再检查属性是否带有
NullableAttribute(C# 8+中string?会自动添加该标记)。 - 最后检查类的默认可空上下文,确保在类级别的nullable设置下也能正确判断。
- 先判断是否为可空值类型(通过
- 调整值的生成逻辑:根据属性的实际可空性决定设为
null还是固定标记,解决了可空字符串的匿名化问题。
修改后,Test_NullableString测试用例将正常通过,可空字符串会被设置为null,而非可空字符串仍保持<Anonymized>的标记。
内容的提问来源于stack exchange,提问作者Stijn Van Antwerpen
相关产品推荐
相关产品推荐

