Here are the steps to make a regular .NET project become a multi framework NuGet package. I have also included the steps to build it with TFS 2017.3. This article came to life after I have come across a problem that took me some times to debug and finally figure it out. I needed that an existing NuGet package have more than one .NET framework to be available. The solution is so neat that I needed to share it.
Changes to the project file
The initial project file
The project I am working with is a regular C# project. Here is the content:
<?xml version="1.0" encoding="utf-8"?> <Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="..\packages\xunit.runner.visualstudio.2.3.1\build\net20\xunit.runner.visualstudio.props" Condition="Exists('..\packages\xunit.runner.visualstudio.2.3.1\build\net20\xunit.runner.visualstudio.props')" /> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> <ProjectGuid>{D645388E-F937-4310-9D13-F2107612C36C}</ProjectGuid> <OutputType>Library</OutputType> <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>BuildLogParsers.UnitTests</RootNamespace> <AssemblyName>BuildLogParsers.UnitTests</AssemblyName> <TargetFrameworkVersion>v4.5.1</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> <SccProjectName>SAK</SccProjectName> <SccLocalPath>SAK</SccLocalPath> <SccAuxPath>SAK</SccAuxPath> <SccProvider>SAK</SccProvider> <NuGetPackageImportStamp> </NuGetPackageImportStamp> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> <DebugType>full</DebugType> <Optimize>false</Optimize> <OutputPath>bin\Debug\</OutputPath> <DefineConstants>DEBUG;TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <DebugType>pdbonly</DebugType> <Optimize>true</Optimize> <OutputPath>bin\Release\</OutputPath> <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> </PropertyGroup> <ItemGroup> <Reference Include="Castle.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=407dd0808d44fbdc, processorArchitecture=MSIL"> <HintPath>..\packages\Castle.Core.4.0.0\lib\net45\Castle.Core.dll</HintPath> </Reference> <Reference Include="Moq, Version=4.7.10.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL"> <HintPath>..\packages\Moq.4.7.10\lib\net45\Moq.dll</HintPath> </Reference> <Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> <HintPath>..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll</HintPath> </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> <Reference Include="System.Xml.Linq" /> <Reference Include="System.Data.DataSetExtensions" /> <Reference Include="Microsoft.CSharp" /> <Reference Include="System.Data" /> <Reference Include="System.Net.Http" /> <Reference Include="System.Xml" /> <Reference Include="xunit.abstractions, Version=2.0.0.0, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL"> <HintPath>..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll</HintPath> </Reference> <Reference Include="xunit.assert, Version=2.1.0.3179, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL"> <HintPath>..\packages\xunit.assert.2.1.0\lib\dotnet\xunit.assert.dll</HintPath> </Reference> <Reference Include="xunit.core, Version=2.1.0.3179, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL"> <HintPath>..\packages\xunit.extensibility.core.2.1.0\lib\dotnet\xunit.core.dll</HintPath> </Reference> <Reference Include="xunit.execution.desktop, Version=2.1.0.3179, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL"> <HintPath>..\packages\xunit.extensibility.execution.2.1.0\lib\net45\xunit.execution.desktop.dll</HintPath> </Reference> </ItemGroup> <ItemGroup> <Compile Include="Issues\IssuesTypeManagerTests.cs" /> <Compile Include="Loader\DataTextLoaderTests.cs" /> <Compile Include="Loader\IssuesJsonLoaderTests.cs" /> <Compile Include="Parsers\BuildLogParserTests.cs" /> <Compile Include="Parsers\SubProcesses\AbstractSubProcessLogParserTests.cs" /> <Compile Include="Parsers\SubProcesses\GenericLogParserTests.cs" /> <Compile Include="TestsFoundations\Helpers\AbstractSubProcessLogParserTestsHelper.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Streams\StreamsManagerTests.cs" /> <Compile Include="TestsFoundations\UnitTestsFoundation.cs" /> </ItemGroup> <ItemGroup> <None Include="App.config" /> <None Include="packages.config" /> <None Include="Settings.StyleCop" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\BuildLogParsers\BuildLogParsers.csproj"> <Project>{ff436071-73cc-4141-903c-c0b418120f49}</Project> <Name>BuildLogParsers</Name> </ProjectReference> </ItemGroup> <ItemGroup> <Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" /> </ItemGroup> <ItemGroup /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild"> <PropertyGroup> <ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText> </PropertyGroup> </Target> </Project>
I would like to point you out a couple of concepts in this project file:
- All files to be included in the project are declared. It can become a source of problem when merging branches with big project files.
- All default information is listed for each configuration.
- The old format of NuGet is used (with the package folder).
On the way…
First, let me tell you a couple of ways I have tried and did not work. I tried to set one debug configuration and one release configuration per framework I wanted to build. Something like that:
- Debug-Net451
- Release-Net451
- Debug-Net452
- Release-Net452
- …
The problem is that the NuGet restore command will failed because NuGet does not know how to deal with multiple build configuration like that. Also, the build in TFS was getting complicated to setup with all the build configurations for each platform…
The Solution to Multi Framework NuGet package!
So… the solution is to take advantage of the new format for dot net core!
Unload the project, edit the csproj file and replace the content with the new dot net core project format:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFrameworks>net451;net452</TargetFrameworks> </PropertyGroup> </Project>
Note the s in TargetFrameworks tag. This is the way you can declare the number of frameworks you need to build against.
If you reload your project, you will see, Visual Studio will reload all the files correctly. In fact, it takes the reverse approach of including all the files and will add the files you want to exclude in the csproj when you specify them. Better than that, once your project will be reloaded, you can now edit the csproj directly without having to unload/reload the project. Reload your project, and have a look to the new option on the right click on the project: Edit YourProject.csproj.
Missing Packages
It’s possible that if you were using some packages for your project, by replacing the csproj by the code above, you flushed them out. You must now add them back and you can add conditions depending on the framework targeted. For example, in one of the unit tests projects, I added those lines with the conditions attributes:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFrameworks>net451;net452</TargetFrameworks> </PropertyGroup> <ItemGroup> <PackageReference Include="Moq" Version="4.8.1" /> <PackageReference Include="xUnit" Version="2.3.1" Condition="'$(TargetFramework)' == 'net452'" /> <!-- xunit 2.2.0 is not compatible with .net 4.5.1.--> <PackageReference Include="xUnit" Version="2.1.0" Condition="'$(TargetFramework)' == 'net451'" /> <PackageReference Include="xUnit.runner.visualstudio" Version="2.3.1" /> </ItemGroup> </Project>
As you can see, I have specified a condition for the xunit framework because the version 2.2.0 is not compatible anymore with .Net 4.5.1.
Duplicate information
If you try to build, you will probably run into compile errors specifying duplicate information in the AssemblyInfo.cs. With the new csproj format, the compiler is now generating all those information for us. Copy your information if you have some information customized to your csproj file:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFrameworks>net451;net452</TargetFrameworks> </PropertyGroup> <PropertyGroup> <Version>1.0.0</Version> <FileVersion>1.0.0</FileVersion> <Authors>The Futue of Code</Authors> <Description>Library used for demo purposes.</Description> <Copyright>Copyright (c) 2018 The Future Of Code</Copyright> </PropertyGroup> </Project>
The NuSpec information
When creating a NuGet package, you must usually specify the information inside a nuspec file. This file is now embedded into your new csproj as well (pretty interesting no?). You can enter the information from your nuspec file by specifying a property group just like this (add all the information you need):
<PropertyGroup> <PackageId>TheFutureOfCode.DevOps.LogParser</PackageId> <PackageVersion>1.0.0</PackageVersion> <PackagerequireLicenseAcceptance>false</PackagerequireLicenseAcceptance> <PackageReleaseNotes>This is my release notes.</PackageReleaseNotes> </PropertyGroup>
You should now be able to build your project. If you have any missing references, add them, but there is nothing else special to know. What is pretty cool about this approach when you build your project is that it just restore and build all the platforms needed in the bin/Debug or bin/Release folder.
MSBuild 15 to restore and pack
NuGet.exe does not know how to deal with this format. So, you must use msbuild 15 that comes with VS2017 to handle that. Open a developer command prompt for VS2017 and type:
msbuild YourProject.csproj /t:restore
I suggest you to have a look at this link for more information.
So, yes, we get rid of the nuspec file, and the AssemblyInfo.cs. Pretty cool no?
How to build the multi platform NuGet package into TFS 2017.3
Let see what it looks like to build such a project in TFS 2017.3. Here are the basic steps to make the restoration works properly:
The importants points are:
- MSBuild version 15 (2017) must be selected, otherwise, it won’t work.
- You can ask MSBuild to only restore, this is the important part. You can do the rest of your process the way you want.
- I enable the regular build, there is no need to ask MSBuild to do it, but I could have done it via
msbuild /t:build
. - I am running my tests using the regular way with the VSTests task. It will run all tests for each framework.
Finally the pack command must use the one from msbuild 15 also (msbuild /t:pack
):
Run your build and you should get a nice package with all the frameworks binaries you have specified.
References
Some of the good references I found that help me to figure out how to put all of that together:
- https://www.bartwolff.com/Blog/2017/03/20/targeting-multiple-net-platforms-in-a-single-nuget-package-w
- https://blog.nuget.org/20170316/NuGet-now-fully-integrated-into-MSBuild.html
- https://docs.microsoft.com/en-us/nuget/consume-packages/package-restore
- https://docs.microsoft.com/en-us/nuget/reference/msbuild-targets
Martin
Martin is a Development Director working for Triton Digital, sub company of iHeart Media, based in Montreal, Canada. He was a developer for more than 10 years, a Technical Lead for 2 years, a Product Owner for 3 years, and he is also the founder of The Future Of Code. He has more than 15 years of experience in CI/CD practices, design, development, testing practices, UI, teams efficiency, value stream management, DevOps mindset, and much more. He is passionate about DevOps culture, mentoring and deployment automation.
1 Comment
Add comment Cancel reply
This site uses Akismet to reduce spam. Learn how your comment data is processed.
with TFS 2017.1 would I need a new build definition for each framework until our new tfs server is built? or can I choose build through visual studio 2017?