diff --git a/Admin.NET/Admin.NET.Core/Attribute/DictAttribute.cs b/Admin.NET/Admin.NET.Core/Attribute/DictAttribute.cs index c01e91a9..c2d80752 100644 --- a/Admin.NET/Admin.NET.Core/Attribute/DictAttribute.cs +++ b/Admin.NET/Admin.NET.Core/Attribute/DictAttribute.cs @@ -10,13 +10,18 @@ namespace Admin.NET.Core; /// 字典值合规性校验特性 /// [SuppressSniffer] -[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true, Inherited = true)] +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public class DictAttribute : ValidationAttribute, ITransient { /// /// 字典编码 /// - public string DictTypeCode { get; } + public readonly string DictTypeCode; + + /// + /// 是否多选 + /// + public readonly bool Multiple; /// /// 是否允许空字符串 @@ -29,109 +34,110 @@ public class DictAttribute : ValidationAttribute, ITransient public bool AllowNullValue { get; set; } = false; /// - /// 字典值合规性校验特性 + /// 构造函数 /// - /// - /// - public DictAttribute(string dictTypeCode = "", string errorMessage = "字典值不合法!") + /// 字典类型编码 + /// 是否允许多选 + /// 自定义错误消息 + public DictAttribute(string dictTypeCode = "", bool multiple = false, string errorMessage = "字典值不合法!") { DictTypeCode = dictTypeCode; + Multiple = multiple; ErrorMessage = errorMessage; } /// - /// 字典值合规性校验 + /// 验证方法(异步安全) /// - /// - /// - /// - protected override ValidationResult IsValid(object? value, ValidationContext validationContext) + private async Task IsValidAsync(object? value, ValidationContext validationContext, CancellationToken cancellation) { - // 判断是否允许空值 - if (AllowNullValue && value == null) return ValidationResult.Success; + // 1. 空值检查 + if (IsEmpty(value)) + return ValidationResult.Success; - // 获取属性的类型 + // 2. 获取属性类型 var property = validationContext.ObjectType.GetProperty(validationContext.MemberName!); - if (property == null) return new ValidationResult($"未知属性: {validationContext.MemberName}"); - - string importHeaderName = GetImporterHeaderName(property, validationContext.MemberName); + if (property == null) + return new ValidationResult($"未知属性: {validationContext.MemberName}"); var propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; - // 先尝试从 ValidationContext 的依赖注入容器中拿服务,拿不到或类型不匹配时,再从全局的 App 容器中获取 - if (validationContext.GetService(typeof(SysDictDataService)) is not SysDictDataService sysDictDataService) - sysDictDataService = App.GetRequiredService(); - - // 获取字典值列表 - var dictDataList = sysDictDataService.GetDataList(DictTypeCode).GetAwaiter().GetResult(); - - // 使用 HashSet 来提高查找效率 - var dictHash = new HashSet(dictDataList.Select(u => u.Value)); - - // 判断是否为集合类型 - if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(List<>)) - { - // 如果是空集合并且允许空值,则直接返回成功 - if (value == null && AllowNullValue) return ValidationResult.Success; - - // 处理集合为空的情况 - if (value is not IEnumerable collection) return ValidationResult.Success; - - // 获取集合的元素类型 - var elementType = propertyType.GetGenericArguments()[0]; - var underlyingElementType = Nullable.GetUnderlyingType(elementType) ?? elementType; - - // 如果元素类型是枚举,则逐个验证 - if (underlyingElementType.IsEnum) - { - foreach (var item in collection) - { - if (item == null && AllowNullValue) continue; - - if (!Enum.IsDefined(underlyingElementType, item!)) - return new ValidationResult($"提示:{ErrorMessage}|枚举值【{item}】不是有效的【{underlyingElementType.Name}】枚举类型值!", [importHeaderName]); - } - return ValidationResult.Success; - } - - foreach (var item in collection) - { - if (item == null && AllowNullValue) continue; - - var itemString = item?.ToString(); - if (!dictHash.Contains(itemString)) - return new ValidationResult($"提示:{ErrorMessage}|字典【{DictTypeCode}】不包含【{itemString}】!", [importHeaderName]); - } - - return ValidationResult.Success; - } - - var valueAsString = value?.ToString(); - - // 是否忽略空字符串 - if (AllowEmptyStrings && string.IsNullOrEmpty(valueAsString)) return ValidationResult.Success; - - // 枚举类型验证 + // 3. 枚举类型校验 if (propertyType.IsEnum) { - if (!Enum.IsDefined(propertyType, value!)) return new ValidationResult($"提示:{ErrorMessage}|枚举值【{value}】不是有效的【{propertyType.Name}】枚举类型值!", [importHeaderName]); + return !Enum.IsDefined(propertyType, value!) ? new ValidationResult($"提示:{ErrorMessage}|枚举值【{value}】不是有效的【{propertyType.Name}】枚举类型值!") : ValidationResult.Success; + } + + // 4. 获取字典服务 + var sysDictDataService = validationContext.GetService(typeof(SysDictDataService)) as SysDictDataService + ?? App.GetRequiredService(); + + // 5. 异步获取字典数据 + var dictDataList = await sysDictDataService.GetDataList(DictTypeCode); + var dictHashSet = new HashSet(dictDataList.Select(d => d.Code), StringComparer.OrdinalIgnoreCase); + + // 6. 单选校验 + if (!Multiple) + { + var valStr = value?.ToString()?.Trim(); + if (string.IsNullOrEmpty(valStr) || !dictHashSet.Contains(valStr)) + { + return new ValidationResult($"提示:{ErrorMessage}|字典【{DictTypeCode}】不包含【{value}】!"); + } return ValidationResult.Success; } - if (!dictHash.Contains(valueAsString)) - return new ValidationResult($"提示:{ErrorMessage}|字典【{DictTypeCode}】不包含【{valueAsString}】!", [importHeaderName]); + // 7. 多选校验 + if (value is string stringValue) + { + var codes = stringValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + foreach (var code in codes) + { + if (!dictHashSet.Contains(code)) + { + return new ValidationResult($"提示:{ErrorMessage}|字典【{DictTypeCode}】不包含【{code}】!"); + } + } + return ValidationResult.Success; + } - return ValidationResult.Success; + // 集合类数据校验 + if (value is IList listValue) + { + foreach (var item in listValue) + { + var itemStr = item?.ToString()?.Trim(); + if (string.IsNullOrEmpty(itemStr) || !dictHashSet.Contains(itemStr)) + { + return new ValidationResult($"提示:{ErrorMessage}|字典【{DictTypeCode}】不包含【{item}】!"); + } + } + return ValidationResult.Success; + } + + // 8. 类型不匹配 + return new ValidationResult($"提示:{ErrorMessage}|不支持的值类型:{value?.GetType().Name},多选时应为逗号分隔字符串或集合类型。"); } /// - /// 获取本字段上 [ImporterHeader(Name = "xxx")] 里的Name,如果没有则使用defaultName. - /// 用于在从excel导入数据时,能让调用者知道是哪个字段验证失败,而不是抛异常 + /// 同步入口(兼容旧版调用) /// - private static string GetImporterHeaderName(PropertyInfo property, string defaultName) + protected override ValidationResult IsValid(object? value, ValidationContext validationContext) { - var importerHeader = property.GetCustomAttribute(); - string importerHeaderName = importerHeader?.Name ?? defaultName; - return importerHeaderName; + return IsValidAsync(value, validationContext, CancellationToken.None).GetAwaiter().GetResult(); + } + + /// + /// 判断值是否为空(null、空字符串、空集合) + /// + private static bool IsEmpty(object? value) + { + return value switch + { + null => true, + string s => string.IsNullOrWhiteSpace(s), + IList list => list.Count == 0, + _ => false + }; } } \ No newline at end of file