Nesting files in a project

As part of the code generation work, I wanted to group the generated code file with the associated source. This was provided for free under project.json if you kept the filename the same and simply appended a new extension, however, this has been lost in the conversion to MSBuild files.

Solution Explorer

Having said that, the project file format for .NET core projects in Visual Studio 2017 has been greatly simplified, for example, you no longer have an entry for each file in the project. There also exists a nice Microsoft.Build NuGet package that simplifies working with the project file and, as a bonus, it also works for .NET Core applications.

Implementation

The way to get Visual Studio to group an item is by adding a Compile item with the DependentUpon set to the parent item, i.e.:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    ...
  </PropertyGroup>

  <ItemGroup>
    <Compile Update="Child.cs">
      <AutoGen>True</AutoGen>
      <DependentUpon>Parent.txt</DependentUpon>
    </Compile>
  </ItemGroup>
</Project>

This can be done in code quite easily by the following code:

string project = "Example.csproj";
string childName = "Example.txt.cs";
string parentName = "Example.txt";

// Load the project file
var root = ProjectRootElement.Open(
    project,
    ProjectCollection.GlobalProjectCollection,
    preserveFormatting: true);

// We need to put the Compile in an ItemGroup
ProjectItemGroupElement group = root.AddItemGroup();

// MUST set the Update attribute before adding it to the group
ProjectItemElement compile = root.CreateItemElement("Compile");
compile.Update = childName;
group.AppendChild(compile);

// MUST be in the group before we can add metadata
compile.AddMetadata("AutoGen", "True");
compile.AddMetadata("DependentUpon", parentName);

// Save changes
root.Save();

As you can see, the code follows the XML structure and can be easily expanded to re-use the same ItemGroup for all the dependent items. Also worth noting is that I’m preserving the formatting of the project file by specifying true for the preserveFormatting parameter in the call to the Open method, as the project files can now be edited manually from within Visual Studio it would annoy me if another tool had overwritten my hand-crafted changes 🙂

Adding a tool to a .NET Core build

I need a command line tool to run over some files in a project and generate some code that can then be built with the rest of the project’s source code. However, the documentation to get this to be an automatic step of the build is a bit lacking – there are plenty of articles on creating a .NET Core CLI tool, however, these have to be manually invoked at the command prompt.

Background

.NET Core projects files (the Visual Studio 2017 csproj type) allow the NuGet packages to be specified in the same file as any other project/assembly reference. It also allows a special type of NuGet package that was created as a DotNetCliTool type to be pulled into a project and integrated with the rest of the dotnet tooling, for example:

dotnet restore
dotnet my-tool

The tool is just a standard .NET Core console application that runs in the projects directory and can receive command line arguments as any other console program can (i.e. dotnet my-tool argument would pass ‘argument’ to the entry point of the program).

Creating a tool

Creating a tool is simple: create a new .NET Core console app (MUST be a .NET Core App 1.0, not 1.1) and edit the project file to set the package type to be a .NET CLI tool (at the moment there’s no UI for this in Visual Studio, so you’ll have to edit the csproj file, but that can be done from Visual Studio now by right clicking on the project and then there will be an Edit option in the context menu).

Here’s an example project file:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp1.0</TargetFramework>
    <PackageType>DotNetCliTool</PackageType>
  </PropertyGroup>
</Project>

Another thing that we need to do is ensure the application is prefixed with dotnet- in order for it to work with dotnet. This can be done via the UI (the Assembly name in the properties) or by editing the csproj:

<PropertyGroup>
  ...
  <AssemblyName>dotnet-example-tool</AssemblyName>
</PropertyGroup>

You can now build that and then create a NuGet package by running the dotnet pack command in the project’s folder.

Using the tool

For testing purposes, it’s probably easier to put the tool on your local machine and setting up Visual Studio to use the local package source:

NuGet Source

In another project, you can now reference the tool as a NuGet package. Unfortunately, at the time of writing the Visual Studio NuGet UI throws an error of “Package ‘ExampleTool’ has a package type ‘DotNetCliTool’ that is not supported by project ‘ExampleConsole’”. This means you’ll need to edit the csproj file to include it manually:

<ItemGroup>
  <DotNetCliToolReference Include="ExampleTool" Version="1.0.0" />
</ItemGroup>

To test this works, open the command prompt in the project’s directory and run the following:

dotnet restore
dotnet example-tool

You should see “Hello World!” displayed (or whatever you made your program do). We’re nearly there, however, I wanted to the tool to run before each build so it can generate some source files that get swept up in the build – at the moment it’s a manual step.

Integration with the build

Although the project.json was nice and compact, one advantage of moving back to MSBuild is we can now take advantage of the exec task to run our tool during the build. To do this, edit the csproj file of the project consuming the tool to add the following:

<Target Name="RunExampleTool" BeforeTargets="Build">
  <Exec Command="dotnet example-tool" WorkingDirectory="$(ProjectDir)" />
</Target>

Now when you build the solution, if you check the build output you should see “Hello World!” in there. Success!

Analysing the above, you can see we’ve created a target that runs before the built in Build task (note you used to be able to just name your task BeforeBuild, however, this no longer works – I’m guessing it’s a result of no longer including the common targets?). Next the task the target will run is an exec task, which invokes our tool (via the dotnet command). Here the important thing to note is that we’re setting the working directory to be the project’s directory, which is achieved using the MSBuild macro $(ProjectDir) – dotnet will only automatically pick up the tool from the NuGet packages is we’re in that directory.

That’s it, quite simple but took me a while to piece everything together as the documentation is missing in some areas and I guess it’s not a common thing I’m trying to achieve.