假设我们开发了一个C#脚本编辑器,利用Roslyn去执行用户所编写的脚本。这时候,如果用户想要引用一个nuget包,应该如何实现呢?
我们想要引用nuget包的话,只要能得到nuget包及其依赖包的所有程序集和资源文件就可以了。如何引用程序集,可以看这一篇:使用Roslyn脚本化C#时如何调用不包含在运行时中的程序集
朴素思路
一种朴素的思路是:下载所需的nuget包,然后从nuget包中解压出程序集和其他资源文件,再遍历nuget包的所有依赖,执行同样的操作,最后得到程序集,就可以让Roslyn来引用它们了。
这个方法思路简单清晰,但同时,操作起来也比较复杂,要处理好两个问题:
- nuget包的所有输出都要放到正确的目录下
- 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}