执行在字符串中定义的C#代码
有时候我们需要执行在字符串中动态定义的C#代码。
在.NET Framework时代,我们可以使用CodeDomProvider.CompileAssemblyFromSource方法(需要在项目中安装NuGet包System.CodeDom),来执行一个字符串中的C#代码。但是该方法在.NET Core中已经不支持了,在.NET Core中调用该方法会抛出PlatformNotSupportedException。
关于该方法的使用,可以参考:C#中动态执行代码,执行字符串中的代码
在.NET Core中,如果我们要执行一个字符串中的C#代码,可以使用CSharpScript类(需要在项目中安装NuGet包Microsoft.CodeAnalysis.CSharp.Scripting)。
下面是一个例子,演示了如何使用CSharpScript类的EvaluateAsync方法,来执行一个简单的加法运算,并获得运算结果的值:
using Microsoft.CodeAnalysis.CSharp.Scripting; namespace Net8CSharpScriptDemo { public class Program { public static async Task Main(string[] args) { var formula = "1+2"; var result = await CSharpScript.EvaluateAsync<int>(formula); Console.WriteLine(result); Console.WriteLine("Press any key to end..."); Console.ReadLine(); } } }
我们还可以在执行CSharpScript.EvaluateAsync方法时,通过globals来传入参数给字符串中的表达式:
using Microsoft.CodeAnalysis.CSharp.Scripting; namespace Net8CSharpScriptDemo { public class Program { //Globals类和其中的属性,必须是public的,不然EvaluateAsync方法会报错 public class Globals { public int x { get; set; } public int y { get; set; } } public static async Task Main(string[] args) { var formula = "x+y"; Globals globals = new Globals() { x = 1, y = 2 }; var result = await CSharpScript.EvaluateAsync<int>(formula, globals: globals); Console.WriteLine(result); Console.WriteLine("Press any key to end..."); Console.ReadLine(); } } }
此外,我们还可以通过ScriptOptions类来添加引用的程序集和命名空间:
using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.Scripting; namespace Net8CSharpScriptDemoNamespace { //Program类必须是public的,不然EvaluateAsync方法会报错 public class Program { //AddOne方法必须是public的,不然EvaluateAsync方法会报错 public static int AddOne(int number) { number++; return number; } //Globals类和其中的属性,必须是public的,不然EvaluateAsync方法会报错 public class Globals { public int x { get; set; } public int y { get; set; } public int z { get; set; } } public static async Task Main(string[] args) { var formula = "Math.Abs(x) + Math.Abs(y) + Program.AddOne(z)"; var options = ScriptOptions.Default .WithReferences(typeof(Program).Assembly)//添加引用的程序集 .WithImports("System", "Net8CSharpScriptDemoNamespace");//添加命名空间,相当于.cs文件最上面的using语句引入命名空间 Globals globals = new Globals() { x = -1, y = -2, z = 3 }; var result = await CSharpScript.EvaluateAsync<int>( formula, globals: globals, options: options); Console.WriteLine(result); Console.WriteLine("Press any key to end..."); Console.ReadLine(); } } }
在下面这篇文章中有提到,在循环中执行CSharpScript.EvaluateAsync方法时,会出现内存一直增长的问题:
Roslyn(CSharpScript).Net脚本编译引擎使用过程内存增涨与稳定的方式
其中的解决办法是只编译和加载一次相同代码的程序集模块。
还有一个办法是使用Script<T>.CreateDelegate方法来创建一个ScriptRunner<T>委托,然后使用ScriptRunner委托的实例来运行字符串中的C#代码:
using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.Scripting; namespace Net8CSharpScriptDemoNamespace { //Program类必须是public的,不然EvaluateAsync方法会报错 public class Program { //AddOne方法必须是public的,不然EvaluateAsync方法会报错 public static int AddOne(int number) { number++; return number; } //Globals类和其中的属性,必须是public的,不然EvaluateAsync方法会报错 public class Globals { public int x { get; set; } public int y { get; set; } public int z { get; set; } } public static async Task Main(string[] args) { var formula = "Math.Abs(x) + Math.Abs(y) + Program.AddOne(z)"; var options = ScriptOptions.Default .WithReferences(typeof(Program).Assembly)//添加引用的程序集 .WithImports("System", "Net8CSharpScriptDemoNamespace");//添加命名空间,相当于.cs文件最上面的using语句引入命名空间 Globals globals = new Globals() { x = -1, y = -2, z = 3 }; for (int i = 0; i < 100; i++) { Script<int> script = CSharpScript.Create<int>(formula, options: options, globalsType: typeof(Globals));//调用CSharpScript.Create方法时,需要通过globalsType参数来预先告诉C#编译器,后面传入ScriptRunner委托的globals参数的Type是什么,不然下面的script.CreateDelegate方法会抛出异常 ScriptRunner<int> scriptDelegate = script.CreateDelegate();//使用script.CreateDelegate方法来创建ScriptRunner委托的实例,ScriptRunner受到.NET GC的管控,所以使用的内存会被.NET的垃圾回收机制清理 int result = await scriptDelegate(globals: globals);//这里传入ScriptRunner委托实例的globals参数的Type,必须和上面CSharpScript.Create方法的参数globalsType一致 Console.WriteLine(i.ToString() + " >> " + result.ToString()); GC.Collect();//每次循环最后,可以调用GC.Collect方法,来强制回收ScriptRunner消耗的内存 } Console.WriteLine("Press any key to end..."); Console.ReadLine(); } } }
因为ScriptRunner委托受到.NET GC的管控,所以使用的内存会被.NET的垃圾回收机制清理。其中下面的文章有提到这一点:
CSharpScript seemingly excessive memory usage
我们还可以在字符串中定义多行C#代码,以及给传入CSharpScript.EvaluateAsync方法和ScriptRunner委托实例的globals参数中定义引用类型,这样执行了字符串中的C#代码后,globals参数中的引用类型也会发生变化:
using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.Scripting; namespace Net8CSharpScriptDemoNamespace { //Program类必须是public的,不然EvaluateAsync方法会报错 public class Program { //AddOne方法必须是public的,不然EvaluateAsync方法会报错 public static int AddOne(int number) { number++; return number; } //NumberRepository类和其中的属性,必须是public的,不然EvaluateAsync方法会报错 public class NumberRepository { public int Number { get; set; } } //Globals类和其中的属性,必须是public的,不然EvaluateAsync方法会报错 public class Globals { public int x { get; set; } public int y { get; set; } public int z { get; set; } public NumberRepository? numberRepository { get; set; } } public static async Task Main(string[] args) { var formula = @"int number=Math.Abs(x) + Math.Abs(y) + Program.AddOne(z); number=number+1000; numberRepository.Number++; return number; "; var options = ScriptOptions.Default .WithReferences(typeof(Program).Assembly)//添加引用的程序集 .WithImports("System", "Net8CSharpScriptDemoNamespace");//添加命名空间,相当于.cs文件最上面的using语句引入命名空间 Globals globals = new Globals() { x = -1, y = -2, z = 3, numberRepository = new NumberRepository() { Number = 1 } }; var result1 = await CSharpScript.EvaluateAsync<int>( formula, globals: globals, options: options); Console.WriteLine("result1 is " + result1.ToString()); Console.WriteLine("globals.numberRepository.Number is " + globals.numberRepository?.Number.ToString());//由于传入CSharpScript.EvaluateAsync方法参数globals的numberRepository属性是引用类型,所以执行字符串中的C#代码后,globals.numberRepository.Number会加1 Script<int> script = CSharpScript.Create<int>(formula, options: options, globalsType: typeof(Globals));//调用CSharpScript.Create方法时,需要通过globalsType参数来预先告诉C#编译器,后面传入ScriptRunner委托的globals参数的Type是什么,不然下面的script.CreateDelegate方法会抛出异常 ScriptRunner<int> scriptDelegate = script.CreateDelegate();//使用script.CreateDelegate方法来创建ScriptRunner委托的实例,ScriptRunner受到.NET GC的管控,所以使用的内存会被.NET的垃圾回收机制清理 int result2 = await scriptDelegate(globals: globals);//这里传入ScriptRunner委托实例的globals参数的Type,必须和上面CSharpScript.Create方法的参数globalsType一致 Console.WriteLine("result2 is " + result2.ToString()); Console.WriteLine("globals.numberRepository.Number is " + globals.numberRepository?.Number.ToString());//由于传入ScriptRunner委托实例参数globals的numberRepository属性是引用类型,所以执行字符串中的C#代码后,globals.numberRepository.Number会加1 Console.WriteLine("Press any key to end..."); Console.ReadLine(); } } }
执行上面的代码后,结果如下所示:
result1 is 1007 globals.numberRepository.Number is 2 result2 is 1007 globals.numberRepository.Number is 3 Press any key to end...
我们可以通过捕获CompilationErrorException异常,来知道字符串中的C#代码中有没有编译错误,还可以通过捕获Exception异常,来知道执行字符串中的C#代码时抛出了什么异常:
using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.Scripting; namespace Net8CSharpScriptDemoNamespace { public class Program { public static async Task Main(string[] args) { var invalidCode = "int x = 1 / 0;"; try { await CSharpScript.EvaluateAsync(invalidCode); } catch (CompilationErrorException ex)//捕获字符串中C#代码的编译错误 { Console.WriteLine("Compilation Error 1 is: " + string.Join("\n", ex.Diagnostics));//可以通过ex.Diagnostics获取编译错误的信息 } try { Script<object> script = CSharpScript.Create(invalidCode); ScriptRunner<object> scriptDelegate = script.CreateDelegate(); await scriptDelegate(); } catch (CompilationErrorException ex)//捕获字符串中C#代码的编译错误 { Console.WriteLine("Compilation Error 2 is: " + string.Join("\n", ex.Diagnostics));//可以通过ex.Diagnostics获取编译错误的信息 } var exceptionCode = "throw new System.Exception(\"This is a demo exception.\");"; try { await CSharpScript.EvaluateAsync(exceptionCode); } catch (Exception ex)//捕获执行字符串中C#代码的异常错误 { Console.WriteLine("Exception 1 is:"); Console.WriteLine(ex.ToString()); } try { Script<object> script = CSharpScript.Create(exceptionCode); ScriptRunner<object> scriptDelegate = script.CreateDelegate(); await scriptDelegate(); } catch (Exception ex)//捕获执行字符串中C#代码的异常错误 { Console.WriteLine("Exception 2 is:"); Console.WriteLine(ex.ToString()); } Console.WriteLine("Press any key to end..."); Console.ReadLine(); } } }
执行上面的代码后,结果如下所示:
Compilation Error 1 is: (1,9): error CS0020: Division by constant zero Compilation Error 2 is: (1,9): error CS0020: Division by constant zero Exception 1 is: System.Exception: This is a demo exception. at Submission#0.<<Initialize>>d__0.MoveNext() --- End of stack trace from previous location --- at Microsoft.CodeAnalysis.Scripting.ScriptExecutionState.RunSubmissionsAsync[TResult](ImmutableArray`1 precedingExecutors, Func`2 currentExecutor, StrongBox`1 exceptionHolderOpt, Func`2 catchExceptionOpt, CancellationToken cancellationToken) at Microsoft.CodeAnalysis.Scripting.Script`1.RunSubmissionsAsync(ScriptExecutionState executionState, ImmutableArray`1 precedingExecutors, Func`2 currentExecutor, Func`2 catchExceptionOpt, CancellationToken cancellationToken) at Microsoft.CodeAnalysis.Scripting.ScriptStateTaskExtensions.GetEvaluationResultAsync[T](Task`1 task) at Net8CSharpScriptDemoNamespace.Program.Main(String[] args) in C:\Users\scohu\source\repos\Net8CSharpScriptDemo\Net8CSharpScriptDemo\Program.cs:line 38 Exception 2 is: System.Exception: This is a demo exception. at Submission#0.<<Initialize>>d__0.MoveNext() --- End of stack trace from previous location --- at Microsoft.CodeAnalysis.Scripting.ScriptExecutionState.RunSubmissionsAsync[TResult](ImmutableArray`1 precedingExecutors, Func`2 currentExecutor, StrongBox`1 exceptionHolderOpt, Func`2 catchExceptionOpt, CancellationToken cancellationToken) at Net8CSharpScriptDemoNamespace.Program.Main(String[] args) in C:\Users\scohu\source\repos\Net8CSharpScriptDemo\Net8CSharpScriptDemo\Program.cs:line 50 Press any key to end...
我们还可以在字符串中的C#代码里使用NuGet包,例如Json.NET(Newtonsoft.Json),为此我们要先在项目中安装Json.NET的NuGet包:

然后,如果我们在项目的代码中调用了Json.NET的代码,那么这会使得Program类所在的当前程序集也包含Json.NET的程序集,所以ScriptOptions.Default.WithReferences方法就不用引用Json.NET的程序集了:
using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.Scripting; using Newtonsoft.Json; using System.Text; namespace Net8CSharpScriptDemoNamespace { public class Person { public string? Name { get; set; } public int Age { get; set; } } public class Program { public static async Task Main(string[] args) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("Person person=new Person(){Name=\"王大锤,Jack Wang\",Age=36};"); stringBuilder.AppendLine("string json=JsonConvert.SerializeObject(person);"); stringBuilder.AppendLine("return json;"); string code = stringBuilder.ToString(); Type type = typeof(JsonConvert);//在当前代码中调用Json.NET的代码,这会使得Program类所在的当前程序集也包含Json.NET的程序集,这样下面的ScriptOptions.Default.WithReferences方法就不用引用Json.NET的程序集了 var options = ScriptOptions.Default .WithReferences(typeof(Program).Assembly)//添加Program类所在的当前程序集(也是Person类所在的程序集),此时因为在上面调用了Json.NET的代码,所以Program类所在的程序集也包含了Json.NET的程序集,就不用在这里再添加Json.NET的程序集了 .WithImports("Newtonsoft.Json", "Net8CSharpScriptDemoNamespace");//添加Json.NET的命名空间和Person类所在的命名空间 object json1 = await CSharpScript.EvaluateAsync(code, options: options); Console.WriteLine("json1 is \r\n {0}", json1.ToString()); Script<object> script = CSharpScript.Create(code, options: options); ScriptRunner<object> scriptDelegate = script.CreateDelegate(); object json2 = await scriptDelegate(); Console.WriteLine("json2 is \r\n {0}", json2.ToString()); Console.WriteLine("Press any key to end..."); Console.ReadLine(); } } }
如果我们在项目中没有直接调用Json.NET的代码,那么Program类所在的当前程序集不会包含Json.NET的程序集,所以ScriptOptions.Default.WithReferences方法要引用Json.NET的程序集,来供字符串中的C#代码调用:
using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.Scripting; using System.Reflection; using System.Text; namespace Net8CSharpScriptDemoNamespace { public class Person { public string? Name { get; set; } public int Age { get; set; } } public class Program { public static async Task Main(string[] args) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("Person person=new Person(){Name=\"王大锤,Jack Wang\",Age=36};"); stringBuilder.AppendLine("string json=JsonConvert.SerializeObject(person);"); stringBuilder.AppendLine("return json;"); string code = stringBuilder.ToString(); var options = ScriptOptions.Default .WithReferences(typeof(Program).Assembly, Assembly.LoadFrom("Newtonsoft.Json.dll"))//添加Program类所在的当前程序集(也是Person类所在的程序集),和Json.NET的程序集(Newtonsoft.Json.dll) .WithImports("Newtonsoft.Json", "Net8CSharpScriptDemoNamespace");//添加Json.NET的命名空间和Person类所在的命名空间 object json1 = await CSharpScript.EvaluateAsync(code, options: options); Console.WriteLine("json1 is \r\n {0}", json1.ToString()); Script<object> script = CSharpScript.Create(code, options: options); ScriptRunner<object> scriptDelegate = script.CreateDelegate(); object json2 = await scriptDelegate(); Console.WriteLine("json2 is \r\n {0}", json2.ToString()); Console.WriteLine("Press any key to end..."); Console.ReadLine(); } } }
执行上面两段代码,输出结果如下:
json1 is
{"Name":"王大锤,Jack Wang","Age":36}
json2 is
{"Name":"王大锤,Jack Wang","Age":36}
Press any key to end...
这里还是推荐第一种方法,也就是在项目的代码中调用Json.NET的代码,这样ScriptOptions.Default.WithReferences方法就不用引用Json.NET的程序集了。
关于CSharpScript类的介绍,还可以参考下面的一些文章:
How to Safely Execute Dynamic C# Code at Runtime Using Roslyn
浙公网安备 33010602011771号