• 博客园logo
  • 会员
  • 众包
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • HarmonyOS
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
PowerCoder
博客园    首页    新随笔    联系   管理    订阅  订阅

执行在字符串中定义的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

C# CSharpScript 的原理与应用

C#脚本Script

 

posted @ 2025-06-19 19:46  PowerCoder  阅读(85)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3