You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

EF Core中如何在值转换器里通过DI获取作用域对象,实现可本地化JSON的序列化与反序列化?

EF Core中如何在值转换器里通过DI获取作用域对象,实现可本地化JSON的序列化与反序列化?

这个需求我之前做项目的时候刚好碰到过,常规的值转换器确实搞不定——因为它是在模型构建阶段就初始化好的单例,没法直接拿到作用域里的本地化服务。不过你想到用IMaterializationInterceptor的思路完全正确,咱们一步步来实现这个可本地化的JSON序列化和反序列化逻辑:

一、核心思路梳理

序列化时,我们要把每个可本地化属性生成「默认语言字段+额外语言字段」的结构(比如titletitle_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)");
}

关键注意点

  1. 为什么不用值转换器?:值转换器是模型构建阶段初始化的单例,无法直接获取作用域内的DI服务(比如当前请求的语言设置),拦截器的方式更灵活。
  2. ** fallback逻辑**:反序列化时如果找不到当前语言的字段,一定要回退到默认语言,避免出现空值。
  3. 性能优化:如果处理大量实体,反射操作可能有性能损耗,可以提前缓存属性信息或用表达式树替代反射。

备注:内容来源于stack exchange,提问作者user2900970

火山引擎 最新活动