C#脚本化(Roslyn):如何在运行时引入nuget包

假设我们开发了一个C#脚本编辑器,利用Roslyn去执行用户所编写的脚本。这时候,如果用户想要引用一个nuget包,应该如何实现呢?

我们想要引用nuget包的话,只要能得到nuget包及其依赖包的所有程序集和资源文件就可以了。如何引用程序集,可以看这一篇:使用Roslyn脚本化C#时如何调用不包含在运行时中的程序集

朴素思路

一种朴素的思路是:下载所需的nuget包,然后从nuget包中解压出程序集和其他资源文件,再遍历nuget包的所有依赖,执行同样的操作,最后得到程序集,就可以让Roslyn来引用它们了。

这个方法思路简单清晰,但同时,操作起来也比较复杂,要处理好两个问题:

  1. nuget包的所有输出都要放到正确的目录下
  2. nuget包的依赖不能有遗漏

另一种解法

在引用nuget包来编译项目时,nuget相关的程序集都会输出到输出目录下,基于这一点,对本文所述的问题就有另一种解法。这个方法的主要思路是,创建一个空的C#项目,然后在这个项目中引用在C#脚本中需要调用的nuget包,再用dotnet.exe去编译这个项目,那么nuget包及其依赖项所包含的所有程序集和资源文件都会输出到输出目录下面去,那么我们在这个目录下面去执行C#脚本,自然就能够找到所有所需的程序集和资源文件了。

下面我们来详细说明一下这个方法的具体流程。

第一步 创建辅助C#项目

我们需要一个空的C#项目,来辅助我们获取我们所需的nuget包相关的文件。

首先根据csproj文件的模板创建一个新的csproj文件,通过PackageReference标签引用我们想要引入的nuget包。OutputType设置为Exe,这样被引用的nuget的程序集才会输出到目录。

复制代码
<Project Sdk="Microsoft.NET.Sdk">
  <OutputType>Exe</OutputType>
  ...
  <ItemGroup>
    <PackageReference Include="newtonsoft.json" Version="13.0.3" />
  </ItemGroup>
  ...
</Project>

然后,像普通项目一样,添加一个Program.cs文件,在文件里写一些简单的代码,注意不要引入其他不必要的包。例如:

复制代码
using System;

namespace Application
{
    class Program
    {
        static void Main(string[] args)
        {
            _ = 0;
        }
    }
}

如果我们选择的.NET和C#版本支持顶级语句和global using特性,那么也可以只写

复制代码
_ = 0;

第二步 调用dotnet编译

我们可以使用dotnet.exe去编译这个辅助项目,命名如下

复制代码
dotnet build <csproj file path>

编译成功后,输出目录下面就能找到nuget包输出的程序集和资源文件,下面以一个既有依赖包,又有资源文件的nuget包为例,可以看到,输出目录下包含了nuget包本身的程序集、资源文件,以及依赖包的程序集和资源文件。

我们得到了这些程序集,就可以引用它们了,效果跟我们引用nuget包一样。

代码实战

下面用一个简化版的代码来验证这一方法,在demo中我们通过Rosly来执行一段使用Newtonsoft.Json来执行序列化的C#脚本。

复制代码
using System.Diagnostics;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var refs = LoadNuget("Newtonsoft.Json", "13.0.3").GetAwaiter().GetResult();

            Console.WriteLine($"Reference assembly:\n");

            foreach (var @ref in refs)
            {
                Console.WriteLine(@ref);
            }
            Console.WriteLine("\n");

            var code2 = @"
            using Newtonsoft.Json;

            record Person(string Name, int Age);

            var p = new Person(""Jack"", 19);

            var str = JsonConvert.SerializeObject(p);

            System.Console.WriteLine(str);

            ";

            Console.WriteLine("Script result:\n");
            try
            {
                var metas = refs.Select(r => MetadataReference.CreateFromFile(r));
                var options = ScriptOptions.Default.AddReferences(metas);
                CSharpScript.Create(code2, options).RunAsync().GetAwaiter().GetResult();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
                throw;
            }
        }

        public static async Task<IEnumerable<string>> LoadNuget(string package_id, string version)
        {
            Directory.CreateDirectory(".restore");

            //创建dotnet控制台项目
            await Process.Start(@"C:\Program Files\dotnet\dotnet.exe", "new console -o .restore\\program").WaitForExitAsync();

            //添加nuget包引用
            await Process.Start(@"C:\Program Files\dotnet\dotnet.exe", $"add .restore\\program package {package_id} -v {version}").WaitForExitAsync();

            //编译
            await Process.Start(@"C:\Program Files\dotnet\dotnet.exe", "build .restore\\program --interactive --nologo -o .restore\\program\\bin").WaitForExitAsync();

            //找出输出路径下的dll
            var dlls = Directory.GetFiles(Path.Combine(Directory.GetCurrentDirectory(), ".restore\\program\\bin"), "*.dll");
            return dlls.Where(d => !Path.GetFileName(d).ToLower().Equals("program.dll"));
        }
    }
}

输出结果如下:

复制代码
Reference assembly:

D:\Projects\CSharp Projects\RebootAndScript\ConsoleApp1\bin\Debug\net8.0\.restore\program\bin\Newtonsoft.Json.dll


Script result:

{"Name":"Jack","Age":19}
相关推荐
shandianchengzi2 小时前
【记录】Unity|Unity从安装到打开一个Github项目(以我的世界(仿)为例)
unity·c#·游戏引擎·github·我的世界·mc
咕白m6253 小时前
通过 C# 给Word文档添加水印:文字水印、图片水印
后端·c#
YuanlongWang5 小时前
C# 基础——async/await 的实现原理与最佳实践
开发语言·c#
kalvin_y_liu5 小时前
ManySpeech —— 使用 C# 开发人工智能语音应用
开发语言·人工智能·c#·语音识别
唐青枫6 小时前
C#.NET FluentSqlKata 全面解析:基于链式语法的动态 SQL 构建
c#·.net
加号316 小时前
【C#】获取电脑网卡MAC地址
windows·c#
yi碗汤园16 小时前
【超详细】C#自定义工具类-StringHelper
开发语言·前端·unity·c#·游戏引擎
sali-tec16 小时前
C# 基于halcon的视觉工作流-章49-网面破损
开发语言·图像处理·算法·计算机视觉·c#
YuanlongWang16 小时前
c# ABP vNext 框架详解及其模块化开发思想介绍
开发语言·c#
张人玉16 小时前
WPF布局控件(界面骨架核心)
开发语言·c#·wpf·布局控件