如何在C#(.NET 5)中使用动态绑定表达式格式化字符串?
在.NET 5中实现类似AngularJS的字符串模板绑定(支持属性与方法调用)
需求概述
需要实现类似AngularJS的字符串模板功能:定义包含占位符的格式字符串,通过访问动态模型的属性或调用其方法,生成最终的格式化字符串。
模型示例:
{ "firstName": "John", "lastName": "Doe", "random": () => Random.Next(), "formatDate": (fmt, value) => new DateTime(value).ToString(fmt) }
目标格式字符串示例(AngularJS风格):
Hello {firstName} {lastName}. Today is {formatDate('d', new Date())}.
已尝试方案及问题
- WPF/XAML:引入大量不必要的UI相关开销,仅需纯字符串格式化功能,不符合需求
- Roslyn:要求格式表达式必须是合法C#代码,实现复杂度高、工作量大
- Formattable字符串/ICustomFormatter:不支持动态模型的属性访问与方法调用
调用代码示例
var formatString = person.Tenant.WelcomeMessageFormat; var model = new { FirstName = person.FirstName, LastName = person.LastName, Random = () => Random.Next(), FormatDate = (fmt, value) => new DateTime(value).ToString(fmt) }; var message = ParseFormatString(formatString, model); SendMessage(message); // 需要实现的核心方法 public string ParseFormatString(string format, object model) { // 待实现逻辑 }
解决方案:轻量级自定义解析器
下面是一个无额外依赖的实现,通过正则匹配占位符,结合反射解析属性和方法调用,满足基础需求:
using System; using System.Text.RegularExpressions; using System.Reflection; public string ParseFormatString(string format, object model) { // 匹配所有{...}格式的占位符 var placeholderRegex = new Regex(@"\{([^{}]+)\}", RegexOptions.Compiled); return placeholderRegex.Replace(format, match => { var expression = match.Groups[1].Value.Trim(); try { return EvaluateExpression(expression, model)?.ToString() ?? string.Empty; } catch { // 解析失败时返回原占位符,避免破坏整体格式 return match.Value; } }); } private object EvaluateExpression(string expression, object model) { // 判断是否为方法调用 if (expression.Contains("(")) { var methodName = expression.Substring(0, expression.IndexOf('(')).Trim(); var paramSection = expression.Substring(expression.IndexOf('(') + 1, expression.LastIndexOf(')') - expression.IndexOf('(') - 1).Trim(); var parameters = ParseMethodParameters(paramSection, model); // 查找匹配的方法 var method = model.GetType().GetMethod(methodName, parameters.Select(p => p.GetType()).ToArray()); if (method == null) throw new InvalidOperationException($"未找到方法:{methodName}"); return method.Invoke(model, parameters); } // 属性访问 else { var property = model.GetType().GetProperty(expression); if (property == null) throw new InvalidOperationException($"未找到属性:{expression}"); return property.GetValue(model); } } private object[] ParseMethodParameters(string paramSection, object model) { if (string.IsNullOrWhiteSpace(paramSection)) return Array.Empty<object>(); var paramStrings = paramSection.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); var parameters = new object[paramStrings.Length]; for (int i = 0; i < paramStrings.Length; i++) { var param = paramStrings[i].Trim(); // 处理单引号包裹的字符串字面量 if (param.StartsWith("'") && param.EndsWith("'")) { parameters[i] = param.Substring(1, param.Length - 2); } // 处理new Date()的特殊情况(替换为当前时间) else if (param.Equals("new Date()", StringComparison.OrdinalIgnoreCase)) { parameters[i] = DateTime.Now; } // 尝试解析为模型属性 else { var property = model.GetType().GetProperty(param); if (property != null) { parameters[i] = property.GetValue(model); } // 尝试转换为基础类型 else if (int.TryParse(param, out int intVal)) { parameters[i] = intVal; } else if (DateTime.TryParse(param, out DateTime dateVal)) { parameters[i] = dateVal; } // 无法解析时保留原字符串 else { parameters[i] = param; } } } return parameters; }
说明
这个实现是轻量级的,仅依赖.NET基础库,支持:
- 访问模型的公共属性
- 调用模型的公共方法(支持字面量参数、模型属性参数、基础类型参数)
- 处理
new Date()这类特殊表达式(可根据需求扩展)
如果需要支持更复杂的表达式(比如嵌套属性、运算符、Lambda等),可以在此基础上扩展解析逻辑。
内容的提问来源于stack exchange,提问作者just.another.programmer
相关产品推荐
相关产品推荐

