基于自定义DataAnnotationsModelMetadataProvider实现RequiredAttribute错误消息多语言切换
老兄,你选的方向完全对——自定义DataAnnotationsModelMetadataProvider正是实现从数据库这类自定义源获取本地化错误消息的最佳方案之一。我来帮你把这个实现补全,并且给你一些关键的优化建议:
1. 完善自定义元数据提供器的核心逻辑
首先要解决依赖注入的问题:你之前用Lazy<ILocalizationService>直接实例化的方式不够优雅,而且会导致依赖硬编码,不利于测试和维护。更好的方式是通过构造函数注入本地化服务,然后在CreateMetadata方法里遍历属性上的注解,替换错误消息。
public class LocalizationModelMetadataProvider : DataAnnotationsModelMetadataProvider { private readonly ILocalizationService _localizationService; // 通过构造函数注入本地化服务,避免硬编码实例化 public LocalizationModelMetadataProvider(ILocalizationService localizationService) { _localizationService = localizationService; } protected override ModelMetadata CreateMetadata( IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName) { // 先调用基类获取默认的模型元数据 var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName); // 遍历所有验证注解,处理需要本地化的类型 foreach (var validationAttr in attributes.OfType<ValidationAttribute>()) { // 重点处理RequiredAttribute,你可以扩展到其他注解(比如StringLength、Range等) if (validationAttr is RequiredAttribute requiredAttr) { // 定义翻译键:建议用「注解类型_实体类名_属性名」的规则,方便数据库存储和查询 var translationKey = $"Required_{containerType?.Name ?? modelType.Name}_{propertyName}"; // 获取当前UI文化(比如zh-CN、en-US) var currentCulture = CultureInfo.CurrentUICulture.Name; // 从数据库获取本地化消息 var localizedMessage = _localizationService.GetTranslation(translationKey, currentCulture); // 如果获取到有效翻译,替换默认错误消息;否则保留原消息 if (!string.IsNullOrWhiteSpace(localizedMessage)) { requiredAttr.ErrorMessage = localizedMessage; } } // 扩展:添加其他需要本地化的注解处理 // else if (validationAttr is StringLengthAttribute lengthAttr) // { // var translationKey = $"StringLength_{containerType.Name}_{propertyName}"; // var localizedMessage = _localizationService.GetTranslation(translationKey, CultureInfo.CurrentUICulture.Name); // if (!string.IsNullOrWhiteSpace(localizedMessage)) // { // lengthAttr.ErrorMessage = localizedMessage; // } // } } return metadata; } }
2. 实现数据库驱动的本地化服务
接下来需要实现ILocalizationService,从数据库中读取翻译内容。假设你的数据库表结构大致如下(可以根据自己的需求调整):
表名:LocalizationEntries
- Key (NVARCHAR):翻译键(比如"Required_User_Email")
- CultureCode (NVARCHAR):文化编码(比如"zh-CN")
- Value (NVARCHAR):本地化后的消息内容
对应的服务实现:
public interface ILocalizationService { // 根据翻译键和文化编码获取本地化内容 string GetTranslation(string key, string cultureCode); } public class DbLocalizationService : ILocalizationService { private readonly YourDbContext _dbContext; private readonly IMemoryCache _cache; // 缓存过期时间,比如5分钟 private static readonly TimeSpan _cacheExpiry = TimeSpan.FromMinutes(5); // 注入数据库上下文和缓存服务,优化查询性能 public DbLocalizationService(YourDbContext dbContext, IMemoryCache cache) { _dbContext = dbContext; _cache = cache; } public string GetTranslation(string key, string cultureCode) { // 先从缓存获取,避免频繁查询数据库 var cacheKey = $"Localization_{key}_{cultureCode}"; if (_cache.TryGetValue(cacheKey, out string cachedValue)) { return cachedValue; } // 数据库查询:优先匹配当前文化,没有则回退到默认文化(比如"en-US") var translation = _dbContext.LocalizationEntries .FirstOrDefault(x => x.Key == key && x.CultureCode == cultureCode) ?.Value ?? _dbContext.LocalizationEntries .FirstOrDefault(x => x.Key == key && x.CultureCode == "en-US") ?.Value; // 将结果存入缓存 if (translation != null) { _cache.Set(cacheKey, translation, _cacheExpiry); } return translation; } }
3. 注册服务到依赖注入容器
最后需要在项目的启动配置中,注册自定义的元数据提供器和本地化服务:
.NET 6+ (Program.cs)
var builder = WebApplication.CreateBuilder(args); // 注册数据库上下文(根据你的实际情况调整) builder.Services.AddDbContext<YourDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); // 注册本地化服务和缓存 builder.Services.AddMemoryCache(); builder.Services.AddScoped<ILocalizationService, DbLocalizationService>(); // 替换默认的模型元数据提供器为自定义实现 builder.Services.AddSingleton<IModelMetadataProvider>(sp => new LocalizationModelMetadataProvider(sp.GetRequiredService<ILocalizationService>())); // 添加MVC/Controllers服务 builder.Services.AddControllersWithViews(); var app = builder.Build(); // ... 其他中间件配置 ... app.Run();
4. 几个实用的优化点
- 缓存策略:上面的实现已经加入了内存缓存,你还可以根据需求改用分布式缓存(比如Redis),适合多实例部署的场景。
- 批量查询:如果一个页面有多个需要本地化的属性,可以考虑批量查询翻译键,减少数据库查询次数。
- 注解扩展:可以自定义一个带翻译键的注解(比如
LocalizedRequiredAttribute),让翻译键的配置更灵活,不用硬编码规则。 - 错误兜底:如果数据库查询失败,确保返回
null,让注解使用默认的错误消息,避免影响正常业务流程。
内容的提问来源于stack exchange,提问作者Nadya
相关产品推荐
相关产品推荐

