可靠地在.NET Core 2.x csproj项目中生成C#代码?

8

描述

我在我的.NET项目中无法可靠地生成C#代码。我可以在源文件事先存在或不存在的情况下构建它们。但是,我不能让相同的设置在这两种情况下都起作用。

为什么这很重要:如果我在开发机器上构建,则可能以前已经构建了代码,因此我需要它重新生成现有的源代码。但是,在构建机器上构建时,这些文件不存在,因此我需要在这种情况下从头开始生成代码。

设置

只需一个csproj和单个源文件即可复制此设置。

下面是引用示例GeneratedClass的简单程序:

class Program
{
    public static void Main(string[] args)
    {
        System.Console.WriteLine(GeneratedClass.MESSAGE);
    }
}

这是我能想到的最简单的csproj文件。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>
  <Target Name="GenerateCode" BeforeTargets="CoreCompile">

    <!-- Removing the source code beforehand makes no difference
    <Exec Command="rm $(ProjectDir)Generated/*.cs" IgnoreExitCode="true" />
    -->

    <Exec Command="echo 'class GeneratedClass { public static int MESSAGE = 1; }' > Generated/GeneratedClass.cs" />

    <!-- Toggling this setting will cause failures in some scenarios and success in others
    <ItemGroup>
      <Compile Include="Generated/*$(DefaultLanguageSourceExtension)" />
    </ItemGroup> -->

  </Target>
</Project>

创建一个名为“Generated”的空目录。
在包含csproj和Program.cs文件的目录中运行dotnet build以进行构建。
我在Linux上运行.NET Core 2.0.3。我的Docker构建容器使用microsoft/dotnet:2.0-sdk镜像;我可以在Docker内外复制该问题。
症状
请注意,在上面的csproj文件中,有一个被注释掉的<Compile Include设置。同时请注意,多次运行构建将生成代码。(可以手动删除代码以复制代码一开始不存在的情况。)
下表是我看到错误和未看到错误的矩阵:
+----------------------+----------------------+-----------------------------------+ | Compile Include=...? | Code Already Exists? | Result | +----------------------+----------------------+-----------------------------------+ | Present | YES | ERROR! "specified more than once" | | Present | NO | SUCCESS! | | Commented Out | YES | SUCCESS! | | Commented Out | NO | ERROR! "does not exist" | +----------------------+----------------------+-----------------------------------+
“指定了多次”错误的完整错误文本:/usr/share/dotnet/sdk/2.0.3/Roslyn/Microsoft.CSharp.Core.targets(84,5): error MSB3105: The item "Generated/GeneratedClass.cs" was specified more than once in the "Sources" parameter. Duplicate items are not supported by the "Sources" parameter. [/home/user/tmp/CodeGenExample.csproj] “不存在”错误的完整错误文本:Program.cs(5,34): error CS0103: The name 'GeneratedClass' does not exist in the current context [/home/user/tmp/CodeGenExample.csproj] 请求帮助
我最好的猜测是我的BeforeTargets="CoreCompile"是错误的。我尝试了很多不同的值(抱歉,不记得哪些)我总是遇到像这样或另一个问题。这只是一个猜测。
我做错了什么?

.NET 2.0和.NET Standard 2.0是非常不同的东西。请更新您的问题标题。 - Dai
@Dai已修复。我正在使用Core。该文件是标准的。 - Logical Fallacy
这里有一个相关的问题链接,但是当生成的文件已经存在时,该解决方案仍然会失败。 - chue x
2个回答

4

声明:您似乎在实际项目中有一些不在上面提到的东西,因此我不确定这个解决方案是否有效。

以下是一种欺骗性方法,它的行为不完全符合预期。
但是对于您的目的来说,可能已经足够好了 - 这取决于您自己的决定。我之所以说它是欺骗性的,是因为预构建文件删除似乎执行了多次。1

我的csproj文件会执行以下操作:

  1. 删除生成目录中的任何文件。这是通过CleanGen目标完成的,并作为Project节点中的初始目标启动。
  2. GeneratedCode目标将附加到输出文件,以证明它只发生一次。
  3. 启用ItemGroup节点以允许编译生成的文件。
  4. 回显变量$(NuGetPackageRoot)以显示它已设置。

完整的csproj文件在此处:

<Project InitialTargets="CleanGen" Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>
  <Target Name="CleanGen">
    <Exec Command="echo 'Cleaning files...'" />
    <Exec Command="rm $(ProjectDir)Generated/*$(DefaultLanguageSourceExtension)" IgnoreExitCode="true" />
  </Target>
  <Target Name="GenerateCode" BeforeTargets="CoreCompile">
    <Exec Command="echo 'Generating files... $(NuGetPackageRoot)'" />
    <Exec Command="echo 'class GeneratedClass { public static int MESSAGE = 1; }' >> Generated/GeneratedClass.cs" />

    <ItemGroup>
      <Compile Include="Generated/*$(DefaultLanguageSourceExtension)" />
    </ItemGroup>
  </Target>
</Project>

这似乎比它应该的要难…


1 OP指出,为了避免多次执行rm命令,可以在Exec中添加一个Condition

<Exec 
    Command="rm $(ProjectDir)Generated/*$(DefaultLanguageSourceExtension)"
    Condition="Exists('$(ProjectDir)Generated/GeneratedClass$(DefaultLanguageSourceExtension)')" />

不幸的是,Exists 不支持通配符,所以您必须指定至少一个您知道将在该文件夹中生成的特定文件。通过这种妥协,您还可以摆脱 IgnoreExitCode="true",因为它只应在有文件需要删除时执行。


虽然这样做可以使构建成功,但实际上它会执行该目标四次。向该目标添加另一个命令,例如 <Exec Command="echo 'hi $(NuGetPackageRoot)' >> hi-counts.txt" />,将导致生成一个包含4行的文件,其中有两行的 $(NuGetPackageRoot) 是空字符串。 - Logical Fallacy
我修改了答案,但仍然有些不完美... 你看看它是否适用于你。 - chue x
这可能最终成为我采用的解决方案。我喜欢这些新增内容,并且我同意这似乎比应该的要困难得多。在我的尝试中,我在目标上添加了一个条件来检查$(NuGetPackageRoot)是否为空,但我会尝试使用你的方法。我更愿意事先删除源文件,就像你做的那样,所以当我回到我的桌子前几个小时,我很想试一试。 - Logical Fallacy
当我意识到NuGetPackageRoot的问题时,我是否应该重新编写整个问题,使用更接近实际用例的内容,即从.proto文件生成C# gRPC服务。目前有一项自动化工作正在进行中(https://github.com/grpc/grpc/pull/13207),但我需要一些临时方案。除了我在这个SO问题中创建的问题之外,它基本上已经解决了。如果你认为我应该重新写问题,我可以这样做。但是如果您的解决方案有效,我会很开心的。 - Logical Fallacy
我认为你不需要重写这个问题。我认为nuget变量未被设置是命令多次运行的副作用...如果你真的想知道答案,这可能是一个单独的问题,你可以问一下。 - chue x
Visual Studio 2017(带有ReSharper)会疯狂进入生成和删除文件的无限循环。 - altso

0

我通过重新包含生成的项目使其正常工作:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>
  <Target Name="GenerateCode" BeforeTargets="CoreCompile">

    <Exec Command="mkdir Generated" Condition="!Exists('Generated')" />
    <Exec Command="echo class GeneratedClass { public static int MESSAGE = 1; } > Generated/GeneratedClass.cs" />

    <ItemGroup>
      <Compile Include="Generated/*$(DefaultLanguageSourceExtension)" Exclude="@(Compile)" />
    </ItemGroup>

  </Target>
</Project>

2019年4月16日更新:

SpecFlow 3使用巧妙的Exclude="@(Compile)"技巧来编译生成的文件(https://specflow.org/2019/updating-to-specflow-3/):

<ItemGroup>
  <Compile Include="Generated/*$(DefaultLanguageSourceExtension)" Exclude="@(Compile)" />
</ItemGroup>

更新于2018年9月20日:

请参考此处的示例git仓库: https://github.com/altso/SO49075282

cmd中重现步骤:

C:\Temp>dotnet --version
2.1.402

C:\Temp>git clone https://github.com/altso/SO49075282.git
Cloning into 'SO49075282'...
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 5 (delta 0), reused 5 (delta 0), pack-reused 0
Unpacking objects: 100% (5/5), done.

C:\Temp>cd SO49075282

C:\Temp\SO49075282>dotnet build
Microsoft (R) Build Engine version 15.8.166+gd4e8d81a88 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

  Restoring packages for C:\Temp\SO49075282\SO49075282.csproj...
  Generating MSBuild file C:\Temp\SO49075282\obj\SO49075282.csproj.nuget.g.props.
  Generating MSBuild file C:\Temp\SO49075282\obj\SO49075282.csproj.nuget.g.targets.
  Restore completed in 311.61 ms for C:\Temp\SO49075282\SO49075282.csproj.
  SO49075282 -> C:\Temp\SO49075282\bin\Debug\netcoreapp2.1\SO49075282.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:02.95

C:\Temp\SO49075282>dotnet build
Microsoft (R) Build Engine version 15.8.166+gd4e8d81a88 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

  Restore completed in 60.66 ms for C:\Temp\SO49075282\SO49075282.csproj.
  SO49075282 -> C:\Temp\SO49075282\bin\Debug\netcoreapp2.1\SO49075282.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:02.17

C:\Temp\SO49075282>

使用那个文件,我仍然得到错误信息“当前上下文中不存在'GeneratedClass'的名称”。 - Logical Fallacy
@LogicalFallacy 对我有效。我在答案中添加了更多细节。 - altso
太奇怪了。我向你的存储库提出了一个Dockerfile,执行相同的步骤并得到了错误。我唯一看到的不同之处是你在Windows上,但这似乎不可能是原因。令人困惑。 - Logical Fallacy
@LogicalFallacy 我更新了答案,加入了 Exclude="@(Compile)" 的解决方案。 - altso

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接