使用Dapper映射DBNull时如何抛出异常并实现字段非空校验?
问题描述
假设我有一个要求字段值非默认值的实体类:
public class MyModel { public int PropertyOne { get; set; } public int? PropertyTwo { get; set; } public string PropertyThree { get; set; } }
当执行以下查询语句查询MyModel数据时:
SELECT NULL as PropertyOne, NULL as PropertyTwo, NULL as PropertyThree
映射后的结果为PropertyOne=0,PropertyTwo=null,PropertyThree=null。原因是Dapper默认遇到DBNull时不会调用属性的setter,因此值类型int的PropertyOne会被设为默认值0。
我的需求如下:
- 确保
PropertyOne始终从查询中获取非空值,当读取到DBNull时抛出异常。 - 不希望
PropertyThree为null,能否在Dapper映射时实现该校验?能否通过给实体类添加自定义[NotDBNull]特性实现?
修改后的实体类如下:
public class MyModel { public int PropertyOne { get; set; } public int? PropertyTwo { get; set; } [NotDBNull] public string PropertyThree { get; set; } }
理想的校验逻辑:
- 场景1:
PropertyOne读取到DBNull且该字段非可空 → 抛出异常 - 场景2:
PropertyTwo读取到DBNull且该字段可空 → 正常处理 - 场景3:
PropertyThree读取到DBNull且标记了[NotDBNull]特性 → 抛出异常
请问能否不借助其他校验方式,仅通过Dapper实现上述需求?
解决方案
可以仅通过Dapper的扩展机制实现你的需求,核心是利用自定义类型处理器和自定义映射逻辑覆盖默认行为:
1. 实现自定义[NotDBNull]特性
先定义需要的校验特性:
[AttributeUsage(AttributeTargets.Property)] public class NotDBNullAttribute : Attribute { }
2. 自定义ITypeMap实现全局校验逻辑
通过实现ITypeMap拦截属性赋值过程,嵌入空值校验逻辑,这是最灵活的方式:
public class StrictTypeMap : DefaultTypeMap { public StrictTypeMap(Type type) : base(type) { } public override PropertyMap GetProperty(string columnName) { var propertyMap = base.GetProperty(columnName); if (propertyMap == null) return null; // 包装原Setter,添加校验逻辑 var originalSetter = propertyMap.Setter; propertyMap.Setter = (target, value) => { var propertyInfo = propertyMap.PropertyInfo; // 场景1:非可空值类型遇到DBNull if (!IsNullableType(propertyInfo.PropertyType) && value == DBNull.Value) { throw new InvalidOperationException($"字段 {columnName} 读取到DBNull,但属性 {propertyInfo.Name} 为非可空类型,无法赋值。"); } // 场景3:标记[NotDBNull]的属性遇到DBNull if (propertyInfo.GetCustomAttribute<NotDBNullAttribute>() != null && value == DBNull.Value) { throw new InvalidOperationException($"字段 {columnName} 读取到DBNull,但属性 {propertyInfo.Name} 标记了[NotDBNull]特性,无法赋值。"); } // 场景2:可空类型处理DBNull为null originalSetter(target, value == DBNull.Value ? null : value); }; return propertyMap; } // 辅助方法:判断类型是否为可空类型 private static bool IsNullableType(Type type) { return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); } }
3. 注册自定义类型映射
在应用启动时,将自定义映射注册到Dapper,针对MyModel生效:
SqlMapper.SetTypeMap(typeof(MyModel), new StrictTypeMap(typeof(MyModel)));
备选方案:针对特定类型的自定义处理器
如果只需要单独处理非可空值类型(比如int),可以实现SqlMapper.TypeHandler<T>:
public class StrictIntHandler : SqlMapper.TypeHandler<int> { public override void SetValue(IDbDataParameter parameter, int value) { parameter.Value = value; } public override int Parse(object value) { if (value == DBNull.Value) { throw new InvalidOperationException("读取到DBNull,无法转换为非可空int类型。"); } return Convert.ToInt32(value); } }
注册处理器:
SqlMapper.AddTypeHandler(new StrictIntHandler());
这种方式仅能处理指定值类型,无法覆盖[NotDBNull]标记的引用类型,因此更推荐第一种ITypeMap方案。
验证效果
执行原查询时:
PropertyOne读取到DBNull,因是非可空int会抛出异常;PropertyTwo读取到DBNull,因是可空int?正常赋值为null;PropertyThree读取到DBNull,因标记[NotDBNull]会抛出异常;
完全符合需求。
内容的提问来源于stack exchange,提问作者Anton Zhernosek
相关产品推荐
相关产品推荐

