Porting ASP.NET Core application to VS 2017
Introduction
It’s been ten days since Visual Studio 2017 RTM was released. .xproj
and project.json
files are not going to be supported anymore so it’s time to port our (ASP).NET Core projects to the new, simplified .csproj
project file format.
This post is not a complete guide on porting. I want to show you what problems I have faced and how I had solved them. I hope that somebody will find this useful. The examples are based on an ASP.NET Core application for trading energy that I develop at work. So it’s something real, not “Hello World”. I have also ported BenchmarkDotNet to the new .csproj file format so it’s a mixture of my personal experience.
Before you start
Nate McMaster, who is a developer on the ASP.NET team wrote two great blog posts about porting to MSBuild. If you want to have a good understanding of what you are doing you should read them first:
Use the tool
The first thing you need to do is to open your solution with new Visual Studio 2017 and approve the One-way upgrade. I recommend doing this from VS, not the console (dotnet migrate
). VS takes better care of all the MSBuild-related paths. So if your solution contains a lot of different projects (C#, F#, VB, .NET Core and classic .NET framework) you have a higher chance of success. (BenchmarkDotNet contains all of them because we support all possible scenarios ;) )
Then you just wait for the dotnet migrate
to do its job.
After it’s finished a Migration Reports is opened in the web browser. Make sure you read the errors part. Zero errors in the report does not mean that you are done with the porting ;)
You will notice that all project.json
and *.xproj
files are gone. Don’t worry, they all have been saved in the Backup
folder. Of course, the new .csproj
files have been created. At this point of time, you should commit all the changes. Later on, you will see what was done by the tool, and what changes you have applied manually.
Verify the output
Software engineers should have limited trust in tools they use. So now it’s time to compare your old project.json
files with the new .csproj
files. If you have read the two recommended posts you should already know what the mappings should be. Look for the missing parts, the less common or more complicated feature, the higher the chances that something is wrong. Once you find a difference you need to do the “manual” porting ;) I recommend you to do a separate commit per every “fix” so your team members can understand what you did.
For me, the first thing I noticed was that paths to custom ruleset files for static code analysis were missing.
"configurations": {
"Release": {
"buildOptions": {
"additionalArguments": [ "/ruleset:../../StyleCop.Analyzers.ruleset" ],
}
}
"dependencies": {
"StyleCop.Analyzers": {
"version": "1.0.0",
"type": "build"
}
}
was translated to:
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.0.0">
<PrivateAssets>All</PrivateAssets>
</PackageReference>
</ItemGroup>
In case you also use StyleCop.Analyzers
with custom rules you need to fix it:
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.0.0" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<CodeAnalysisRuleSet>$(SolutionDir)\StyleCop.Analyzers.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
Please notice the usage of $(SolutionDir)
. MSBuild has a long list of available properties. By using them you can take advantage of all MSBuild features!
You might also discover that VS has created runtimeconfig.template.json
file next to your ASP.NET Core app project file.
{
"gcServer": true,
"gcConcurrent": true
}
For me the less the files the better, so I advise you to move these two flags to the .csproj
of your app and remove the runtimeconfig.template.json
.
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
</PropertyGroup>
Please also keep in mind that Concurrent Server GC mode which is the default mode for APS.NET Core apps has some drawbacks. It gives you a great performance, but not for free. It creates two dedicated GC threads per every logical core. So if you have a hundred of “Microservices” running on your 24 core server you end up with 100 x (24 x 2 + 1) = 4900
GC threads. +1
is for single dedicated finalizer thread for every .NET process. Which option fits you the best? Just measure it ;)
Build and Run
Now it’s the time to build and run your app. But before you do so I recommend you to clean up your solution directory to make sure that you perform a “clean” build. What works best for me:
// commit your changes, close VS
git clean -xfd
dotnet restore
// open VS
// build from VS
The first problem that I have encountered was The debug profile 'web' is missing the path executable to debug
. The fact that I could not google it pushed me to blog so others don’t waste their time.
It turned out that my launchSettings.json
file did not get ported correctly. It was:
"web": {
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"web-production": {
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Production"
}
But should be:
"web": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"web-production": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Production"
}
Which got me one step closer:
System.IO.FileLoadException: 'Could not load file or assembly 'Microsoft.Win32.Primitives, Version=4.0.0.0
Which I fixed with help of Stack Overflow user Cody by installing Microsoft.Win32.Primitives 4.0.0.0
NuGet package. This particular app is using ASP.NET Core and Kestrel, but runs on net46
. Those of you who target netcoreapp
might not encounter the same problem.
Now I was able to compile, run and debug our app.
As of today ASP.NET Core app that targets classic .NET is x86
by default. You can enforce x64
by applying: <PlatformTarget>x64</PlatformTarget>
in your .csproj
.
Versioning and Deployment
We used to have some PowerShell script that was setting version for every project.json
file in our repository. With .csproj
you don’t need to write your own scripts for doing this. MSBuild is a mature platform, this problem has already been solved.
If you want to use your build number that is passed by your CI to every process it spawns you can do it with MSBuild by reading the environment variable.
<BuildNumber Condition=" '$(APPVEYOR_BUILD_NUMBER)' != '' ">$(APPVEYOR_BUILD_NUMBER)</BuildNumber> <!-- for AppVeyor -->
<BuildNumber Condition=" '$(BUILD_NUMBER)' != '' ">$(BUILD_NUMBER)</BuildNumber> <!-- for Team City -->
<BuildNumber Condition=" '$(BuildNumber)' == '' ">0</BuildNumber> <!-- if not set -->
Then you can set whatever suits you best by using the combination of MSBuild properties, features, and variables. Sample:
<AssemblyVersion>$(BuildNumber)</AssemblyVersion>
<AssemblyFileVersion>$(BuildNumber)</AssemblyFileVersion>
<InformationalVersion>$(BuildNumber)</InformationalVersion>
<PackageVersion>$(BuildNumber)</PackageVersion>
To build a package you still need to call dotnet publish
. However, the way of handling relative path for --output
option has changed. Whatever you pass to it is now relative to the .csproj
file, not the working directory (as it was before).
One important thing about CI: if you install the new .NET Core SDK it will become the default one. It means that all projects, that still use project.json
file will fail to compile. It’s bad for other projects that are compiled on the same CI machine. You can avoid this problem by setting the SDK version for old projects it the corresponding global.json
file:
{
"sdk": { "version": "1.0.0-preview2-1-003177" }
}
Refactoring
MSBuilds allows you to include multiple .props
files in your .csproj
files. Props file can contain most of the .csproj
settings. I recommend you to move all your common settings that are the same for every project to a .props
file and simply include it in your .csproj
files.
Sample common settings of Kestrel project:
<Project>
<Import Project="dependencies.props" />
<Import Project="..\version.props" />
<PropertyGroup>
<Product>Microsoft ASP.NET Core</Product>
<RepositoryUrl>https://github.com/aspnet/KestrelHttpServer</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)Key.snk</AssemblyOriginatorKeyFile>
<SignAssembly>true</SignAssembly>
<PublicSign Condition="'$(OS)' != 'Windows_NT'">true</PublicSign>
<VersionSuffix Condition="'$(VersionSuffix)'!='' AND '$(BuildNumber)' != ''">$(VersionSuffix)-$(BuildNumber)</VersionSuffix>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Internal.AspNetCore.Sdk" Version="1.0.1-*" PrivateAssets="All" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFrameworkIdentifier)'=='.NETFramework' AND '$(OutputType)'=='library'">
<PackageReference Include="NETStandard.Library" Version="$(NetStandardImplicitPackageVersion)" />
</ItemGroup>
</Project>
And then in your .csproj
:
<Import Project="..\..\settings\common.props" />
In our solution, I have moved all things related to versioning to single .props
file, common settings to another .props
file. Then included these in .csprojs
and removed all AssemblyInfo.cs
files. In my case, I just don’t need them anymore. And my new.csproj
files are now smaller than my old, beloved project.json
files!