通过模板字符串和一个匿名对象来解析字符串,这种需求比较常见,对于比较简单的模板字符串,我们可以直接替换,但对于比较复杂的模板,我们可以通过模板工具来实现。
Scriban简介
Scriban是一个适用于.Net的一款轻量级的脚本引擎。
通过包管理进行安装:

官方案例:
var template = Template.Parse("Hello {{name}}!");
var result = template.Render(new { Name = "World" }); // => "Hello World!"
对于这种简单的模板可以直接使用,但针对嵌套的会出现问题,如:
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模板渲染工具
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;
}
}
使用示例:
/// <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); // 你好,我是时秒。我目前居住在四川成都。我特别喜欢 李白的长风破浪会有时,直挂云帆济沧海 赵恒的书中自有颜如玉,书中自有黄金屋
}