Migrating to SDK Project Format
If you are not aware yet Visual Studio 2017 shipped with support for a newer project format (informally known as the SDK project format). The newer format came about for a variety of reasons including:
- The traditional project format is very verbose.
- The traditional project format is hard to read and edit.
- The traditional project format requires the file to be modified for every change made to the project.
- The .NET Core
project.json
format did not easily map to the traditional project format.
While the focus on the new format has been to support .NET Core applications it can be used with many other project types as well, with restrictions discussed later. In this article I will discuss how to migrate a traditional project file to the newer SDK format. There are numerous other blogs on this same topic if you want to get different viewpoints.
Why Would You Bother
The first question you should always ask yourself before making massive changes to a project is whether the benefits outweigh the costs. In the case of the SDK format I believe they do. The new format has many benefits.
- The project file is generally only a handful of lines irrelevant of how many files are actually in the project.
- The project file does not need to be updated very often even when manipulating files in the project. This is very important when using source control systems.
- The project file consolidates settings that were previously in other files such as package references.
- The project file is less reliant on magic values making it easier to copy the project file and manipulate it outside of the tooling without breaking anything.
- The project file supports multi-targeting.
Reasonable Defaults
One of the biggest changes around the format is most of the settings you’d typically have in the file are now the defaults. For example in a Debug
configuration you would expect optimizations to be turned off. Here’s what it looks like in a traditional project file.
<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>
There is no entries in the SDK format. When using the Debug
configuration, optimizations are disabled by default (and the other options default as well). You can, of course, turn them on if you wanted but it is an opt-in. This has the net effect of dramatically removing the boilerplate PropertyGroup
elements that every project file has.
File Globbing
Another change is how files are included in the project. In a traditional project file each file is added to an ItemGroup
. For example you might have this in your project file.
<ItemGroup>
<Compile Include="ExceptionHandling\ErrorResponse.cs" />
<Compile Include="Headers\HeaderLinkUrl.cs" />
</ItemGroup>
If you have a lot of files in your project then this list will get long. In source control systems the project file tends to be the place where merge conflicts occur as multiple team members add and remove (different) files from the project. The project file is being updated.
In the new SDK format file globbing is used to automatically include the standard file types into the project. You do not need to list them explicitly. Instead you will only list the files that are exceptions (e.g. files to not include or files that should be treated as content). This will be probably the most dramatic drop in lines in the project file for anything beyond the smallest projects. References to projects, NuGet packages and assemblies are similarly reduced to either a single line or removed altogether as discussed later.
There is a negative side to this as well though. All files are included by default. If you are in the habit of having files in your project that you don’t want to compile then you have to explicitly opt them out. In my experience this isn’t a big deal but be warned that after switching to this format suddenly your project may have new files being compiled.
Things to Know Before Migrating
Clearly the SDK format is the way to go and you can begin converting your existing projects over now. Not all projects can be converted yet so you should follow some guidelines before converting a project. You can convert either some or all the projects in a solution as needed.
Note: There has been some suggestions online that solutions containing mixed traditional and SDK projects don’t work correctly. I have migrated many solutions and most of them have mixed formats and I’ve yet to run into any issues.
Project Types
Since the new format was originally designed for .NET Core it has limited support for project types that don’t fall into that category. At this time the following project types are supported (either .NET Core or .NET Framework is fine).
- Class Library
- Console Application
Specifically do not try to use the new format with existing ASP.NET applications, WCF web services, Windows Forms applications or WPF applications. While the project file itself may build you will run into tooling issues.
Package References
The SDK format only supports package references for NuGet packages. You cannot use the old packages.config
format. Because of this you won’t want to convert some projects that rely on featuers not supported by package references. Packages that use config transforms is a common problem.
Refer to my previous post on converting to the new format if needed.
T4 Files
If you use T4 (.tt
) files then be aware they may not behave correctly. In general you will not get the option to transform the files like you did in a traditional project. The option is simply not there. You can still use the solution-level option to transform all the T4 files and that will generally work.
For whatever reason the DTE objects and other interfaces that have historically supported T4 and extensions is not available in the SDK format. Any templates that rely on these will most likely fail because of missing services.
Nested Files
Nested files are not supported. Technically the format does support the elements and you can manually edit your file to use them but they will be ignored. Microsoft has made it clear that beyond web applications, nested files aren’t a high priority. In VS 2017 Microsoft is moving nested file support out of the project file and to a solution level setting. The new approach is to auto-magically set up nesting support based upon the project type. This seems, to me, like a step backwards. Other than for web applications the effect is that it is becoming harder to nest files easily without setting some global level option that may not even be applicable to all projects in a solution. For example you may have a web and WCF project in your solution. They would use different nesting formats. Even worse is if you have a web class library that VS doesn’t think is a web project.
For now, if you use nested files and really can’t lose those options then don’t migrate the project. When Windows Forms and WPF projects are supported in .NET Core I suspect Microsoft will have to make adjustments to their nesting to support them. At that time it may make sense to re-evaluate this.
Migrating an existing Project File
Assuming you have verified the project is a good candidate for the SDK format you are now ready to do the migration. But you should first ask the question of whether you’ll create a new project or update the existing one. The more detailed documentation is available here.
Here’s the steps, roughly.
- Clean any temporary and output directories.
- Unload the project (if using Visual Studio).
- Edit the project file.
- Specify the SDK information.
- Specify the target information.
- Clean up the property groups.
- Clean up the item groups.
- Clean up the files.
- Clean up the references.
- Clean up the project references.
- Reload.
- Add missing settings and verify other settings.
- Fix version information.
- Compile and run unit tests.
- Add SourceLink support for packages.
- Update packaging information.
New vs Upgrading Existing Project
I have heard the recommendation is to create a new project but I cannot fathom why you would do this. If you were migrating to .NET Standard or completely redoing your project then a new project makes sense but simply changing the format does not warrant a new project. There are too many issues with this.
The first issue is with the project file itself. Since you cannot have multiple files with the same name you’ll either need to create a new project file in the same folder as the existing project or create a whole new folder structure. If you create a new project file then it’s name won’t match the folder or project which introduces problems down the road (especially with the default options in the SDK format). Additionally you’ll have a project file you’ll never be using again. So why bother keeping it around.
If you create a new project folder then you’ll have to copy all the source files across. Again though you’ll want to keep the project and folder names in sync which means you’ll have to rename them which makes them inconsistent. If you’re making this change you surely already have a copy of the project in source control. There really is no reason to not update the existing project file and keep the names the same. Nobody other than you and the compiler are going to know anyway.
Note: For this discussion we will take an existing project file and gut it. You could create a temporary project file, add the appropriate options and then remove the old project if you’d like.
Project Declaration
One of the big advantages of the SDK format is that VS can edit the project file without unloading it. But since you’re starting out you’ll have to unload the project and then edit it in Visual Studio. You could use a different editor if you like but you’ll lose Intellisense.
To start the process remove the XML declaration at the top of the file. While this is an XML file you don’t need the declaration anymore.
Note: When removing lines we will simply comment them out in the code blocks so you can see what was removed. You should actually remove them from your project file instead.
Next the Project
element reduces down to a single attribute Sdk that specifies the SDK to use. Currently there are two. For non-web projects you will use Microsoft.NET.Sdk
. For ASP.NET Core applications you’ll use Microsoft.NET.Sdk.Web
.
<!--<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="15.0">-->
<Project Sdk="Microsoft.NET.Sdk">
Note: As of this writing, the value is case sensitive. Microsoft.NET.Sdk
will work but Microsoft.Net.Sdk
will not.
Cleanup
The required information is now in the project file. The rest of the effort is mostly removing lines.
To start with you should review each PropertyGroup
element. There are generally three: root (no condition), debug and release. Each of these contains the default and non-default settings for the project. You can remove any of the settings that are at their default values. Unfortunately there is no easy way to see this so here’s some guidelines.
- If this is a debug configuration then optimizations are turned off, the appropriate debug settings are configured and
TRACE
andDEBUG
are defined. - If this is a release configuration then optimizations are turned on, PDB files are generated and
TRACE
is define. - Output type is
Library
. - The output path is set to
bin\$(Configuration)
. - The root namespace matches the project file name.
- The assembly name matches the project file name.
These are logical defaults. But there are many other settings that most people never really change anyway and can be ignored.
AllowUnsafeCodeBlocks
ApplicationRevision
andApplicationVersion
BaseAddress
andFileAlignment
- Anything starting with
Bootstrapper
orIsWebBootstrapper
- Most
CodeAnalysis
settings unless you are using Code Analysis Install
andPublishUrl
MapFileExtensions
ProjectGuid
,ProjectType
andProductVersion
RestorePackages
Update
andUpgrade
UseApplicationTrust
Warning
levels
Here’s an example of what a typical project may look like.
<PropertyGroup>
<ProjectType>Local</ProjectType>
<ProductVersion>9.0.30729</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{A49F5168-AB7C-472F-B7B5-7AB3DDB37372}</ProjectGuid>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<AssemblyName>P3Net.Kraken.Data</AssemblyName>
<OutputType>Library</OutputType>
<RootNamespace>P3Net.Kraken.Data</RootNamespace>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\..\Source\</SolutionDir>
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<OutputPath>..\bin\Debug\</OutputPath>
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
<BaseAddress>285212672</BaseAddress>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DocumentationFile>..\bin\Debug\P3Net.Kraken.Data.xml</DocumentationFile>
<DebugSymbols>true</DebugSymbols>
<FileAlignment>4096</FileAlignment>
<Optimize>false</Optimize>
<WarningLevel>4</WarningLevel>
<DebugType>full</DebugType>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<OutputPath>..\bin\Release\</OutputPath>
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
<BaseAddress>285212672</BaseAddress>
<DefineConstants>TRACE</DefineConstants>
<DocumentationFile>..\bin\Release\P3Net.Kraken.Data.xml</DocumentationFile>
<DebugSymbols>true</DebugSymbols>
<FileAlignment>4096</FileAlignment>
<Optimize>true</Optimize>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<WarningLevel>4</WarningLevel>
<DebugType>pdbonly</DebugType>
</PropertyGroup>
Remove them all. You still need one root PropertyGroup
element. The only element you need is the target framework you are using. Refer to the TargetFrameworkVersion
element already in the file. This is the framework the project is targeting. This changes to simply TargetFramework
. However the value must match a framework moniker defined here. For a .NET Framework 4.7.1 project it would be net471
.
<PropertyGroup>
<TargetFramework>net471</TargetFramework>
</PropertyGroup>
What to Put Back
You now need to put into the root PropertyGroup
any settings that weren’t at their default. In most cases you just put them in the root element. If you really need to set things per-configuration then preferably use the $(Configuration)
variable instead. For example the output path can be set to ..\bin\$(Configuration)
. Here are some common things you’ll want to set.
Project Type
If the project is not a class library then you’ll need to set OutputType
to the appropriate value.
Documentation
If your project needs to generate documentation (the XML files) then set the GenerateDocumentationFile
element to true
.
Assembly and Namespace Names
By default the namespace and assembly name follow the project file name. If you need to change this then add the RootNamespace
and AssemblyName
elements back in.
Console Applications
For console applications be aware that the default platform is x86
. If you want to run on any CPU you need to specify it for all build configurations explicitly. This is a known issue.
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<PlatformTarget>AnyCPU</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<PlatformTarget>AnyCPU</PlatformTarget>
</PropertyGroup>
Unit Tests
If this is a unit test project then add the following attribute so binding redirects work properly.
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
Here’s the final version of the project groups.
<PropertyGroup>
<TargetFramework>net47</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
Files
Now comes the part where you remove all the files. As mentioned earlier the default SDK files will use file globbing to correctly include most files including source files, resources, HTML files, etc. So you don’t need any of these. The general rule of thumb is to go through your ItemGroup
elements and remove any that have a value of Compile
or Content
. If there are any that were marked with None
then they were excluded in the original project and probably need to be excluded now.
Once you reload the project you can confirm if any additional files need to be excluded or not.
<!--<Compile Include="Common\ConnectionData.cs" />
<Compile Include="Common\DataCommand.cs" />
<Compile Include="Common\DataCommandExtensions.cs" />
<Compile Include="Common\DataParameterT.cs" />
<Compile Include="Common\DataParameter.cs" />
<Compile Include="Common\DataParameterCollection.cs" />
<Compile Include="Common\InputOutputParameterT.cs" />
<Compile Include="Common\InputOutputParameter.cs" />
<Compile Include="Common\InputParameterT.cs" />
<Compile Include="Common\InputParameter.cs" />
<Compile Include="Common\OutputParameterT.cs" />
<Compile Include="Common\OutputParameter.cs" />
<Compile Include="Common\SchemaInformation.cs" />
<Compile Include="DataSetExtensions.cs" />
<Compile Include="DataReaderExtensions.cs" />
<Compile Include="Common\DataTransaction.cs" />
<Compile Include="Common\DbProviderFactoryConnectionManager.cs" />
<Compile Include="Common\ConnectionManager.cs" />
<Compile Include="GlobalSuppressions.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
-->
Package files
If you have already switched the project file over to use PackageReference notation then you’re done here. No additional changes need to be made. Otherwise switch over to that format now.
You cannot use the packages.config
file with the new format so you can remove that file from the project file’s ItemGroup
and you can delete the file from disk. If you had previously added a RestoreProjectStyle
element to the PropertyGroup
to help Visual Studio know which format to use then you can remove that as well. It is not needed anymore.
Runtime Dependencies
Runtime dependencies are identified by an ItemGroup
with Reference
elements in them. Each element is an assembly being referenced. The default SDK includes the core assemblies from the framework so you can remove them. You only need to leave the ones that are not standard and any custom ones you have. I personally recommend you note which ones you are referencing and then remove them all. Once you reload the project look at which assemblies are being referenced (or simply compile) and then add the assemblies back in that are missing. A couple common assemblies are missing from the default list, just so you know.
System.ComponentModel.DataAnnotations
System.Configuration
One issue that gets introduced by switching to this format is that assembly redirects tend to not be evaluated properly. More specifically NuGet packages don’t know to update the binding so you get runtime errors. To help alleviate this you should add the AutoGenerateBindingRedirects
element into any runnable projects (e.g. unit tests, console applications, WCF services) so that the bindings are auto-generated into the configuration as needed.
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
For project references you will leave them in too. But with the change to the new format project files are no longer identified by their unique GUID. Instead the relative path is used. So project references are reduced to a single Include
attribute.
<ItemGroup>
<!--<ProjectReference Include="..\P3Net.Kraken\P3Net.Kraken.csproj">
<Project>{BF45A218-FDD0-400B-AFBC-F043C24CEE0B}</Project>
<Name>P3Net.Kraken</Name>
</ProjectReference>
-->
<ProjectReference Include="..\P3Net.Kraken\P3Net.Kraken.csproj" />
</ItemGroup>
Imports
The last set of changes are around the Import
elements. In most cases a project file will have a few imports for MSBuild targets. These can be removed completely. Some imports may come from NuGet packages. These should remain although the paths are likely wrong now because of package references. Additionally for unit tests there are generally some Visual Studio-specific targets. My personal recommendation is to remove the package, unit test targets and references. Then add them back via NuGet after the project loads. This will fix up the paths.
<!--<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />-->
Here’s the final version of the project file for a class library. You should be able to reload the project, fix up any references mentioned earlier and rebuild.
<Project Sdk="Microsoft.Net.Sdk">
<PropertyGroup>
<TargetFramework>net47</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\P3Net.Kraken\P3Net.Kraken.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.DotNet.Analyzers.Compatibility" Version="0.1.3-alpha" />
</ItemGroup>
</Project>
Fixing Warnings
NuGet has switched to semantic versioning and so your packages should do the same. But right now you’ll get a warning because semantic versioning does not work with older clients. To disable this warning add the NU5105
value to the NoWarn
element.
<NoWarn>$(NoWarn);NU5105</NoWarn>
<MinClientVersion>4.3</MinClientVersion>
The MinClientVersion
element prevents the package from working with older clients that may not support it yet.
Versioning
If you recompile at this point you are probably going to get errors about duplicate attributes. This is because of versioning changes. With the SDK format the build will now auto-generate the core assembly attributes for versioning (in the intermediate directory). In older project formats you had an AssemblyInfo.cs
file for this.
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("P3Net.Kraken.Data")]
[assembly: AssemblyDescription("Kraken library for data access.")]
[assembly: AssemblyCulture("")]
[assembly: CLSCompliant(true)]
[assembly: ComVisible(false)]
#if DEBUG
[assembly: AssemblyConfiguration("Debug")]
#endif
[assembly: AssemblyCompany("P3Net")]
[assembly: AssemblyProduct("Kraken")]
[assembly: AssemblyCopyright("Copyright © Michael Taylor")]
[assembly: AssemblyTrademark()]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0.0"))]
This file is pretty much obsolete now and can be removed from the project. However before you delete it you’ll want to capture some information. The version information, company, product and other core attributes are now part of the project file itself (inside the root PropertyGroup
).
<Title>P3Net.Kraken.Data</Title>
<Description>Kraken library for data access.</Description>
<Product>Kraken</Product>
<Company>P3Net</Company>
<Copyright>Copyright © Michael Taylor</Copyright>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<InformationalVersion>1.0.0.0</InformationalVersion>
Eliminating these leaves only a couple of attributes left.
AssemblyTrademark
does not currently have an equivalent.AssemblyConfiguration
maps toConfiguration
but is generally just the build configuration.CLSCompliant
is still needed if that is important for your project.ComVisible
along with anyGuid
attribute for the type library is still needed. .NET Core doesn’t, at this time, need COM so leaving them off won’t change anything. I tested and if you don’t specify the attributes then the assembly isn’t COM-visible automatically so you only need to include them if true.InternalsVisibleTo
are still needed, if used.
In most projects the need for the AssemblyInfo.cs
file goes away so it can simply be deleted. Unless you have other files in the Properties
folder, the entire folder can be removed. If you need some of the attributes either remove the duplicate attributes that are there or create a new file with just the attributes you need to avoid confusion.
In some cases you may want to keep using your AssemblyInfo
file, perhaps because of automated build versioning. You can set the GenerateAssemblyInfo
element to false
. This disables the auto-generation for now.
Note: If you want to share versioning information (or just about any other project setting) across projects in a solution refer to directory.build.props on how to do this easily. I will blog about it in the future.
Now would be a good time to compile and re-run your tests to make sure everything is working correctly.
Creating Packages
Another feature that was merged into the project file is the generation of packages. Most libraries that needed to generate packages included a nuspec
file. That isn’t necessary anymore in many situations. Instead the package generation can be retrieved directly from the project settings. This has some plusses and minuses and is still a very much in progress implementation.
Converting to Project Packaging
Here’s an example nuspec
file.
<?xml version="1.0"?>
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
<metadata>
<id>P3Net.Kraken</id>
<version>6.0.0</version>
<title>Kraken Core Library</title>
<authors>Michael Taylor</authors>
<owners>P3Net</owners>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>Provides support for writing .NET applications.</description>
<releaseNotes>Supported Platforms:
.NET 4.6.1
</releaseNotes>
<copyright>Copyright 2016</copyright>
<tags>Kraken P3Net</tags>
<frameworkAssemblies>
<frameworkAssembly assemblyName="System.ComponentModel.DataAnnotations" />
<frameworkAssembly assemblyName="System.Configuration" />
<frameworkAssembly assemblyName="System.Core" />
</frameworkAssemblies>
</metadata>
<files>
<file src="P3Net.Kraken.dll" target="lib\net461" />
<file src="P3Net.Kraken.pdb" target="lib\net461" />
<file src="P3Net.Kraken.xml" target="lib\net461" />
<file src="P3Net.Kraken.Data.dll" target="lib\net461" />
<file src="P3Net.Kraken.Data.pdb" target="lib\net461" />
<file src="P3Net.Kraken.Data.xml" target="lib\net461" />
</files>
</package>
Open the nuspec
file associated with the project. You will have to add the appropriate package attribute to the project file but the mapping is not one to one. Here’s the common ones.
Nuspec | Property | Comments |
---|---|---|
id | PackageId | Defaults to project name |
title | Title | Defaults to assembly name |
version | PackageVersion | Defaults to Version |
authors | Authors | |
owners | Currently not configurable, defaults to Authors |
|
description | Description | |
releaseNotes | PackageReleaseNotes | |
copyright | Copyright | |
tags | PackageTags | Format has changed, use semicolons between tags instead of spaces |
PackageProjectUrl | URL to source code in GitHub or elsewhere | |
RepositoryUrl | URL to source code in GitHub or elsewhere |
Notice several of these come from the corresponding assembly attributes. Not all nuspec
attributes are currently supported. In the case of tags
notice the delimiter changes from spaces to semicolons. Here’s how it would look in the project file (again, in the root PropertyGroup
).
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<Version>6.0.0</Version>
<Company>P3Net</Company>
<Authors>P3Net</Authors>
<Copyright>Copyright © Michael Taylor</Copyright>
<Description>Core assembly for the Kraken Class Library.</Description>
<PackageProjectUrl>https://github.com/CoolDadTx/kraken</PackageProjectUrl>
<RepositoryUrl>https://github.com/CoolDadTx/kraken</RepositoryUrl>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageReleaseNotes>Supported Platforms:
.NET 4.7.2</PackageReleaseNotes>
<PackageTags>kraken;p3net</PackageTags>
<Title>Kraken Core</Title>
<IncludeSymbols>true</IncludeSymbols>
</PropertyGroup>
The Version
attribute is the package version and should follow semantic versioning. The PackageId
attribute will be the unique ID of the package and wiil default to the project name. Ensure this value matches the nuspec
file otherwise NuGet will see it as a brand new package. The Title
attribute defaults to the assembly name and is what a developer will see in the NuGet package list. Microsoft has been naming most of their packages by the assembly name but you can use anything you want. I prefer to title them by library and then subset.
The Description
attribute is particularly annoying. NuGet packages tend to provide useful information about the package but this attribute is also the assembly description. If you put a useful description here then it will be the assembly AND package description.
The PackageProjectUrl
and RepositoryUrl
elements are useful for providing links to the source code itself. You can also do this for PackageReleaseNotes
. This makes it easier for users to find up to date information about the package.
Generating the Package
The GeneratePackageOnBuild
is the core element. If true
then a package is generated at build time. You’ll want to build and then verify your generated package is correct.
The packager will set up the package to include the build output by default. It will add dependencies to any Reference
assembly references and any PackageReference
elements. Content files should be included as content. There are some limitations with this approach right now.
Note: Pay careful attention when looking at the generated package. Ensure the metadata lines up correctly including version and ID. Make sure all the appropriate files are present and that any dependencies are correct.
Symbols
By default symbol files (PDB) won’t be copied to the package. This is a known issue. Documentation files may not be copied either. Set the IncludeSymbols
element to true
to enable this. However it may still not work so refer to SourceLink later.
Package References
If the project is using package references then they are automatically included as dependencies. Currently the dependency is an absolute. If you want to support version ranges then the current syntax does not work. Additionally it does not distinguish between development and runtime dependencies. If your project relies on analyzers or other development-only packages then be sure to use the PrivateAssets
element on the package reference.
<PackageReference Include="MyAnalyzer">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Project References
It is common to have multiple projects generate outputs to go into a single package. This currently is not supported by the project package syntax. Project references are converted to regular package references. Of course if there is no package then it will fail at installation.
Currently the only workaround that doesn’t require a hack is to simply separate each package into a single assembly. If you cannot do this then you may want to stick with nuspec
files for now.
Another issue with this approach is that you have no control over the versioning. Every project reference is added as a dependency against the latest version. If you do separate your packages by project and you want to be able to release, say, an update to a single package then the generated package will have a dependency on all the other projects using the current version number. This is going to cause issue if you don’t deploy them all at once.
Multi Targeting
One of the best features of the new format is multi-targeting. This allows a single project to target multiple versions of the framework (e.g. .NET Framework 4.7.2 and .NET Standard 2.0). At build time the compiler will build both versions of the framework. In later updates to Visual Studio 2017 the editor can toggle between frameworks using the navigation bar.
Note: Multi-targeting has very limited support in Visual Studio 2017. Once you start using it you will find yourself having to use the text editor more often for making project changes.
To enable multi target support for a project you need to change the TargetFramework
element. Specifically you make it plural and then add additional framework monikers separated by semicolons.
<!--<TargetFramework>net472</TargetFramework>-->
<TargetFrameworks>net472;netstandard20</TargetFrameworks>
Save and reload and you’re done.
Note: In my experience Visual Studio crashes or generates errors when switching a project to the new format and reloading it. You may need to restart Visual Studio.
Once you multi-target the biggest difference you’ll see is under Dependencies in Solution Explorer. You will now have a separate node for each framework. That is because you will often find yourself using different dependencies depending upon framework. For .NET Framework you’ll rely mostly on the framework assemblies but .NET Standard ships most of those assemblies via NuGet. In the project file you can specify dependencies by framework using the $(TargetFramework)
condition.
<ItemGroup Condition="'$(TargetFramework)'=='net472'">
<Reference Include="System.ComponentModel.DataAnnotations" />
<Reference Include="System.configuration" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0'">
<PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" />
</ItemGroup>
One of the impacts of this is that you’ll now also have multiple outputs (sent to target-specific directories) and, if packaging, multiple binaries in the package.
.NET Standard vs Multi Targeting
As an aside, the whole purpose of .NET Standard is to allow a single assembly to be used on different frameworks. This reduces the binaries floating around, the code that has to be maintained, etc. In a perfect world you’d just use .NET Standard but in my experience this isn’t the correct approach if you have existing .NET Framework code to support. The problem isn’t that .NET Standard doesn’t work, it’s that it generates too much interop. If you simply retarget your library to .NET Standard then, assuming compatibility, you can use it on .NET Core or .NET Framework code. But the bulk of the (framework) dependencies will now come from NuGet.
.NET Framework Projects
This isn’t actually a problem as on the .NET Framework the assemblies should be referenced from the GAC but there is an issue with pre-4.7.1 runtimes that cause them to be copied anyway. This means you’re shipping more binaries than before. And since you have little control over the runtime framework that may be installed you’ll likely ship the assemblies even for .NET 4.7.1. This includes assemblies that you didn’t even have before. Even worse some packages (System.ComponentModel.Annotations
I’m looking at you) don’t work without binding redirects. It causes havoc. So for the foreseeable future I’d recommend multi targeting. You don’t have to maintain the older code but by shipping it you can allow .NET Framework applications to continue to work.
Unit Tests
Another limitation of multi targeting right now is that unit tests won’t work. Depending upon which approach you take to running unit tests determines the tests that will run. For Visual Studio it’ll run the first framework under TargetFrameworks
. The other will be ignored.
Tooling Support
As mentioned throughout this article, the tooling in Visual Studio for SDK format is lacking even after all the updates. Here’s a summary of what doesn’t work (or work well).
- Adding NuGet packages to the project does not always show up under references until you reload the solution. This includes the compiler not seeing the packages and therefore failing the build.
- T4 files cannot be transformed individually. You must transform at the solution level.
- DTE and other host services are not available to T4 files.
- File nesting does not work.
- Multi-targeting support outside the references in Solution Explorer is either non-existent or very limited.
- Most modifications will require that you hand edit the project file.
- For package generation, not all options are exposed in the UI.
SourceLink
If you do not know what SourceLink is then you should read up on it. The gist of it is the ability to reference a package containing the assembly and symbols. During debugging you can step into the package code and the debugger will automatically download the source code and allow seamless stepping. Visual Studio 2017 supports it but you have to enable it.
This feature is awesome. It solves the general discussion about having symbols in NuGet packages or not and the need for a symbol package (mostly). The general recommendation before was to use a separate symbol package so that NuGet packages were small. But now that we have a generic symbol format that is dramatically smaller than Windows symbol files AND SourceLink, shipping the symbols with the package makes sense.
Getting SourceLink set up for a package is not trivial. Fortunately Microsoft has solves this more easily now. Just include the appropriate NuGet package. Depending upon what source control system you use determines which package is included.
Note: The packages are currently pre-release so ensure you have enabled pre-release packages first.
Note: Be sure the package has PrivateAssets
set to all
.
You also need to add some elements to the project file.
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageProjectUrl></PackageProjectUrl>
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
Note that if you’re using VSTS and doing automated builds you can get the URLs auto-generated for you during builds.
<PackageProjectUrl Condition="'$(PackageProjectUrl)'==''">$(Build_Repository_Uri)</PackageProjectUrl>
<SourceRoot>
<SourceLinkVstsGitDomain>$(System_TeamFoundationServerUri)</SourceLinkVstsGitDomain>
</SourceRoot>
Download example code on GitHub.