如诗
如诗
发布于 2025-11-14 / 8 阅读
0
0

csharp通过对象和模板字符串解析模板

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

Scriban简介

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

通过包管理进行安装:

nuget_scriban
官方案例:

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); // 你好,我是时秒。我目前居住在四川成都。我特别喜欢 李白的长风破浪会有时,直挂云帆济沧海  赵恒的书中自有颜如玉,书中自有黄金屋 
}

评论