EF Core中如何在值转换器里通过DI获取作用域对象,实现可本地化JSON的序列化与反序列化?
EF Core中如何在值转换器里通过DI获取作用域对象,实现可本地化JSON的序列化与反序列化?
这个需求我之前做项目的时候刚好碰到过,常规的值转换器确实搞不定——因为它是在模型构建阶段就初始化好的单例,没法直接拿到作用域里的本地化服务。不过你想到用IMaterializationInterceptor的思路完全正确,咱们一步步来实现这个可本地化的JSON序列化和反序列化逻辑:
一、核心思路梳理
序列化时,我们要把每个可本地化属性生成「默认语言字段+额外语言字段」的结构(比如title、title_fr);反序列化时,从DI获取当前请求的语言设置,读取JSON中对应的字段值赋值给实体属性。
因为值转换器无法直接获取作用域服务,所以序列化环节改用SaveChangesInterceptor(在保存前处理),反序列化用IMaterializationInterceptor(在实体实例化时处理)。
二、定义必要的服务和实体
首先我们需要两个核心服务:一个存储当前请求的语言设置,一个提供本地化数据;再定义对应的实体类:
// 作用域服务:存储当前请求的语言设置 public interface ILanguageSettings { string CurrentLanguage { get; set; } // 比如"fr"、"de"、"en" } public class ScopedLanguageSettings : ILanguageSettings { public string CurrentLanguage { get; set; } = "en"; // 默认英语 } // 作用域服务:提供本地化数据(你可以根据实际业务从资源文件/数据库读取) public interface ILocalizationService { IEnumerable<string> AdditionalLanguages { get; } // 额外支持的语言列表 string GetLocalizedValue(string defaultKey, string language); // 根据默认值和语言获取本地化文本 } public class LocalizationServiceImpl : ILocalizationService { public IEnumerable<string> AdditionalLanguages => new[] { "fr", "de" }; public string GetLocalizedValue(string defaultKey, string language) { // 示例逻辑,实际替换成你的本地化数据源 if (defaultKey == "apple") { return language switch { "fr" => "pomme", "de" => "Apfel", _ => defaultKey }; } return defaultKey; } } // 实体类:业务用的本地化属性不映射到数据库,用单独字段存多语言JSON public class Product { public int Id { get; set; } // 业务层使用的本地化属性,不映射到数据库 public LocalizedText Title { get; set; } // 存储到数据库的多语言JSON字段 public string LocalizedJson { get; set; } } // 自定义本地化文本类,方便业务层统一处理 public class LocalizedText { public string Value { get; set; } }
三、实现序列化逻辑(SaveChangesInterceptor)
在实体保存到数据库前,利用拦截器获取本地化服务,生成包含多语言字段的JSON:
public class LocalizedSaveInterceptor : SaveChangesInterceptor { private readonly ILocalizationService _localizationService; public LocalizedSaveInterceptor(ILocalizationService localizationService) { _localizationService = localizationService; } public override async ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default) { var context = eventData.Context; if (context == null) return await base.SavingChangesAsync(eventData, result, cancellationToken); // 筛选出新增/修改的Product实体 var productEntries = context.ChangeTracker.Entries<Product>() .Where(e => e.State is EntityState.Added or EntityState.Modified); foreach (var entry in productEntries) { var product = entry.Entity; if (product.Title == null) continue; // 构建多语言JSON对象 var jsonObj = new Newtonsoft.Json.Linq.JObject(); // 默认语言字段 jsonObj["title"] = product.Title.Value; // 遍历额外语言,生成对应字段 foreach (var lang in _localizationService.AdditionalLanguages) { var localizedValue = _localizationService.GetLocalizedValue(product.Title.Value, lang); jsonObj[$"title_{lang}"] = localizedValue; } // 赋值给数据库字段 product.LocalizedJson = jsonObj.ToString(); } return await base.SavingChangesAsync(eventData, result, cancellationToken); } }
四、实现反序列化逻辑(IMaterializationInterceptor)
当从数据库读取实体时,拦截器会在实例化完成后触发,我们在这里根据当前语言读取JSON中对应的字段:
public class LocalizedMaterializationInterceptor : IMaterializationInterceptor { private readonly ILanguageSettings _languageSettings; public LocalizedMaterializationInterceptor(ILanguageSettings languageSettings) { _languageSettings = languageSettings; } public void Materialized(MaterializationInterceptionData interceptionData, object entity) { if (entity is not Product product) return; if (string.IsNullOrEmpty(product.LocalizedJson)) return; var jsonObj = Newtonsoft.Json.Linq.JObject.Parse(product.LocalizedJson); string currentLang = _languageSettings.CurrentLanguage; string titleValue; // 优先读取当前语言字段,找不到则回退到默认语言 if (currentLang == "en") { titleValue = jsonObj["title"]?.ToString(); } else { titleValue = jsonObj[$"title_{currentLang}"]?.ToString() ?? jsonObj["title"]?.ToString(); } product.Title = new LocalizedText { Value = titleValue }; } }
五、注册服务和拦截器
在Program.cs(或Startup.cs)里把这些服务和拦截器注册到DI容器:
// 注册作用域服务 builder.Services.AddScoped<ILanguageSettings, ScopedLanguageSettings>(); builder.Services.AddScoped<ILocalizationService, LocalizationServiceImpl>(); // 注册拦截器 builder.Services.AddScoped<LocalizedSaveInterceptor>(); builder.Services.AddScoped<LocalizedMaterializationInterceptor>(); // 配置DbContext并添加拦截器 builder.Services.AddDbContext<AppDbContext>(options => { options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")); // 从服务提供者获取拦截器实例 options.AddInterceptors(sp => { sp.GetRequiredService<LocalizedSaveInterceptor>(); sp.GetRequiredService<LocalizedMaterializationInterceptor>(); }); });
六、模型配置
最后在DbContext的OnModelCreating里,把业务用的本地化属性设为未映射,避免EF Core试图将其存入数据库:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Product>() .Property(p => p.Title) .HasNoMapping(); // 标记为不映射到数据库列 modelBuilder.Entity<Product>() .Property(p => p.LocalizedJson) .HasColumnType("nvarchar(max)"); }
关键注意点
- 为什么不用值转换器?:值转换器是模型构建阶段初始化的单例,无法直接获取作用域内的DI服务(比如当前请求的语言设置),拦截器的方式更灵活。
- ** fallback逻辑**:反序列化时如果找不到当前语言的字段,一定要回退到默认语言,避免出现空值。
- 性能优化:如果处理大量实体,反射操作可能有性能损耗,可以提前缓存属性信息或用表达式树替代反射。
备注:内容来源于stack exchange,提问作者user2900970




