1. 背景与目标
最近项目中加入多语言翻译需求:一个实体列表里,可能有多处需要显示化 / 翻译的字段,比如枚举状态、国家名称等。一开始想用ActionFilter来实现,但是项目要用到hangfire,我们经常要用hangfire去导出excel,这里经常需要翻译,所以舍弃了这种思路,还是选择在业务代码中写入相关的翻译代码,但是写多了以后就感觉很麻烦了,于是想着再封装一个方法方便进行翻译以及赋值。
目标:以链式流式语法收集所有“待翻译文本 + 回填策略”,统一调用服务一次,最后批量回填。
2. 期望的调用方式(直觉 DSL)
最开始脑海里大概是这样设计的:
var res = await users
.MapTranslation(a => a.IsEnable, (a, translated) => a.IsEnableName = translated)
.MapTranslation(a => a.Country, (a, translated) => a.Country = translated)
.ExecuteAsync();
语义解读:
MapTranslation:收集一个“源字段 + 翻译后如何写回”的规则ExecuteAsync():统一翻译 + 回填翻译后的内容
3. 初版实现与问题分析
下面是最初的一个版本:
public static class ExpressionBuilder
{
public static TranslatedContext<T> BuildCollect<T>(IEnumerable<T> items) where T : class
=> new(items);
}
public class TranslatedContext<T> where T : class
{
private readonly IEnumerable<T> _source;
private readonly List<(T item, string value, Action<T, string> action)> _translations;
public TranslatedContext(IEnumerable<T> source){
_source = source;
_translations=new()
}
public IEnumerable<T> MapTranslation(Func<T, string> selector, Action<T, string> action)
{
foreach (var item in _source)
{
var result = selector(item);
_translations.Add((item, result, action));
yield return item;
}
}
public IEnumerable<T> Execute()
{
foreach (var (item, enumValue, action) in _translations)
action(item, enumValue);
foreach (var item in _source)
yield return item;
}
}
4. 服务注入的演进
如果把翻译Service加入流程中的话就要在 BuildCollect 里接收翻译服务:
var res = await users
.BuildCollect(translateService)
.MapTranslation(a => a.IsEnable, (a, translated) => a.IsEnableName = translated)
.ExecuteAsync();
又觉得顺序不自然,有点反直觉了,于是思考了一下将 BuildCollect 挪进 TranslateService后,长这样:
var res = await translateService
.BuildCollect(users)
.MapTranslation(a => a.IsEnable.GetDescription(), (a, translated) => a.IsEnableName = translated)
.ExecuteAsync();
5. 改进代码
public class TranslateContext<T> where T : class
{
private readonly TranslateService _service;
private readonly IEnumerable<T> _items;
private readonly List<(T Item, string SourceText, Action<T, string> Action)> _translations;
public TranslateContext(TranslateService service, IEnumerable<T> items)
{
_service = service;
_items = items ?? throw new ArgumentNullException(nameof(items));
_translations = new();
}
/// <summary>
/// 收集“待翻译文本 + 回填策略”
/// </summary>
public TranslateContext<T> MapTranslation(Func<T, string> selector, Action<T, string> action)
{
ArgumentNullException.ThrowIfNull(selector);
ArgumentNullException.ThrowIfNull(action);
foreach (var item in _items)
{
var source = selector(item);
_translations.Add((item, source, action));
}
return this;
}
public async Task<IEnumerable<T>> ExecuteAsync()
{
var pending = _translations
.Select(t => t.SourceText)
.Where(s => !string.IsNullOrWhiteSpace(s))
.Select(s => s.Trim())
.Distinct()
.ToList();
var dic = await _service.GetTranslationDic(pending);
foreach (var (item, source, action) in _translations)
{
action(item, dic.TryGetValue(source, out var translated) ? translated : source);
}
return _items;
}
}
public class TranslateService
{
private readonly ILogger<TranslateService> _logger;
public TranslateService(ILogger<TranslateService> logger) => _logger = logger;
public TranslateContext<T> BuildCollect<T>(IEnumerable<T> items) where T : class
=> new(this, items);
public async Task<Dictionary<string, string>> GetTranslationDic(IEnumerable<string> pendingTranslations)
{
if (pendingTranslations == null)
return new();
var dic = new Dictionary<string, string>();
foreach (var text in pendingTranslations)
{
dic[text] = text switch
{
"启用" => "Enable",
"禁用" => "Disable",
_ => text
};
}
return await Task.FromResult(dic);
}
}
6. 表达式树增强版:自动生成 Setter
现在其实已经大差不差了,但是一看每次都需要写(a, translated) => a.Property = translated,这部分,感觉有点麻烦,或许我们可以再优化一下?
我研究一下private readonly List<(T Item, string SourceText, Action<T, string> Action)> _translations;这部分,我发现其实可以用表达式树构建一下,因为其实只涉及到了两个字段,一个是待翻译,一个是翻译后要被回填的字段,所以我想了下。或许我可以简化一下,于是我创建一个MapTranslation的重载
public TranslateContext<T> MapTranslation(
Func<T, string> sourceExpr,
Expression<Func<T, string>> targetExpr)
{
if (sourceExpr == null || targetExpr == null) throw new ArgumentNullException();
// var getter = sourceExpr.Compile();
// 解析目标属性
if (targetExpr.Body is not MemberExpression member || member.Member is not PropertyInfo prop)
throw new InvalidOperationException("目标表达式必须是可写属性");
if (!prop.CanWrite) throw new InvalidOperationException("属性不可写");
// 构造 setter 委托: (T e, string v) => e.Prop = v;
var pEntity = Expression.Parameter(typeof(T), "e"); //创建一个类型为 T 的参数,名字叫 "e"
var pValue = Expression.Parameter(typeof(string), "v"); //创建一个类型为 string 的参数,名字叫 "v"
var assign = Expression.Assign(Expression.Property(pEntity, prop), pValue); // 创建赋值表达式 e.Prop = v
var setter = Expression.Lambda<Action<T, string>>(assign, pEntity, pValue).Compile(); // 创建 Lambda 表达式并编译成委托
foreach (var item in _items)
{
var source = sourceExpr(item);
_translations.Add((item, source, setter));
}
return this;
}
调用示例:
var res = await translateService
.BuildCollect(users)
.MapTranslation(u => u.IsEnable.GetDescription(), u => u.IsEnableName)
.ExecuteAsync();
但是这样的话会有一些性能缺点,运行期表达式解析 + 动态编译会有轻微的性能开销。
7. 示例输出
示例运行结果(与原始枚举翻译示例一致):
[
{ "name": "张三", "country": "中国", "isEnable": 1, "isEnableName": "Enable" },
{ "name": "李四", "country": "美国", "isEnable": 0, "isEnableName": "Disable" },
{ "name": "王五", "country": "英国", "isEnable": 1, "isEnableName": "Enable" },
{ "name": "赵六", "country": "法国", "isEnable": 0, "isEnableName": "Disable" },
{ "name": "田七", "country": "德国", "isEnable": 1, "isEnableName": "Enable" }
]
8. 可进一步扩展的方向
- 缓存翻译结果(本地内存 / 分布式缓存)
将表达式树重载的 setter 生成再做一次缓存,避免重复编译
9.表达式树重载的 setter 生成再做一次缓存
其实这一步操作只需要考虑一个点就可以了:用什么当缓存key 我思考了一下,因为我是一个Action委托,我是没有入参的,所以我们只需要把两个入参缓存了即可,也就是T和属性名,所以我的Key设计为:
var key = $"{typeof(T).FullName}:{prop.Name}";
FullName表示的是一个类型的完整名称,是包括命名空间的,比如typeof(System.String).FullName 的结果是 System.String
prop.Name就是属性名了
相关代码如下,只要添加private static readonly Dictionary<string, Action<T, string>> _setterCache = [];再修改表达式树构建委托那部分就可以
var key = $"{typeof(T).FullName}:{prop.Name}";
if (!_setterCache.TryGetValue(key, out var setter))
{
// 构造 setter 委托: (T e, string v) => e.Prop = v;
var pEntity = Expression.Parameter(typeof(T), "e"); //创建一个类型为 T 的参数,名字叫 "e"
var pValue = Expression.Parameter(typeof(string), "v"); //创建一个类型为 string 的参数,名字叫 "v"
var assign = Expression.Assign(Expression.Property(pEntity, prop), pValue); // 创建赋值表达式 e.Prop = v
setter = Expression.Lambda<Action<T, string>>(assign, pEntity, pValue).Compile(); // 创建 Lambda 表达式并编译成委托
_setterCache[key] = setter;
Console.WriteLine($"缓存新委托: {key},当前缓存数量:{_setterCache.Count}");
}