【问题标题】:What's a good way of doing string templating in .NET?在 .NET 中进行字符串模板化的好方法是什么?
【发布时间】:2010-10-18 12:23:56
【问题描述】:

我需要向用户发送电子邮件通知,并且我需要允许管理员为邮件正文(可能还有标题)提供模板。

我想要像 string.Format 这样的东西,它允许我提供命名替换字符串,所以模板可以如下所示:

Dear {User},

Your job finished at {FinishTime} and your file is available for download at {FileURL}.

Regards,

-- 
{Signature}

对我来说最简单的方法是什么?

【问题讨论】:

标签: c# .net string templating


【解决方案1】:

如果有人正在寻找替代方案——一个真正的 .NET 替代方案:

https://github.com/crozone/FormatWith | https://www.nuget.org/packages/FormatWith

一个不错的简单可扩展解决方案。谢谢crozone!

所以使用 FormatWith 中提供的字符串扩展名,这里有两个例子:

    static string emailTemplate = @"
Dear {User},

Your job finished at {FinishTime} and your file is available for download at {FileURL}.

-- 
{Signature}
    ";

//////////////////////////////////
/// 1. Use a dictionary that has the tokens as keys with values for the replacement
//////////////////////////////////
    public void TestUsingDictionary()
    {    
        var emailDictionary = new Dictionary<string, object>()
        {
            { "User", "Simon" },
            { "FinishTime", DateTime.Now },
            { "FileUrl", new Uri("http://example.com/dictionary") },
            { "Signature", $"Sincerely,{Environment.NewLine}Admin" }
        };

        var emailBody = emailTemplate.FormatWith(emailDictionary);

        System.Console.WriteLine(emailBody);
    }

//////////////////////////////////
/// 2. Use a poco with properties that match the replacement tokens
//////////////////////////////////
    public class MessageValues
    {
        public string User { get; set; } = "Simon";
        public DateTime FinishTime { get; set; } = DateTime.Now;
        public Uri FileURL { get; set; } = new Uri("http://example.com");
        public string Signature { get; set; } = $"Sincerely,{Environment.NewLine}Admin";
    }

    public void TestUsingPoco()
    {
        var emailBody = emailTemplate.FormatWith(new MessageValues());

        System.Console.WriteLine(emailBody);
    }


它也允许格式化替换内联。例如,尝试将emailTemplate 中的{FinishTime} 更改为{FinishTime:HH:mm:ss}

【讨论】:

  • 我真的很想用它,但是 char 作为大括号让它对我完全没用。我希望将来支持字符串作为大括号。
【解决方案2】:

使用模板引擎。 StringTemplate 就是其中之一,而且还有很多。

例子:

using Antlr.StringTemplate;
using Antlr.StringTemplate.Language;
 
StringTemplate hello = new StringTemplate("Hello, $name$", typeof(DefaultTemplateLexer));
hello.SetAttribute("name", "World");
Console.Out.WriteLine(hello.ToString());

【讨论】:

【解决方案3】:

我编写了一个非常简单的库SmartFormat,它可以满足您的所有要求。它专注于编写“自然语言”文本,非常适合从列表生成数据或应用条件逻辑。

语法与String.Format极为相似,非常简单易学易用。这是文档中的语法示例:

Smart.Format("{Name}'s friends: {Friends:{Name}|, |, and}", user)
// Result: "Scott's friends: Michael, Jim, Pam, and Dwight"

该库具有出色的错误处理选项(忽略错误、输出错误、抛出错误)。显然,这非常适合您的示例。

该库是开源的且易于扩展,因此您也可以通过其他功能对其进行增强。

【讨论】:

  • 感谢这个方便的库。这是我在审查了许多替代方案后选择的。还有一个Nuget package
  • 如果你不需要像局部这样的高级东西,这个非常棒。尤其是可以将string.FormatSmart.Format 交换的功能,这样您就可以逐步发展您的模板。谢谢!
【解决方案4】:

在 Benjamin Gruenbaum 的回答的基础上,在 C# 版本 6 中,您可以添加带有 $ 的 @ 并且几乎可以按原样使用您的代码,例如:

var text = $@"Dear {User},

Your job finished at {FinishTime} and your file is available for download at {FileURL}.

Regards,

-- 
{Signature}
";

$ 用于字符串插值:https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/tokens/interpolated

@ 是逐字标识符:https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/tokens/verbatim

...您可以将它们结合使用。

:o)

【讨论】:

  • 字符串插值仅在编译期间有效,因此您无法从配置文件或数据库等外部源获取字符串模板。
【解决方案5】:

如果您在 VB.NET 中编码,则可以使用 XML 文字。如果您使用 C# 进行编码,则可以使用 ShartDevelop 将 VB.NET 中的文件与 C# 代码放在同一个项目中。

【讨论】:

    【解决方案6】:

    可以使用“string.Format”方法:

    var user = GetUser();
    var finishTime = GetFinishTime();
    var fileUrl = GetFileUrl();
    var signature = GetSignature();
    string msg =
    @"Dear {0},
    
    Your job finished at {1} and your file is available for download at {2}.
    
    Regards,
    
    --
    {3}";
    msg = string.Format(msg, user, finishTime, fileUrl, signature);
    

    它允许您将来更改内容,并且对本地化很友好。

    【讨论】:

    • 只允许输入编号,如果用户输入的数字太高,则会引发异常。
    • 是的,只有编号的输入,好的。但是“如果用户输入的数字太高,则抛出异常”是不正确的。我用“1024 * 1024”输入参数试了一下,效果很好。
    • 不,我的意思是如果您将 {3} 替换为 {4} 但不提供其他输入参数。
    • 是的,但是如果模板包含未被替换的符号,任何解决方案都应该产生异常。找出问题所在的事实比把头埋在沙子里要好得多!
    • +1 这实际上是最好的答案,因为它不需要外部库,而且它不仅在编译时有用。
    【解决方案7】:

    如果您需要一些非常强大的东西(但实际上不是最简单的方法),您可以托管 ASP.NET 并将其用作模板引擎。

    您将拥有 ASP.NET 的所有功能来格式化您的消息正文。

    【讨论】:

      【解决方案8】:

      一个非常简单的基于正则表达式的解决方案。支持\n-style 单字符转义序列和{Name}-style 命名变量。

      来源

      class Template
      {
          /// <summary>Map of replacements for characters prefixed with a backward slash</summary>
          private static readonly Dictionary<char, string> EscapeChars
              = new Dictionary<char, string>
              {
                  ['r'] = "\r",
                  ['n'] = "\n",
                  ['\\'] = "\\",
                  ['{'] = "{",
              };
      
          /// <summary>Pre-compiled regular expression used during the rendering process</summary>
          private static readonly Regex RenderExpr = new Regex(@"\\.|{([a-z0-9_.\-]+)}",
              RegexOptions.IgnoreCase | RegexOptions.Compiled);
      
          /// <summary>Template string associated with the instance</summary>
          public string TemplateString { get; }
      
          /// <summary>Create a new instance with the specified template string</summary>
          /// <param name="TemplateString">Template string associated with the instance</param>
          public Template(string TemplateString)
          {
              if (TemplateString == null) {
                  throw new ArgumentNullException(nameof(TemplateString));
              }
      
              this.TemplateString = TemplateString;
          }
      
          /// <summary>Render the template using the supplied variable values</summary>
          /// <param name="Variables">Variables that can be substituted in the template string</param>
          /// <returns>The rendered template string</returns>
          public string Render(Dictionary<string, object> Variables)
          {
              return Render(this.TemplateString, Variables);
          }
      
          /// <summary>Render the supplied template string using the supplied variable values</summary>
          /// <param name="TemplateString">The template string to render</param>
          /// <param name="Variables">Variables that can be substituted in the template string</param>
          /// <returns>The rendered template string</returns>
          public static string Render(string TemplateString, Dictionary<string, object> Variables)
          {
              if (TemplateString == null) {
                  throw new ArgumentNullException(nameof(TemplateString));
              }
      
              return RenderExpr.Replace(TemplateString, Match => {
                  switch (Match.Value[0]) {
                      case '\\':
                          if (EscapeChars.ContainsKey(Match.Value[1])) {
                              return EscapeChars[Match.Value[1]];
                          }
                          break;
      
                      case '{':
                          if (Variables.ContainsKey(Match.Groups[1].Value)) {
                              return Variables[Match.Groups[1].Value].ToString();
                          }
                          break;
                  }
      
                  return string.Empty;
              });
          }
      }
      

      用法

      var tplStr1 = @"Hello {Name},\nNice to meet you!";
      var tplStr2 = @"This {Type} \{contains} \\ some things \\n that shouldn't be rendered";
      var variableValues = new Dictionary<string, object>
      {
          ["Name"] = "Bob",
          ["Type"] = "string",
      };
      
      Console.Write(Template.Render(tplStr1, variableValues));
      // Hello Bob,
      // Nice to meet you!
      
      var template = new Template(tplStr2);
      Console.Write(template.Render(variableValues));
      // This string {contains} \ some things \n that shouldn't be rendered
      

      注意事项

      • 我只定义了\n\r\\\{ 转义序列并对其进行了硬编码。您可以轻松地添加更多内容或使它们可由消费者定义。
      • 我已将变量名设置为不区分大小写,因为这样的事情通常会呈现给最终用户/非程序员,我个人认为在该用例中区分大小写没有意义 - 它只是一个更多的事情他们可能会出错并打电话给你抱怨(一般来说,如果你认为你需要区分大小写的符号名称,你真正需要的是更好的符号名称)。要使它们区分大小写,只需删除 RegexOptions.IgnoreCase 标志即可。
      • 我从结果字符串中去除了无效的变量名和转义序列。要使它们保持不变,请在 Regex.Replace 回调的末尾返回 Match.Value 而不是空字符串。您也可以抛出异常。
      • 我使用了{var} 语法,但这可能会干扰本机内插字符串语法。如果您想在代码中以字符串文字定义模板,建议将变量分隔符更改为例如%var% (regex \\.|%([a-z0-9_.\-]+)%) 或您选择的其他更适合用例的语法。

      【讨论】:

      • 默认情况下我更喜欢区分大小写的模板。除了删除RegexOptions.IgnoreCase,我还必须将A-Z 添加到RenderExpr
      【解决方案9】:

      这里是那些可以使用新版本 C# 的人的版本:

      // add $ at start to mark string as template
      var template = $"Your job finished at {FinishTime} and your file is available for download at {FileURL}."
      

      在一行中 - 这是现在完全支持的语言功能(字符串插值)。

      【讨论】:

      • 请注意,这是一个编译时替换,本质上是 String.Format() 周围的糖,如果您在运行时获取模板字符串,则无济于事。不过,+1 是在编译时执行此操作的最佳方式。
      • 您可以将代码提取到一个函数中,然后将其中的项目作为参数传递 - 这与其他模板技术一样具有表现力。
      • @DaveRandom:docs 否则说:The interpolated string is evaluated each time the code with the interpolated string executes. This allows you to separate the definition and evaluation of an interpolated string. 还是我读错了?
      • @Martijn 抱歉,措辞不好,我的意思是模板本身是在编译时定义的,例如不可能让用户更改模板。编译器有效地生成string.Format() 替换,虽然可以让用户重新定义它们,但它们并不是特别用户友好,因为它们不能再在模板中使用 named 值。当我提出这个问题时,我所追求的类似于我最终为我的答案写的解决方案。
      • 如果您从外部模板文件加载模板,这将不起作用
      【解决方案10】:

      实现您自己的自定义格式化程序可能是个好主意。

      这就是你的做法。首先,创建一个类型来定义要注入到消息中的内容。注意:我只会用模板的用户部分来说明这一点......

      class JobDetails
      {
          public string User 
          { 
              get;
              set; 
          }        
      }
      

      接下来,实现一个简单的自定义格式化程序...

      class ExampleFormatter : IFormatProvider, ICustomFormatter
      {
          public object GetFormat(Type formatType)
          {
              return this;
          }
      
          public string Format(string format, object arg, IFormatProvider formatProvider)
          {
              // make this more robust
              JobDetails job = (JobDetails)arg;
      
              switch (format)
              {
                  case "User":
                  {
                      return job.User;
                  }
                  default:
                  {
                      // this should be replaced with logic to cover the other formats you need
                      return String.Empty;
                  }
              }
          }
      }
      

      最后,像这样使用它......

      string template = "Dear {0:User}. Your job finished...";
      
      JobDetails job = new JobDetails()
                           {
                                   User = "Martin Peck"
                           };
      
      string message = string.Format(new ExampleFormatter(), template, job);
      

      ... 这将生成文本“亲爱的 Martin Peck。你的工作完成了...”。

      【讨论】:

        【解决方案11】:

        实际上,您可以使用 XSLT。 创建一个简单的 XML 模板:

        <?xml version="1.0" encoding="utf-8"?>
        <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
          <xsl:template match="TETT">
            <p>
               Dear <xsl:variable name="USERNAME" select="XML_PATH" />,
        
               Your job finished at <xsl:variable name="FINISH_TIME" select="XML_PATH" /> and your file is available for download at <xsl:variable name="FILE_URL" select="XML_PATH" />.
        
               Regards,
                -- 
               <xsl:variable name="SIGNATURE" select="XML_PATH" />
            </p>
        </xsl:template>
        

        然后创建一个 XmlDocument 来执行转换: XmlDocument xmlDoc = new XmlDocument();

                XmlNode xmlNode = xmlDoc .CreateNode(XmlNodeType.Element, "EMAIL", null);
                XmlElement xmlElement= xmlDoc.CreateElement("USERNAME");
                xmlElement.InnerXml = username;
                xmlNode .AppendChild(xmlElement); ///repeat the same thing for all the required fields
        
                xmlDoc.AppendChild(xmlNode);
        

        之后,应用转换:

                XPathNavigator xPathNavigator = xmlDocument.DocumentElement.CreateNavigator();
                StringBuilder sb = new StringBuilder();
                StringWriter sw = new StringWriter(sb);
                XmlTextWriter xmlWriter = new XmlTextWriter(sw);
                your_xslt_transformation.Transform(xPathNavigator, null, xmlWriter);
                return sb.ToString();
        

        【讨论】:

        • 虽然我很喜欢 XSLT,但这可能无法满足 OP 中的最后一条语句:“对我来说,最简单的方法是什么?” :)
        • 我在回答“在 .NET 中做字符串模板的好方法是什么?”)不是“最简单的”)
        • 抵押驱动软件开发研究所的程序员同事youtube.com/watch?v=7RJmoCWx4cE
        【解决方案12】:

        您可以使用 string.Replace(...),最终在 for-each 中遍历所有关键字。如果只有几个关键字,您可以将它们放在一行中:

        string myString = template.Replace("FirstName", "John").Replace("LastName", "Smith").Replace("FinishTime", DateTime.Now.ToShortDateString());
        

        如果您需要更强大的功能和更多选项,您也可以使用 Regex.Replace(...)。

        阅读此article on codeproject 以查看哪种字符串替换选项最适合您。

        【讨论】:

        • 这不是一个好方法,如果你有很多参数要替换和/或有长字符串 - 因为字符串是不可变的,所以内存使用量很大。
        • +1 @TcKs - 同意,该示例有 4 个参数,所以我会接受 @Ovi 的建议,但如果它达到 10 左右,那么我会寻找一种模板语言..
        • 这可能会占用大量内存,但模板引擎或语言也很可能会过大。
        • 如果你明白我的意思,垃圾收集器会告诉你的一些亲戚......
        • 以上所有内容都是正确的,但与许多应用程序无关(模板很小,毫秒无关紧要)。如果解决方案不需要是 generic 并且可以禁止任何模板的 literal 文本看起来像符号(例如通过禁止 '{' 和 ' }' 在文字中),这实际上可能就足够了。另一方面,使用与简单替换相同的功能很容易提高性能方面。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2010-09-13
        • 1970-01-01
        • 2014-04-12
        • 1970-01-01
        • 1970-01-01
        • 2012-05-24
        • 1970-01-01
        相关资源
        最近更新 更多