With the migration back to MSBuild for .NET Core projects, a few new avenues are opened up to us as developers when it comes to managing our projects. Package references are now part of the MSBuild XML definition which means we can start using the existing power of MSBuild to move to a more centrally managed list of packages. But it doesn't stop there, we can start making our MSBuild files a bit smarter, more convention-based. This is not new, Microsoft themselves are doing this today as they evolve the project template .csproj
files. This was highlighted recently and it demonstrates how minimal a .NET Core MSBuild project can be:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp1.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETCore.App" Version="1.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="1.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="1.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="1.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="1.0.0" />
</ItemGroup>
</Project>
You'll notice a few things - No need for a ToolsVersion
attribute, and no standard <Compile>
and <EmbeddedResource>
elements:
<Compile Include="**\*.cs" />
<EmbeddedResource Include="**\*.resx" />
These are in inferred by your use of an Sdk
attribute.
Making things smarter
Now, imagine we have a project solution similar to the following:
\Common.props
\src\MyPackage.Abstractions\MyPackage.Abstractions.csproj
\src\MyPackage.Host\MyPackage.Host.csproj
\src\MyPackage\MyPackage.csproj
\test\MyPackage.Tests\MyPackage.Tests.csproj
Each one of those MSBuild XML files would contain their own independent set of package references, and target frameworks monikers (TFMs), etc. There are a couple of goals I want to achieve:
- I want to ensure that my NuGet package references are version-aligned through all of my projects.
- I want to import a standard set of packages depending on the type of project (website, library, unit test, etc.)
So how do we go about this? Let's start simple, we'll take our Abstractions library:
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\Common.props" />
<PropertyGroup Label="Output">
<AssemblyName>MyPackage.Abstractions</AssemblyName>
<AssemblyTitle>MyPackage.Abstractions</AssemblyTitle>
<TargetFramework>netstandard1.6</TargetFramework>
</PropertyGroup>
</Project>
Pretty much nothing except a bare-bones file describing the output and an import (we'll come to the import later).
Now, let's move into out Implementations library:
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\Common.props" />
<PropertyGroup Label="Common">
<UsesEntityFramework>true</UsesEntityFramework>
</PropertyGroup>
<PropertyGroup Label="Output">
<AssemblyName>MyPackage</AssemblyName>
<AssemblyTitle>MyPackage</AssemblyTitle>
<TargetFramework>netstandard1.6</TargetFramework>
</PropertyGroup>
<ItemGroup Label="ProjectReferences">
<ProjectReference Include="..\..\src\MyPackage.Abstractions\MyPackage.Abstractions.csproj" />
</ItemGroup>
</Project>
Ok, a little more here - I've defined a custom <PropertyGroup>
, which I have labelled as Common
. The Label isn't required, I just prefer to put some commentary around certain elements. Within my custom PropertyGroup
, I've declared a property called UsesEntityFramework
. We'll use this property later in our Common.props
file.
Next up, add a Test project:
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\Common.props" />
<PropertyGroup Label="Common">
<HostType>UnitTest</HostType>
<DatabaseProvider>InMemory</DatabaseProvider>
</PropertyGroup>
<PropertyGroup Label="Package">
<AssemblyTitle>MyPackage.Tests</AssemblyTitle>
<AssemblyName>MyPackage.Tests</AssemblyName>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp1.0</TargetFramework>
</PropertyGroup>
<ItemGroup Label="ProjectReferences">
<ProjectReference Include="..\..\src\MyPackage\MyPackage.csproj" />
<ProjectReference Include="..\..\src\MyPackage.Abstractions\MyPackage.Abstractions.csproj" />
</ItemGroup>
</Project>
Again, very similar, except this time we're adding another custom property, HostType
with a value of UnitTest
, and a property DatabaseProvider
with a value of InMemory
. Additionally, we're now targeting netcoreapp1.1
instead of netstandard1.6
. Still, all very standard MSBuild. Now, I'm currently using the Preview5 CLI bits and I don't know if the Microsoft.NET.Test.Sdk
value will work, so I'll stick to the standard Microsoft.NET.Sdk
value, and we'll manually handle pulling in our SDK. You'll also notice I haven't added any package references to my unit test framework of choice (Xunit in my case).
Lastly, our Host project:
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\Common.props" />
<PropertyGroup Label="Common">
<HostType>Website</HostType>
<DatabaseProvider>SqlServer</DatabaseProvider>
</PropertyGroup>
<PropertyGroup Label="Package">
<AssemblyTitle>MyPackage.Host</AssemblyTitle>
<AssemblyName>MyPackage.Host</AssemblyName>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp1.0</TargetFramework>
</PropertyGroup>
<ItemGroup Label="ProjectReferences">
<ProjectReference Include="..\..\src\MyPackage\MyPackage.csproj" />
<ProjectReference Include="..\..\src\MyPackage.Abstractions\MyPackage.Abstractions.csproj" />
</ItemGroup>
</Project>
Much like the Test project, or Host project defines a custom property HostType
with a value of Website
, but this time, I've set my DatabaseProvider
property to be SqlServer
.
Conditional logic in Common.props
The glue that binds this all together is a shared MSBuild XML file, Common.props
which sits at the root. Through this file, which is imported into everything, we can apply custom package references and a bit more logic.
Version-aligning package references
My first goal, is that I wanted to version align my package references. This is to ensure my libraries are all built against the same version of a NuGet package. This is one of the design goals behind Paket. In our design, we're using MSBuild directly.
Using the Target Framework Moniker to import the .NET Standard or App library
<ItemGroup Condition="'$(TargetFramework)'=='netstandard1.6'">
<PackageReference Include="NETStandard.Library" Version="1.6.1" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)'=='netcoreapp1.1'">
<PackageReference Include="Microsoft.NETCore.App" Version="1.1.0" />
</ItemGroup>
We can take advantage of simple MSBuild conditionals to determine which version of our standard library we want to import. Where we are targeting netstandard1.6
, we'll import NETStandard.Library
v1.6.1. Where we are targeting netcoreapp1.1
, we'll import Microsoft.NETCore.App
v1.1.0.
We no longer have to explicitly reference them in the projects, we apply them by convention.
Next, we can use our custom HostType
property to determine which packages to import, for instance:
<ItemGroup Condition="'$(HostType)'=='Website'">
<PackageReference Include="Microsoft.NET.Sdk.Web" Version="1.0.0-alpha-20161104-2-112" />
<!-- Other ASP.NET Core Packages -->
</ItemGroup>
<ItemGroup Condition="'$(HostType)'=='UnitTest'">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0-preview-20161024-02" />
<PackageReference Include="xunit" Version="2.2.0-beta3-build3402" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0-beta4-build1194" />
</ItemGroup>
This is where it is starting to get a bit smarter. For our HostType
= Website
condition, we'll pull in the Web SDK, and at this point, you could specify ASP.NET Core package references, such as Kestrel.
For our HostType
= UnitTest
condition, we'll pull in the Test SDK and Xunit libraries required to build and run our tests.
Hopefully you can already see how we are using existing MSBuild functionality to ensure some consistency in our projects.
Importing a standard set of projects
Much like the use of our HostType
attribute, we also designed another property, DatabaseProvider
. Firstly, we defined a property group:
<PropertyGroup Condition="'$(DatabaseProvider)'!=''">
<UsesEntityFramework>true</UsesEntityFramework>
</PropertyGroup>
You don't have to do this, as the build and project systems support transitive dependencies. That being sub-dependencies of your project's direct dependencies. But it's good to demonstrate what you can do.
And lastly, a final set of package references:
<ItemGroup Condition="'$(UsesEntityFramework)'=='true'">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="1.1.0" />
</ItemGroup>
<ItemGroup Condition="'$(DatabaseProvider)'=='SqlServer'">
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="1.1.0" />
</ItemGroup>
<ItemGroup Condition="'$(DatabaseProvider)'=='InMemory'">
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="1.1.0" />
</ItemGroup>
So quite simply, if we are targeting Sql Server, we'll pull in the Microsoft.EntityFrameworkCore.SqlServer
package, and if we target the In Memory database for testing, we'll pull in Microsoft.EntityFrameworkCore.InMemory
.
Conclusions
As we can see, going back to the MSBuild XML files actually gives us a lot more power than what was possible with project.json
. Ignoring the JSON vs XML argument, MSBuild itself is stable and mature and already has a great eco-system of tools from IDEs to build servers. It makes sense to take advantage of that.
Personally, I've never really spent a great deal of time with MSBuild because using Visual Studio, that is a hidden power. Visual Studio takes care of the file for you but now in the .NET Core world, MSBuild is becoming an ever more important aspect of the toolchain, so I am appreciating what it can do a lot more.
Example project files are on a gist:
https://gist.github.com/Antaris/b7d86d3485606e9f1b9fc8698092d56d