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.
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.
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]
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?
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:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With