通过模板字符串和一个匿名对象来解析字符串,这种需求比较常见,对于比较简单的模板字符串,我们可以直接替换,但对于比较复杂的模板,我们可以通过模板工具来实现。

Scriban简介

Scriban是一个适用于.Net的一款轻量级的脚本引擎。

通过包管理进行安装:

nuget_scriban
官方案例:

1
2
var template = Template.Parse("Hello {{name}}!");
var result = template.Render(new { Name = "World" }); // => "Hello World!"

对于这种简单的模板可以直接使用,但针对嵌套的会出现问题,如:

1
2
3
4
5
6
7
var obj = new
{
Today = new { time = DateTime.Now, luckyNumber = 3.1415926525 },
};
var temp = Template.Parse("当前时间:{{Today.time}}");
var test = temp.Render(obj);
// Cannot get the member Today.time for a null object

针对有嵌套的对象,需要单独对传入的对象进行处理,以下是对该功能的封装

TemplateUtil模板渲染工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
public class TemplateUtil
{
/// <summary>
/// 渲染模板
/// </summary>
/// <param name="template">模板内容</param>
/// <param name="anonymousObject">匿名对象</param>

public static string RenderTemplate(string template, object anonymousObject)
{
if (string.IsNullOrEmpty(template) || anonymousObject == null)
return template;
try
{
// 通过对象构建脚本对象示例
var scriptObject = (ScriptObject)ToScriptObject(anonymousObject);
// 解析模板
var parsedTemplate = Template.Parse(template);
// 创建模板引擎的执行上下文
var context = new TemplateContext();
// 将ScriptObject添加到模板的全局作用域中
context.PushGlobal(scriptObject);
// 解析错误就直接返回template
if (parsedTemplate.HasErrors)
{
// 错误信息:parsedTemplate.Messages
return template;
}
// 获取解析后的字符串
var result = parsedTemplate.Render(context);
return result;
}
catch
{
return template;
}
}

/// <summary>
/// 将匿名对象转换为Scriban可识别的ScriptObject【解决嵌套对象中非字符字段解析问题】
/// </summary>
/// <param name="anonymousObject">要转换的匿名对象</param>
/// <returns>转换后的ScriptObject</returns>
public static object ToScriptObject(object anonymousObject)
{
// 空值检查
if (anonymousObject == null) return null;
// 检查是否为JObject
if (anonymousObject is JObject jObject)
{
return ConvertJObjectToScriptObject(jObject);
}
// 检查是否为JToken(处理JValue等)
if (anonymousObject is JToken jToken)
{
return ConvertJTokenToObject(jToken);
}
// 创建新的 ScriptObject 来存储转换后的数据
var scriptObject = new ScriptObject();
// 获取匿名对象的实际类型
var type = anonymousObject.GetType();
// 通过反射获取对象的所有属性
foreach (var property in type.GetProperties())
{ // 获取属性值
var value = property.GetValue(anonymousObject);
// 保留模板中的原始属性,并将转换后的属性值添加到ScriptObject
scriptObject[property.Name] = ConvertValue(value);
}
return scriptObject;
}

/// <summary>
/// 将JObject转换为ScriptObject
/// </summary>
/// <param name="jObject">要转换的JObject</param>
/// <returns>转换后的ScriptObject</returns>
private static ScriptObject ConvertJObjectToScriptObject(JObject jObject)
{
var scriptObject = new ScriptObject();
foreach (var property in jObject.Properties())
{
scriptObject[property.Name] = ConvertJTokenToObject(property.Value);
}
return scriptObject;
}

/// <summary>
/// 将JToken转换object
/// </summary>
/// <param name="token">要转换的JToken</param>
/// <returns>转换后的对象</returns>
private static object ConvertJTokenToObject(JToken token)
{
if (token == null) return null;
return token.Type switch
{
JTokenType.Object => ConvertJObjectToScriptObject((JObject)token),
JTokenType.Array => ConvertJArrayToList((JArray)token),
JTokenType.Integer => token.Value<long>(),
JTokenType.Float => token.Value<double>(),
JTokenType.String => token.Value<string>(),
JTokenType.Boolean => token.Value<bool>(),
JTokenType.Null => null,
JTokenType.Date => token.Value<DateTime>(),
JTokenType.Guid => token.Value<Guid>(),
_ => token.ToString(),
};
}

/// <summary>
/// 将JArray转换为List
/// </summary>
/// <param name="jArray">要转换的JArray</param>
/// <returns>转换后的列表</returns>
private static List<object> ConvertJArrayToList(JArray jArray)
{
var list = new List<object>();
foreach (var item in jArray)
{
list.Add(ConvertJTokenToObject(item));
}
return list;
}

/// <summary>
/// 递归转换属性值
/// 处理各种类型的值:基本类型、匿名对象、集合等
/// </summary>
/// <param name="value">要转换的值</param>
/// <returns>转换后的值</returns>

private static object ConvertValue(object value)
{
// 空值检查
if (value == null) return null;
// 获取值类型
var valueType = value.GetType();
// 检查是否为JObject
if (value is JObject jObject)
{
return ConvertJObjectToScriptObject(jObject);
}
// 检查是否为JToken
if (value is JToken jToken)
{
return ConvertJTokenToObject(jToken);
}
// 如果是匿名类型,递归转换
if (valueType.IsClass && valueType.Name.Contains("AnonymousType"))
{
return ToScriptObject(value);
}
// 如果是数组或集合,就单独处理
if (IsCollection(valueType))
{
return ConvertEnumerable(value);
}
// 直接返回值
return value;
}

/// <summary>
/// 判断是否为集合或数组
/// </summary>
private static bool IsCollection(Type type)
{
// 排除字符串(虽然字符串实现了 IEnumerable<char>,但通常不作为集合处理)
if (type == typeof(string)) return false;
// 1. 检查是否为数组
if (type.IsArray)
{
return true;
}
// 2. 检查是否实现了 IEnumerable<> 泛型接口
if (type.IsGenericType)
{
var interfaces = type.GetInterfaces();
if (interfaces.Any(i =>
i.IsGenericType &&
i.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
{
return true;
}
}
// 3. 检查是否实现了非泛型 IEnumerable 接口
if (typeof(IEnumerable).IsAssignableFrom(type))
{
return true;
}
return false;
}

/// <summary>
/// 转换可枚举对象
/// </summary>
/// <param name="enumerable">要转换的可枚举对象</param>
/// <returns>转换后的列表</returns>
private static object ConvertEnumerable(object enumerable)
{
// 创建列表来存储转换后的元素
var list = new List<object>();
// 遍历原始集合中的每个元素
foreach (var item in (IEnumerable)enumerable)
{
// 递归转换每个元素并添加到列表
list.Add(ConvertValue(item));
}
return list;
}
}

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/// <summary>
/// 使用样例(仅支持匿名对象和JObject的渲染)
/// </summary>
public static void Test()
{
// 匿名对象
var obj = new
{
Name = "时分",
Address = new { Province = "四川", City = "成都" },
Today = new { time = DateTime.Now, luckyNumber = 3.1415926525 },
Scores = new[] { 80, 85, 90 },
爱好 = new
{
音乐 = "燕归巢",
游戏 = "原神"
}
};
// 文本处理
var textTemplate = "你好,我是{{ Name }}。我目前居住在{{ Address['Province'] }}{{ Address.City }},我喜欢听{{ 爱好.音乐 }}这首歌,偶尔玩玩{{ 爱好.游戏 }}";
var text = TemplateUtil.RenderTemplate(textTemplate, obj);
Console.WriteLine(text); // 你好,我是时分。我目前居住在四川成都,我喜欢听燕归巢这首歌,偶尔玩玩原神
// 日期处理 时间参数不能是date,方法date.to_string冲突
var dateTemplate = "当前时间: {{ Today.time | date.to_string '%Y-%m-%d' }}"; // Y(年)、m(月)、d(日)、H(时)、M(分)、S(秒)
var date = TemplateUtil.RenderTemplate(dateTemplate, obj);
Console.WriteLine(date); // 当前时间: 2025-11-13 17:15:06
// 数字处理
var number1 = TemplateUtil.RenderTemplate("{{ Today.luckyNumber | math.round 3 }}", obj); // 四舍五入并指定小数位数
Console.WriteLine(number1); // 3.142
var number2 = TemplateUtil.RenderTemplate("{{ Today.luckyNumber | math.format 'p2'}}", obj); // 百分数 P(n)表示保留n位小数
Console.WriteLine(number2); // 314.16 %
// 循环处理
var scoreTemplate = "分数列表:{{ for score in Scores }} {{ score }} 分 {{ end }}";
var score = TemplateUtil.RenderTemplate(scoreTemplate, obj);
Console.WriteLine(score); // 分数列表: 80 分 85 分 90 分

// JObject对象
var jobj = new JObject
{
["Name"] = "时秒",
["Address"] = new JObject
{
["Province"] = "四川",
["City"] = "成都",
},
["座右铭"] = new JArray
{
new JObject
{
["Sentence"] = "长风破浪会有时,直挂云帆济沧海",
["Author"] = "李白"
},
new JObject
{
["Sentence"] = "书中自有颜如玉,书中自有黄金屋",
["Author"] = "赵恒"
}
}
};
var textTemplate2 = "你好,我是{{ Name }}。我目前居住在{{ Address['Province'] }}{{ Address.City }}。" +
"我特别喜欢{{ for poem in 座右铭 }} {{ poem.Author }}的{{ poem.Sentence }} {{ end }}";
var text2 = TemplateUtil.RenderTemplate(textTemplate2, jobj);
Console.WriteLine(text2); // 你好,我是时秒。我目前居住在四川成都。我特别喜欢 李白的长风破浪会有时,直挂云帆济沧海 赵恒的书中自有颜如玉,书中自有黄金屋
}