I made a NuGet package which generates C# code from DSL files at design and build time. It works fine in Visual Studio but has some issues in Rider (which I'll describe below).
In Visual Studio
The package declares custom items for the DSL files, and assigns them a MSBuild:Compile
Generator
metadata, which triggers a design-time build in Visual Studio every time a file changes.
Then, a custom target hooks to the build with BeforeTargets="CoreCompile"
. It generates the C# code and adds Compile
items to the project. The target runs in both normal and design-time builds, and supports incremental builds to avoid unnecessary work.
Here's the relevant (simplified and commented) MSBuild code:
The props
file:
<Project>
<!-- Define the item type with its default metadata -->
<ItemDefinitionGroup>
<ZebusMessages>
<Generator>MSBuild:Compile</Generator>
</ZebusMessages>
</ItemDefinitionGroup>
<!-- Make the item type user-visible in VS -->
<ItemGroup>
<AvailableItemName Include="ZebusMessages" />
<PropertyPageSchema Include="$(MSBuildThisFileDirectory)ZebusMessages.xml" />
</ItemGroup>
<!-- Include all .msg files by default -->
<ItemGroup>
<ZebusMessages Include="**\*.msg" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>
</Project>
The targets
file:
<Project>
<ItemGroup>
<!-- Add a GeneratorTargetPath metadata for the target file -->
<ZebusMessages Update="@(ZebusMessages)" GeneratorTargetPath="$([MSBuild]::ValueOrDefault('$(IntermediateOutputPath)ZebusMessages/%(RecursiveDir)%(FileName)%(Extension).cs', '').Replace('\', '/'))" />
<!-- Remove all None items for .msg files -->
<None Remove="**\*.msg" />
</ItemGroup>
<Target Name="GenerateZebusMessages"
BeforeTargets="CoreCompile"
Condition="'@(ZebusMessages)' != ''"
Inputs="@(ZebusMessages)"
Outputs="@(ZebusMessages->'%(GeneratorTargetPath)')">
<!-- Generate output files -->
<GenerateZebusMessagesTask InputFiles="@(ZebusMessages)" />
<ItemGroup>
<!-- Add the output items -->
<Compile Include="@(ZebusMessages->'%(GeneratorTargetPath)')" Visible="false" />
<FileWrites Include="@(ZebusMessages->'%(GeneratorTargetPath)')" />
</ItemGroup>
</Target>
</Project>
The full files are available here.
In Rider
Tested with Rider 2018.3.3:
This didn't work out of the box in Rider, as it doesn't seem to trigger design-time builds like VS does.
At first, Rider was totally unaware of the generated C# files, which caused it to display errors in code that references the generated classes.
I used Rider's internal mode which exposes a "Reload Project and Show Logs" action on the project, which shows that Rider calls MSBuild on project load with the following targets:
GenerateAssemblyInfo;_CheckForInvalidConfigurationAndPlatform;GetFrameworkPaths;ResolvePackageDependenciesDesignTime;CollectPackageReferences;BeforeResolveReferences;ResolveAssemblyReferences;ResolveComReferences;ResolveSDKReferences;ResolveCodeAnalysisRuleSet
Notice the lack of CompileDesignTime
or Compile
in that list.
So I added the following target to support Rider:
<Target Name="GenerateZebusMessagesRiderDesignTime"
Condition="'$(BuildingByReSharper)' == 'true'"
AfterTargets="PrepareForBuild"
DependsOnTargets="GenerateZebusMessages" />
It hooks to PrepareForBuild
which ResolveAssemblyReferences
depends on, and calls GenerateZebusMessages
.
This makes Rider aware of the generated files, but there are issues I'd like to solve:
- If I edit a .msg file, the generated C# code is only updated when the project is built, and Rider's code completion is not up to date until then.
- If I add a .msg file to the project, I need to reload the project for it to be taken into account. Building the project doesn't help here.
Question
I'd like to make Rider call a MSBuild target and reevaluate the project whenever a .msg file is edited or is added/removed.
I only found the following in the logs when adding a file, but it doesn't help much:
10:54:05.076 |I| ProjectModel | RequestBuilder thread:7 | Add item ZebusMessages = 'OtherFile.msg'...
10:54:05.076 |I| ProjectModel | RequestBuilder thread:7 | Item matches to a wildcard pattern, mark project as dirty
10:54:05.095 |I| ProjectModel | RequestBuilder thread:7 | Project file content requested: ZebusMessages.csproj
10:54:08.330 |I| ProjectModel | RequestBuilder thread:7 | Item with EvaluatedInclude 'OtherFile.msg' was already changed. Perform project reevaluation before processing.
10:54:08.361 |I| ProjectModel | RequestBuilder thread:7 | Project 'ZebusMessages.csproj' was reevaluated in 22 ms, EvaluationCounter: 9
Nothing else mentions MSBuild in this time frame in the logs.
Is it possible to trigger something similar to a design-time build in Rider when a given file type changes?
Or more generally, under what circumstances does Rider call MSBuild to reevaluate a project?