Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reliably generating C# code in .NET Core 2.x csproj project?

Description

I have been unable to get C# code generation to work reliably in my .NET project. I can get it to build EITHER (a) when the source files exist beforehand OR (b) when the source files do NOT exist beforehand. I can't get the same settings to work in both scenarios.

Why this matters: If I'm building on my development machine, I've probably built the code before, so I need it to regenerate the source that exists. However, when building on the build machine, those files do NOT exist, so I need it to generate the code from scratch in that case.

Setup

A csproj and a single source file are all that's needed to duplicate this.

Here's a trivial program that references a sample GeneratedClass:

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

Here's the simplest csproj file I could come up with.

<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>

Create an empty directory called "Generated".

To build, run dotnet build from the directory where the csproj and Program.cs file are located.

I'm running .NET Core 2.0.3 on Linux. My Docker build containers use the microsoft/dotnet:2.0-sdk image; I can replicate the issue both inside and outside of Docker.

Symptoms

Note that in the csproj file above there's a <Compile Include setting that's commented out. Note also that running the build multiple times will generate the code. (The code can be manually deleted to replicate the situation where the code does not exist at the beginning of the build.)

Here's the matrix of where I see errors and where I don't:

+----------------------+----------------------+-----------------------------------+
| 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"           |
+----------------------+----------------------+-----------------------------------+

The full error text of the "specified more than once" error: /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]

The full error text of the "does not exist" error: Program.cs(5,34): error CS0103: The name 'GeneratedClass' does not exist in the current context [/home/user/tmp/CodeGenExample.csproj]

Plea for Help

My best guess is that my BeforeTargets="CoreCompile" is wrong. I've tried lots of different values there (sorry don't remember which ones) and I always ran into some issue like this or another one. That's just a guess.

What am I doing wrong?

like image 794
Logical Fallacy Avatar asked Mar 02 '18 18:03

Logical Fallacy


Video Answer


1 Answers

Disclaimer: You seem to have things in your real project that isn't in the above, so I am unsure if this solution will work.

The following is a hacky method, in that it doesn't quite behave as it should.
However it maybe good enough for your purposes - that is for you to decide. The reason I say it is hacky is that the pre-build file deletion does seem to execute more than once.1

The csproj file that I have does this:

  1. Delete any files in the Generated directory. This is done through the CleanGen target and kicked off as an initial target in the Project node.
  2. The GeneratedCode target appends to the output file, so as to prove that it only happens once.
  3. The ItemGroup node is enabled to allow the generated file to be compiled.
  4. Echoes the variable $(NuGetPackageRoot) to show that it is set.

Complete csproj file here:

<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>

This really does seem like it is harder than it should be...


1 OP notes that to avoid executing the rm command multiple times, you can add a Condition to Exec:

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

Unfortunately Exists doesn't accept globs, so you have to specify at least one specific file that you know will be generated in that folder. With this compromise, you could also get rid of IgnoreExitCode="true" since it should only be executed when there are files to be deleted.

like image 77
chue x Avatar answered Oct 22 '22 16:10

chue x