最近框架中的可视化界面设计需要使用到表达式引擎(解析代码字符串并动态执行),之前旧框架的实现是将表达式字符串解析为语法树后解释执行该表达式,本文介绍如何使用Roslyn解析表达式字符串,并直接转换为Linq的表达式后编译执行。

一、语法(Syntax)与语义(Semantic)

  C#的代码通过Roslyn解析为相应的语法树,并且利用语义分析可以获取语法节点所对应的符号及类型信息,这样利用这些信息可以正确的转换为Linq的表达式。这里作者就不展开了,可以参考Roslyn文档。

二、实现表达式解析器(ExpressionParser)

1. 解析字符串方法

  下面开始创建一个类库工程,引用包Microsoft.CodeAnalysis.CSharp.Features,然后参照以下代码创建ExpressionParser类, 静态ParseCode()方法是解析字符串表达式的入口:

using System.Linq.Expressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax; namespace ExpEngine; public sealed class ExpressionParser : CSharpSyntaxVisitor<Expression>
{
private ExpressionParser(SemanticModel semanticModel)
{
_semanticModel = semanticModel;
} private readonly SemanticModel _semanticModel; /// <summary>
/// 解析表达式字符串转换为Linq的表达式
/// </summary>
public static Expression ParseCode(string code)
{
var parseOptions = new CSharpParseOptions().WithLanguageVersion(LanguageVersion.CSharp11);
var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
.WithNullableContextOptions(NullableContextOptions.Enable); var tree = CSharpSyntaxTree.ParseText(code, parseOptions);
var root = tree.GetCompilationUnitRoot();
var compilation = CSharpCompilation.Create("Expression", options: compilationOptions)
.AddReferences(MetadataReference.CreateFromFile(typeof(string).Assembly.Location))
.AddSyntaxTrees(tree);
var semanticModel = compilation.GetSemanticModel(tree);
//检查是否存在语义错误
var diagnostics = semanticModel.GetDiagnostics();
var errors = diagnostics.Count(d => d.Severity == DiagnosticSeverity.Error);
if (errors > 0)
throw new Exception("表达式存在语义错误"); var methodDecl = root.DescendantNodes().OfType<MethodDeclarationSyntax>().First();
if (methodDecl.Body != null && methodDecl.Body.Statements.Count > 1)
throw new NotImplementedException("Parse block body"); if (methodDecl.ExpressionBody != null)
throw new NotImplementedException("Parse expression body"); var firstStatement = methodDecl.Body!.Statements.FirstOrDefault();
if (firstStatement is not ReturnStatementSyntax returnNode)
throw new Exception("表达式方法不是单行返回语句"); var parser = new ExpressionParser(semanticModel);
return parser.Visit(returnNode.Expression)!;
}
}

2. 解析运行时类型的方法

  因为转换过程中需要将Roslyn解析出来的类型信息转换为对应的C#运行时的类型,所以需要实现类型转换的方法:

    private readonly Dictionary<string, Type> _knownTypes = new()
{
{ "bool", typeof(bool) },
{ "byte", typeof(byte) },
{ "sbyte", typeof(sbyte) },
{ "short", typeof(short) },
{ "ushort", typeof(ushort) },
{ "int", typeof(int) },
{ "uint", typeof(uint) },
{ "long", typeof(long) },
{ "ulong", typeof(ulong) },
{ "float", typeof(float) },
{ "double", typeof(double) },
{ "char", typeof(char) },
{ "string", typeof(string) },
{ "object", typeof(object) },
}; /// <summary>
/// 根据类型字符串获取运行时类型
/// </summary>
private Type ResolveType(string typeName)
{
if (_knownTypes.TryGetValue(typeName, out var sysType))
return sysType; //通过反射获取类型
var type = Type.GetType(typeName);
if (type == null)
throw new Exception($"Can't find type: {typeName} "); return type;
}

3. 解析各类语法节点转换为对应的Linq表达式

  这里举一个简单的LiteralExpression转换的例子,其他请参考源码。需要注意的是Linq的表达式严格匹配类型签名,比如方法调用object.Equals(object a, object b), 如果参数a是int类型,需要使用Expression.Convert(int, typeof(object))转换为相应的类型。

    private Type? GetConvertedType(SyntaxNode node)
{
var typeInfo = _semanticModel.GetTypeInfo(node);
Type? convertedType = null;
if (!SymbolEqualityComparer.Default.Equals(typeInfo.Type, typeInfo.ConvertedType))
convertedType = ResolveType((INamedTypeSymbol)typeInfo.ConvertedType!); return convertedType;
} public override Expression? VisitLiteralExpression(LiteralExpressionSyntax node)
{
var convertedType = GetConvertedType(node);
var res = Expression.Constant(node.Token.Value);
return convertedType == null ? res : Expression.Convert(res, convertedType);
}

三、测试解析与执行表达式

  现在可以创建一个单元测试项目验证一下解析字符串表达式并执行了,当然实际应用过程中应缓存解析并编译的表达式委托:

namespace UnitTests;

using static ExpEngine.ExpressionParser;

public class Tests
{
[Test]
public void StaticPropertyTest() => Assert.True(Run<object>("DateTime.Today") is DateTime); [Test]
public void InstancePropertyTest() => Run<int>("DateTime.Today.Year"); [Test]
public void MethodCallTest1() => Run<DateTime>("DateTime.Today.AddDays(1 + 1)"); [Test]
public void MethodCallTest2() => Run<DateTime>("DateTime.Today.AddDays(DateTime.Today.Year)"); [Test]
public void MethodCallTest3() => Run<DateTime>("DateTime.Today.AddDays(int.Parse(\"1\"))"); [Test]
public void MethodCallTest4() => Assert.True(Run<bool>("Equals(new DateTime(1977,3,1), new DateTime(1977,3,1))")); [Test]
public void PrefixUnaryTest() => Run<DateTime>("DateTime.Today.AddDays(-1)"); [Test]
public void NewTest() => Assert.True(Run<DateTime>("new DateTime(1977,3,16)") == new DateTime(1977, 3, 16)); [Test]
public void BinaryTest1() => Assert.True(Run<float>("3 + 2.6f") == 3 + 2.6f); [Test]
public void BinaryTest2() => Assert.True(Run<bool>("3 >= 2.6f"));
}

四、 一些限制与TODO

  Linq的表达式本身存在一些限制,请参考文档:

https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/expression-trees/

  另上述代码仅示例,比如表达式输入参数等未实现,小伙伴们可以继续自行完善。

用Roslyn玩转代码之一: 解析与执行字符串表达式的更多相关文章

  1. 在C#开发中使用第三方组件LambdaParser、DynamicExpresso、Z.Expressions,实现动态解析/求值字符串表达式

    在进行项目开发的时候,刚好需要用到对字符串表达式进行求值的处理场景,因此寻找了几个符合要求的第三方组件LambdaParser.DynamicExpresso.Z.Expressions,它们各自功能 ...

  2. 分享非常有用的Java程序 (关键代码)(六)---解析/读取XML 文件(重要)

    原文:分享非常有用的Java程序 (关键代码)(六)---解析/读取XML 文件(重要) XML文件 <?xml version="1.0"?> <student ...

  3. Java打印整数的二进制表示(代码与解析)

    Java打印整数的二进制表示(代码与解析) int a=-99; for(int i=0;i<32;i++){ int t=(a & 0x80000000>>>i)&g ...

  4. 《编译原理》画 DAG 图与求优化后的 4 元式代码- 例题解析

    <编译原理>画 DAG 图与求优化后的 4 元式代码- 例题解析 DAG 图(Directed Acylic Graph)无环路有向图 (一)基本块 基本块是指程序中一顺序执行的语句序列, ...

  5. webpack优化之玩转代码分割和公共代码提取

    前言 开发多页应用的时候,如果不对webpack打包进行优化,当某个模块被多个入口模块引用时,它就会被打包多次(在最终打包出来的某几个文件里,它们都会有一份相同的代码).当项目业务越来越复杂,打包出来 ...

  6. 人脸跟踪开源项目HyperFT代码算法解析及改进

    一.简介 人脸识别已经成为计算机视觉领域中最热门的应用之一,其中,人脸信息处理的第一个环节便是人脸检测和人脸跟踪.人脸检测是指在输入的图像中确定所有人脸的位置.大小和姿势的过程.人脸跟踪是指在图像序列 ...

  7. Python使用pyexecjs代码案例解析

    针对现在大部分的网站都是使用js加密,js加载的,并不能直接抓取出来,这时候就不得不适用一些三方类库来执行js语句 execjs,一个比较好用且容易上手的类库(支持py2,与py3),支持 JS ru ...

  8. 【技巧总结】Penetration Test Engineer[3]-Web-Security(SQL注入、XXS、代码注入、命令执行、变量覆盖、XSS)

    3.Web安全基础 3.1.HTTP协议 1)TCP/IP协议-HTTP 应用层:HTTP.FTP.TELNET.DNS.POP3 传输层:TCP.UDP 网络层:IP.ICMP.ARP 2)常用方法 ...

  9. JS的解析与执行过程

    JS的解析与执行过程 全局中的解析和执行过程 预处理:创建一个词法环境(LexicalEnvironment,在后面简写为LE),扫描JS中的用声明的方式声明的函数,用var定义的变量并将它们加到预处 ...

  10. 如何执行字符串的PHP代码

    如何执行字符串的PHP代码 最近因项目需要,引出一个议题:如何执行字符串的php代码(php和html混写). 注:传统情况下,php代码存储在文件中,直接运行文件即可.以下讨论的情况是,如果php代 ...

随机推荐

  1. 如何在没有第三方.NET库源码的情况,调试第三库代码?

    大家好,我是沙漠尽头的狼. 本方首发于Dotnet9,介绍使用dnSpy调试第三方.NET库源码,行文目录: 安装dnSpy 编写示例程序 调试示例程序 调试.NET库原生方法 总结 1. 安装dnS ...

  2. salesforce零基础学习(一百三十二)Flow新功能: Custom Error

    本篇参考: https://help.salesforce.com/s/articleView?id=sf.flow_ref_elements_custom_error.htm&type=5 ...

  3. ERP 财务管理的应付帐款流程

    导读:应付帐款流程与应收帐款流程是财务管理的开端,也是财务工作的主要流程.若能够这两大流程控制好了,ERP系统的财务模块也就成功了一大半了.我先讲一下财务管理的应付帐款流程. 企业的应付帐款有很多种类 ...

  4. Android Studio3.2.1升级刨坑记录

    Android Studio出了3.2.1,我用的是2.3,所有决定升级一下,看看如何 为了保险一点,下载了官方的解压版本,也就是说不含sdk,下载android-studio-ide-181.501 ...

  5. Subtree 题解

    Subtree 题目大意 给定一颗树,你可以选出一些节点,你需要对于每个点求出在强制选这个点的情况下所有选择的点联通的方案数,对给定模数取模. 思路分析 对于这种求树上每一个点方案数的题目,首先考虑换 ...

  6. Linux账号密码安全运维

    前言 随着云计算厂商的兴起,云资源如ECS不再只有企业或者公司才会使用,普通人也可以自己买一台ECS来搭建自己的应用或者网站.虽然云计算厂商帮我们做了很多安全相关的工作,但并不代表我们的机器资源就绝对 ...

  7. 把 map 中的 key 由驼峰命名转为下划线

    import cn.hutool.core.util.StrUtil; /** * 把 map 中的 key 由驼峰命名转为下划线 */public HashMap<String, Object ...

  8. [vue]精宏技术部试用期学习笔记 II

    精宏技术部试用期学习笔记(vue) router : vue的模拟路由 前置准备 安装 vue-router pnpm i vue-router@4 //安装版本4的 vue-router 可以在 p ...

  9. Error running 'TestAlterNickname.test': Command line is too long. Shorten command line for TestAlterNickname.test or also for JUnit default configuration

    问题描述 如图IDEA报错问题,发生在我用JUnit进行测试时. 解决方法 1. 直接点击 default 2. Modify options -> Shorten command line 3 ...

  10. RLHF · PBRL | SURF:使用半监督学习,对 labeled segment pair 进行数据增强

    论文名称:SURF: Semi-supervised reward learning with data augmentation for feedback-efficient preference- ...