我正在寻找一种方法来扩展 C# 项目的现有 MSBuild 编译流程,方法是在编译器拾取某些代码文件之前重写它们。更具体地说,我想使用顶级语句处理文件,并将底层代码包装在自定义类/方法中,而不是自动生成的<Program>$.Main().
换句话说,给定一个具有以下两个编译单元的 C# 项目:
Foo.csConsole.WriteLine("Foo");
Run Code Online (Sandbox Code Playgroud)
Bar.csConsole.WriteLine("Bar");
Run Code Online (Sandbox Code Playgroud)
我想注入一些处理逻辑,以便将这些文件传递给编译器:
Foo.g.cspublic static class Foo
{
public static void Execute()
{
Console.WriteLine("Foo");
}
}
Run Code Online (Sandbox Code Playgroud)
Bar.g.cspublic static class Bar
{
public static void Execute()
{
Console.WriteLine("Bar");
}
}
Run Code Online (Sandbox Code Playgroud)
同时,我希望用户在编辑原始文件Foo.cs和Bar.cs文件时保持完整的静态分析、自动完成和其他编译器驱动的 IDE 功能。这意味着,如果生成的文件包含错误,我希望将其报告在原始文件上。
最终,我的目标是使用户能够在多个文件中编写顶级语句,同时让我的框架调用Execute()生成的代码来执行所有这些语句(顺序并不重要)。
为了实现这样的事情,我需要采取的最佳方法和最少的步骤是什么?
我最初的想法是执行以下操作:
<None>项目包含在内,以便它们保留为项目的一部分#line指令将生成文件中的所有静态分析重新映射到原始文件Project.csproj<ItemGroup>
<Compile Remove="*.cs" />
<None Include="*.cs" />
</ItemGroup>
Run Code Online (Sandbox Code Playgroud)
Foo.g.cspublic static class Foo
{
public static void Execute()
{
#line 1 "File.cs"
Console.WriteLine("Foo");
}
}
Run Code Online (Sandbox Code Playgroud)
这可以很好地工作dotnet build(在原始文件上报告错误),但在 IDE 中表现不佳:
#pragma warning disable CS8802没有效果。这些错误也需要一段时间才能重复,有时会“卡住”,直到我手动运行dotnet build。注意:我试图使问题的描述尽可能简单,但我的主要想法是构建一个新的测试框架原型,该框架将使用 TLS 来定义测试(而不是类和方法),类似于在其他语言: https: //github.com/Tyrrrz/Hallstatt
Kit*_*Kit 12
为了维持两者
...编辑原始 Foo.cs 和 Bar.cs 文件时的完整静态分析、自动完成和其他编译器驱动的 IDE 功能。这意味着,如果生成的文件包含错误,我希望将其报告在原始文件上。
和
...使用户能够在多个文件中编写顶级语句,同时让我的框架在生成的代码上调用 Execute() 来执行所有这些...
将会很困难。
我找到了一种有严重限制和警告的方法。其他解决方案(例如源生成器、自定义预编译脚本/构建步骤和动态代码生成)可能也是可行的,但它们有自己的包袱。
注意:我并不提倡这样做。我的解决方案可能非常脆弱,并且严重滥用了 .NET 中惯用的处理方式。最终,我会让你所谓的“用户”(稍后我将在不带引号的情况下引用它)做正常的事情,在命名空间的类中编写方法(在一个文件本身中,用于源代码控制目的) 。但话虽如此,潜入...
限制1
每个顶级语句都必须位于单独的.csproj. 不幸的是,.NET 世界在施加逻辑边界(命名空间)之前为代码施加了物理边界(项目/程序集)。为了避免CS8802您别无选择,只能将每个顶级语句代码放在单独的项目中。
如果我们接受这个限制,那么编译之前的用户体验就会变得轻而易举。他们获得了顶级的静态分析乐趣,并且在调试时,他们可以运行他们的“主”并获得强大的调试器支持以及与其他正在工作的用户的隔离。
注意事项 1
但是,这很奇怪。每个单独的项目都.csproj必须是可执行文件(可能是控制台项目),对于您(框架开发人员)来说,如何处理所有这些项目?
嗯,通常的做法是添加项目引用。您需要为每一种“顶级体验”添加一个。您可以添加可执行项目作为对库或另一个可执行文件(例如您的框架)的引用,因为 .NET exe 和 dll 仍然是程序集。
它有点颠倒了构建过程。我理解不想将硬引用纳入框架。就像MEF时代一样,很可能需要某种程度的运行时检查。
限制 2 和 3
顶级语句没有命名空间,或者更准确地说,它们的命名空间是根命名空间。这意味着每个顶级语句都将具有相同的命名空间、类和方法:global::Program.<Main>$如反编译所示:
更糟糕的是,类和方法的可访问性是internal。
注意事项 2 和 3
那么如何解决internal以及如何解决相同的类型和签名呢?答案是应用InternalsVisibleToAttribute并为每个引用指定命名空间的别名。
在使用的项目中,将属性添加到顶级语句文件中(我知道,这并不酷,而且有点违背了目的,但就这样吧)。
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("ClassLibrary1")]
Console.WriteLine("Hello, World!");
Run Code Online (Sandbox Code Playgroud)
在我所调用的消费项目中ClassLibrary1,将元素添加Aliases到每个项目引用中。
<ItemGroup>
<ProjectReference Include="..\ConsoleApp1\ConsoleApp1.csproj">
<Aliases>One</Aliases>
</ProjectReference>
<ProjectReference Include="..\ConsoleApp2\ConsoleApp6.csproj">
<Aliases>Two</Aliases>
</ProjectReference>
</ItemGroup>
Run Code Online (Sandbox Code Playgroud)
为了使这种多项目体验不那么繁重,您可以为用户创建一个模板,也许可以从 C#控制台应用程序模板进行自定义。在此模板中,您可以添加一个 C# 文件,其中InternalsVisibleTo.cs包含以下内容:
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("ClassLibrary1")]
Run Code Online (Sandbox Code Playgroud)
请参阅文档了解如何创建自定义模板以及如何安装它。完成后,您的用户可以使用dotnet new nameOfTemplateVisual Studio 快速添加项目。现在创建的Program.cs不会有这些样板行,用户可以忽略InternalsVisibleTo.cs。
执行这个混乱
要调用每个用户的顶级语句,您可以使用反射和指令extern alias:
using static System.Reflection.BindingFlags;
namespace ClassLibrary1;
extern alias Two;
extern alias One;
public static class Class1
{
public static void YourExecutor()
{
var method = typeof(One::Program).GetMethod("<Main>$", NonPublic | Static);
method?.Invoke(null, new object?[] { Array.Empty<string>() });
method = typeof(Two::Program).GetMethod("<Main>$", NonPublic | Static);
method?.Invoke(null, new object?[] { Array.Empty<string>() });
}
}
Run Code Online (Sandbox Code Playgroud)
当然,您需要一种变体,它以某种方式通过反射找到所有用户的语句。我只是在这里提供了一个例子。
以上经过测试。这是一个简单的测试,它按预期运行。
[TestFixture]
public class Tester
{
[Test]
public void ThisIsCrazy()
{
Class1.YourExecutor();
}
}
Run Code Online (Sandbox Code Playgroud)
最后的警告
当您编写插件、框架并尝试泛化代码时,这显然与通常(惯用的)做事方式不同,但它确实有效。
然而,
<Main>$不再正确会发生什么)?与往常一样,这归结为编程中常见的权衡。
尾声(什么是尾声?)
如果不通过某种虚拟调度(或其他机制)对 .NET CLR 进行更改以允许多个顶级语句,即使源生成器也会出现短缺。
它们最终会很短,因为在静态分析级别和调试级别上的体验会失败,这在某种程度上依赖于静态级别。
我其实很想知道我上面的说法是否正确。
你的“要求”并不奇怪或疯狂,但它违背了当前的惯例和实施。简而言之,顶级语句的编码是有目的的:目的是简化单个程序的执行,而无需样板。
语言 (C#) 和/或运行时 (.NET) 的惯用更改将需要某种对您和您的用户来说合理的权衡(便利/样板/体验/测试)以及......更大的社区。
我认为你必须找到一个可以接受的平衡点。
| 归档时间: |
|
| 查看次数: |
686 次 |
| 最近记录: |