csharp通过对象和模板字符串解析模板
通过模板字符串和一个匿名对象来解析字符串,这种需求比较常见,对于比较简单的模板字符串,我们可以直接替换,但对于比较复杂的模板,我们可以通过模板工具来实现。
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);// 解析错误就直接返回templateif (parsedTemplate.HasErrors){// 错误信息:parsedTemplate.Messagesreturn 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;// 检查是否为JObjectif (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);// 保留模板中的原始属性,并将转换后的属性值添加到ScriptObjectscriptObject[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();// 检查是否为JObjectif (value is JObject jObject){return ConvertJObjectToScriptObject(jObject);}// 检查是否为JTokenif (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.142var 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); // 你好,我是时秒。我目前居住在四川成都。我特别喜欢 李白的长风破浪会有时,直挂云帆济沧海 赵恒的书中自有颜如玉,书中自有黄金屋
}
