在 Visual Studio 中使用 C# 脚本(CSX 脚本)进行代码生成






4.88/5 (19投票s)
如何在 Visual Studio 中运行 C# 脚本 (CSX 脚本)、加载第三方库并直接生成代码
引言
CSX 脚本功能非常强大,因为它们可以使用 C# 和完整的 .NET Framework。在本文中,我将分步指导和提供脚本来运行 CSX 脚本 (C# 脚本),其中包含对外部程序集的引用 (第三方库,包括NuGet 包),使用可以直接从 Visual Studio 调用的 PowerShell 脚本。我还将展示一些技巧,使脚本在您的开发团队中无需修改即可运行。
最后,我将使用一个简单的 代码生成库,根据 AdventureWorks
数据库模式生成 POCO。
C# 脚本 (CSX 文件)
C# 脚本文件 (CSX) 是通过 Roslyn 引入的,可以在 Roslyn 或其他兼容的跨平台 脚本引擎 (如 dotnet-script) 甚至 C# REPL (称为 csi.exe) 中执行。
这些脚本引擎存在一些限制 (如缺少命名空间),但它们允许我们调用几乎任何 C# 代码,这提供了诸如强类型、编译时检查、完整的 IDE 支持 (包括调试)、跨平台 (dotnet core)、完全访问所有 .NET Framework (包括不仅 SqlServer 库,还有像 Dapper、Newtonsoft JSON 等优秀的第三方库) 等惊人功能。因此,如果我们谈论自动化,我们就获得了一个功能齐全的、具有熟悉语法的语言,而不是依赖于例如 PowerShell。如果我们谈论代码生成,我们也获得了一个功能齐全的、具有熟悉语法的语言,而不是依赖于一个只提供底层语言功能子集的模板引擎。
示例 CSX 脚本
Visual Studio 中的 CSX 脚本对 IntelliSense (自动完成) 和编译时检查有一些支持,但这些功能在 CS 文件中效果更好。因此,最好将尽可能多的代码放入 cs
文件中,而将尽可能少的代码放入 CSX 脚本中。我喜欢只在 CSX 中用于加载库、设置连接字符串、设置路径和调用 CS 文件中实际代码等基本操作。
MyProgram.cs
public class MyProgram
{
public void MyMethod()
{
Console.WriteLine("Hello from MyMethod");
}
}
MyScript.csx
#load "MyProgram.cs"
new MyProgram().MyMethod();
Console.WriteLine("Hello Code-Generation!");
使用 C# REPL (CSI.EXE) 运行 CSX 脚本
Visual Studio 附带一个名为 CSI 的命令行 REPL,可用于运行 .csx 脚本。
您可以直接从 Visual Studio Developer Command Prompt 运行 CSI.EXE (csi MyScript.csx
)
程序集引用
同样,使用简单的 CSX 语句来调用更复杂的 C# 代码是一个好主意,加载现有库的外部程序集也是如此。
CSX 允许通过脚本顶部的 #r
指令加载程序集引用
// CSI.EXE requires absolute paths for loading external assemblies:
#r "C:\Users\drizin\.nuget\packages\dapper\2.0.35\lib\netstandard2.0\Dapper.dll"
#load "File1.cs"
#load "File2.cs"
#load "MyProgram.cs"
new MyProgram().MyMethod();
Console.WriteLine("Hello Code-Generation!");
NuGet 包
如果您需要引用 NuGet 包,您可以仅依靠 NuGet 工具 (以及 Visual Studio 构建过程) 来自动恢复脚本所需的包。为了实现这一点,您可以只需将 CSX 作为 Visual Studio 项目的一部分,这样当每个开发人员尝试构建项目时,Visual Studio 都会下载缺少的包,开发人员只需修复程序集位置。
从 PowerShell 调用 C# REPL (以运行 CSX 脚本)
虽然您可以直接从 Visual Studio Developer Command Prompt 运行 CSI.exe,但通过 PowerShell 调用它在几个方面非常有帮助
- 您可以直接在 Visual Studio 之外运行。您甚至不需要 Visual Studio 来运行 CSX。
- PowerShell 允许我们使用相对路径引用外部程序集 (稍后会详细介绍)。
要使用 Powershell 调用 CSI,我们必须知道 csi.exe 的位置。
CSI 随 Visual Studio 一起提供,但根据您的 Visual Studio 版本,它可能有不同的安装位置。即使您没有 Visual Studio,也可以通过使用 NuGet 包 Microsoft.Net.Compilers.Toolset 来安装 CSI。
因此,第一步是根据我下面示例 Powershell 脚本 RunMyScript.ps1 中展示的方式在多个位置搜索 csi.exe
# Locate CSI.EXE by searching common paths
$csi = (
"$Env:userprofile\.nuget\packages\microsoft.net.compilers.toolset\3.6.0\tasks\net472\csi.exe",
"$Env:programfiles
(x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\Roslyn\csi.exe",
"$Env:programfiles
(x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\Roslyn\csi.exe",
"$Env:programfiles
(x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\Roslyn\csi.exe",
"$Env:programfiles
(x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\Roslyn\csi.exe",
"$Env:programfiles
(x86)\Microsoft Visual Studio\2017\Professional\MSBuild\15.0\Bin\Roslyn\csi.exe",
"$Env:programfiles
(x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\Roslyn\csi.exe"
) | Where-Object { Test-Path $_ } | Select-Object -first 1
$dir = Split-Path $MyInvocation.MyCommand.Path
$script = Join-Path $dir "MyScript.csx"
& $csi $script
要从命令行运行 PowerShell 脚本,只需运行
Powershell Full-Path-To-Your-Script-ps1
从 Visual Studio IDE 运行
要在 Visual Studio 中运行,您可以将 PS1 添加到项目或解决方案中,右键单击文件,然后单击“使用 PowerShell ISE 打开”选项,这是用于编辑/运行 PowerShell 脚本的 IDE。
另一种选择是,您可以为右键菜单添加新的操作 -
您可以单击“打开方式...”并将 PowerShell 配置为直接从 Visual Studio 执行
操作列表将包括直接从 IDE 调用 PS1 脚本的此新选项,您还可以将其设置为打开 PS1 文件的默认操作。
允许未签名脚本
如果您以前从未执行过未签名的 PowerShell 脚本,您可能需要通过以管理员身份运行 PowerShell (x64 版本和从 Visual Studio 内部执行的 x86 版本) 并运行此命令来启用 PowerShell 未签名脚本
Set-ExecutionPolicy -ExecutionPolicy Unrestricted
相对程序集引用
CSI 的一个主要问题是 #r
指令 (用于加载程序集引用) 不支持 类似 NuGet 的引用或环境变量,因此所有程序集引用都应指定完整路径。这不是一个致命问题,但有点烦人,因为它使得在多个开发人员之间共享代码更加困难,因为每个开发人员都必须修复他们的引用。
正如我们之前所见,CSX 期望像这样使用绝对引用
#r "C:\Users\drizin\.nuget\packages\dapper\2.0.35\lib\netstandard2.0\Dapper.dll"
使用 PowerShell (如上所述) 的一个优点是我们可以使用环境变量和使用带相对路径的 #r
指令。在 PowerShell 脚本中,我们只需要找到程序集所在的基路径,然后将其传递给 CSI,以便它可以在该文件夹中搜索程序集,如下所示
$assemblies = "${env:userprofile}\.nuget\packages\";
& $csi /lib:"$assemblies" $script
然后,在 CSX 中,您可以使用相对路径,如下所示:
#r "dapper\2.0.35\lib\netstandard2.0\Dapper.dll"
PackageReference (NuGet 4) vs packages.config (NuGet 3)
新的 MSBuild 格式 (“SDK-Style”,在 csproj 中使用 PackageReference
) 将 NuGet 包安装在此用户配置文件文件夹中。
旧的 MSBuild 格式 (“非 SDK-Style”,Visual Studio 2017 之前,使用 packages.config
) 将 NuGet 包安装在 Solution 文件夹下的 “packages” 文件夹中。
我们可以根据我们的项目将恢复 NuGet 包的位置来调整我们的 PowerShell 脚本
$csi = ... # (locate your csi.exe)
$dir = Split-Path $MyInvocation.MyCommand.Path
$script = Join-Path $dir "MyScript.csx"
# Call csi.exe and specify that libraries referenced by #r directives
# should search in a few nuget locations
# New NuGet 4.0+ (PackageReference) saves User-specific packages
# in "%userprofile%\.nuget\packages\"
$nuget1 = "${env:userprofile}\.nuget\packages\";
# New NuGet 4.0+ (PackageReference) saves Machine-wide packages
# in "%ProgramFiles(x86)%\Microsoft SDKs\NuGetPackages\"
$nuget2 = "${env:ProgramFiles(x86)}\Microsoft SDKs\NuGetPackages\";
# Old NuGet (packages.config) saves packages in "\packages" folder at solution level.
# Locate by searching a few levels above
$nuget3 = (
(Join-Path $dir ".\packages\"),
(Join-Path $dir "..\packages\"),
(Join-Path $dir "..\..\packages\"),
(Join-Path $dir "..\..\..\packages\"),
(Join-Path $dir "..\..\..\..\packages\")
) | Where-Object { Test-Path $_ } | Select-Object -first 1
# if you're using new NuGet format (PackageReference defined inside csproj)
& $csi /lib:"$nuget1" $script
# if you're using old NuGet format (packages.config)
# & $csi /lib:"$nuget3" $script
我们的 CSX 将使用相对引用
// CSX can load libraries by defining their relative paths
// New NuGets (PackageReference) are installed under "${env:userprofile}\.nuget\packages\"
// or "${env:ProgramFiles(x86)}\Microsoft SDKs\NuGetPackages\")
// and have this format:
#r "dapper\2.0.35\lib\netstandard2.0\Dapper.dll"
// Old NuGets (packages.config) are installed under "(SolutionFolder)\packages"
// and have this format
// #r "Dapper.2.0.35\lib\netstandard2.0\Dapper.dll"
//...
new MyProgram().MyMethod();
Console.WriteLine("Hello Code-Generation!");
非常棒,而且非常简单,不是吗?
创建一个简单的 POCO 生成器
到目前为止,本文已转载自我的博客上的这篇文章。为了简洁起见,我将跳过一些步骤,但在另一篇文章中,我创建了一个工具来从 SQL 数据库提取模式并将其保存为 JSON 文件。
基于这个 JSON 模式并使用 CodegenCS 代码生成库,我们可以轻松生成 POCO。
public class SimplePOCOGenerator
{
CodegenContext generatorContext = new CodegenContext();
public void Generate()
{
DatabaseSchema schema = JsonConvert.DeserializeObject<DatabaseSchema>(
File.ReadAllText(_inputJsonSchema));
foreach (var table in schema.Tables)
GeneratePOCO(table);
// This saves one .cs for each table
generatorContext.SaveFiles(outputFolder: targetFolder);
// This will add each .cs to our csproj file (if using old format)
//generatorContext.AddToProject(csProj, targetFolder);
}
void GeneratePOCO(DatabaseTable table)
{
var writer = generatorContext[table.TableName + ".cs"];
writer
.WriteLine(@"using System;")
.WriteLine(@"using System.Collections.Generic;")
.WriteLine(@"using System.ComponentModel.DataAnnotations;")
.WriteLine(@"using System.ComponentModel.DataAnnotations.Schema;")
.WriteLine(@"using System.Linq;")
.WriteLine();
writer.WithCBlock($"namespace {myNamespace}", () =>
{
writer.WithCBlock($"public partial class {table.TableName}", () =>
{
foreach (var column in table.Columns)
GenerateProperty(writer, table, column);
});
});
}
void GenerateProperty(CodegenOutputFile writer, DatabaseTable table,
DatabaseTableColumn column)
{
string propertyName = GetPropertyNameForDatabaseColumn(table, column.ColumnName);
string typeDefinition = GetTypeDefinitionForDatabaseColumn(table, column);
if (column.IsPrimaryKeyMember)
writer.WriteLine("[Key]");
if (propertyName.ToLower() != column.ColumnName.ToLower())
writer.WriteLine($"[Column(\"{column.ColumnName}\")]");
writer.WriteLine($"public {typeDefinition} {propertyName} {{ get; set; }}");
}
}
最终结果是每个表都有一个 POCO
POCO 可以由您最喜欢的微型 ORM 使用
非常棒,而且非常简单,不是吗?希望您喜欢这篇文章!
本文的完整源代码可供下载 (在顶部),并且在此处发布。
代码包含适用于 SDK 风格和非 SDK 风格项目的 CSX 和 PowerShell 脚本
- Visual Studio 2017+ (SDK 风格项目,dotnetcore 也使用此风格)
在 SDK 风格格式中,NuGet 包存储在每个用户的配置文件中。 - Visual Studio <=2015 (非 SDK 风格项目)
在非 SDK 风格格式中,NuGet 包存储在 solution 文件夹下,并且源文件必须在 csproj 中显式描述。
上面 POCO 生成器的代码只是一个简化版本 (为了简洁),但在附加的源代码中,您会找到完整的代码,它允许将 POCO 生成到多个文件中或单个文件中。
免责声明:我是 CodegenCS 代码生成库 的作者。
历史
- 2020 年 7 月 16 日:初始版本