This is a series on the latest 2.* .Net Core bits, Following on from the original .Net Core Series
- Getting Started
- What’s in the box
- Using Multiple Projects
- Testing
- NuGet
- Multi-targeting
- Publishing Portable Applications
- Self-contained Applications <=(We are here)
(At the time of writing, 2.1.4. I use windows, you don’t have to!)
The Problem
Distributing the fruits of your labour is one of the rewarding parts of building any kind of software. People actually start to use it! But distribution is never as easy as it should be. With .Net Core Applications, the story can be a similar one.
Traditionally, distributing a .Net Application means requiring a runtime on the target machine. While .Net isn’t the only platform to work this way, It has meant a bit of dependency wrangling on the target machine before you can run your app. Again, by no means is this as terrible an experience, or as painful as other platforms & runtimes can be, but you need to be aware of it.
With netcoreapp*
applications this is no different. While using the latest SDK can target multiple runtime versions and platforms (including older versions), and can pull down the right files for building against, you may find that unless you install the correct runtime, you won’t be able to run what you build. This is true even with the SDKs for the same major version are installed.
Let’s first take a look at how we can install different target runtime versions, then we can see how we can remove the need for this altogether.
Installing Multiple Platforms
One option on your development machine is to install everything.
This page has all the download links for all the different versions of .Net Core. You could download all these installers and install all the SDKs and runtimes.
If you do that, you can then run any version of .Net Core applications, and also build using any version of the SDK. You will have to install them in order from oldest to newest (especially true of the sdks) or some might not install properly. (All I mean is that installing oldest to newest will install everything as intended, but doing so out of order may not produce the desired result.)
But who has time for that? Automation anyone?
There are a set of dotnet-install scripts, both in Bash and PowerShell formats, that allow you to script the installation of dotnet runtimes. By default, these scripts will install the latest version into your user profile, but there are a tonne of options you can choose from.
There are a couple of ways to run the script, either download and execute as one command, or save the file, then execute it. (Shown below in PowerShell command format.)
# Download and Execute the script in one command
"&([scriptblock]::Create((Invoke-WebRequest -useb 'https://dot.net/v1/dotnet-install.ps1'))) -DryRun"
#Alternatively save the script, then execute it
Invoke-WebRequest -useb 'https://dot.net/v1/dotnet-install.ps1' -OutFile "dotn
et-install.ps1"
.\dotnet-install.ps1 -DryRun
(I’ve used -DryRun
argument to test the script parameters, before actually making changes.)
Let’s see how to use the script in two ways: First to install the equivalent of the way the installers would onto your machine. (You will need an Administrator Prompt for this.)
# This assumes your ${Env:ProgramFiles(x86)} is set to "C:\Program Files (x86)"
# Your default Program Files directory might be different otherwise.
.\dotnet-install.ps1 -Version 2.1.4 -InstallDir "C:\Program Files (x86)\dotnet"
# You can do this for other SDK versions
.\dotnet-install.ps1 -Version 2.0.3 -InstallDir "C:\Program Files (x86)\dotnet"
.\dotnet-install.ps1 -Version 1.1.7 -InstallDir "C:\Program Files (x86)\dotnet"
.\dotnet-install.ps1 -Version 1.1.5 -InstallDir "C:\Program Files (x86)\dotnet"
.\dotnet-install.ps1 -Version 1.0.4 -InstallDir "C:\Program Files (x86)\dotnet"
# And for other runtimes
.\dotnet-install.ps1 -Version 2.0.5 -InstallDir "C:\Program Files (x86)\dotnet" -SharedRuntime
.\dotnet-install.ps1 -Version 2.0.3 -InstallDir "C:\Program Files (x86)\dotnet" -SharedRuntime
.\dotnet-install.ps1 -Version 2.0.0 -InstallDir "C:\Program Files (x86)\dotnet" -SharedRuntime
.\dotnet-install.ps1 -Version 1.1.6 -InstallDir "C:\Program Files (x86)\dotnet" -SharedRuntime
.\dotnet-install.ps1 -Version 1.1.5 -InstallDir "C:\Program Files (x86)\dotnet" -SharedRuntime
.\dotnet-install.ps1 -Version 1.0.9 -InstallDir "C:\Program Files (x86)\dotnet" -SharedRuntime
.\dotnet-install.ps1 -Version 1.0.8 -InstallDir "C:\Program Files (x86)\dotnet" -SharedRuntime
If you haven’t already run an installer for any versions of dotnet SDK, you will probably have to manually add C:\Program Files (x86)\dotnet
to your PATH Environment Variable. By default it will be made immediately available for the current console window session.
Next let’s look at doing the same thing into a stand-alone folder on your machine instead.
# Install the latest/current stable SDK first
.\dotnet-install.ps1 -Channel Current -InstallDir C:\dotnet-cli
# Now other required SDKs
.\dotnet-install.ps1 -Version 2.1.4 -InstallDir C:\dotnet-cli # Should be the same as Current at the time of writing
.\dotnet-install.ps1 -Version 2.0.3 -InstallDir C:\dotnet-cli
.\dotnet-install.ps1 -Version 1.1.7 -InstallDir C:\dotnet-cli
.\dotnet-install.ps1 -Version 1.1.5 -InstallDir C:\dotnet-cli
.\dotnet-install.ps1 -Version 1.0.4 -InstallDir C:\dotnet-cli
.\dotnet-install.ps1 -Version 1.0.1 -InstallDir C:\dotnet-cli
# Now all the required runtimes
.\dotnet-install.ps1 -Version 2.0.5 -InstallDir C:\dotnet-cli -SharedRuntime
.\dotnet-install.ps1 -Version 2.0.3 -InstallDir C:\dotnet-cli -SharedRuntime
.\dotnet-install.ps1 -Version 2.0.0 -InstallDir C:\dotnet-cli -SharedRuntime
.\dotnet-install.ps1 -Version 1.1.6 -InstallDir C:\dotnet-cli -SharedRuntime
.\dotnet-install.ps1 -Version 1.1.5 -InstallDir C:\dotnet-cli -SharedRuntime
.\dotnet-install.ps1 -Version 1.1.2 -InstallDir C:\dotnet-cli -SharedRuntime
.\dotnet-install.ps1 -Version 1.1.1 -InstallDir C:\dotnet-cli -SharedRuntime
.\dotnet-install.ps1 -Version 1.1.0 -InstallDir C:\dotnet-cli -SharedRuntime
.\dotnet-install.ps1 -Version 1.0.9 -InstallDir C:\dotnet-cli -SharedRuntime
.\dotnet-install.ps1 -Version 1.0.8 -InstallDir C:\dotnet-cli -SharedRuntime
.\dotnet-install.ps1 -Version 1.0.4 -InstallDir C:\dotnet-cli -SharedRuntime
.\dotnet-install.ps1 -Version 1.0.3 -InstallDir C:\dotnet-cli -SharedRuntime
As mentioned before, for the current session you will find the newly installed dotnet.exe
set in your path pointing at the new folders, but You will also need to manually put this into your PATH Environment variable to make this version always available as the default dotnet
command.
But why a Runtime?
Why do we need to install the target runtime? Well, mainly it is because we want to keep our application distribution small. We do this by leaving out a bunch of common framework executable code, that we entrust to a runtime distribution to deliver to the target machine for us. This runtime can be shared across applications and so they will all get the benefit of being smaller.
But what if we don’t want to share? Traditionally this hasn’t been much of an option. For certain dll
files, we can distribute them with our app to ensure the correct one is chosen, rather than relying on the system to find them for us. But this isn’t generally true for all parts of the framework, especially more crucial low-level system details. (And sometimes the installed framework will override us anyway - “GAC DLL hell” or “dee el hell” is what we called it.)
Look Mum, No Runtime!
But .Net Core…
Yes, now with .Net Core, there is a way to bundle the entire (yes all!) specific (exact version!) runtime with your application.
This has two main benefits:
- We do not require anything* to be pre-installed on the target machine.
- We can ensure exactly the right set of libraries and code is running with our application, down to the framework bugs at the time of packaging.
Just so this doesn’t sound too Silver-Bullet-y, a few quick drawbacks, too:
- Your distributable won’t be small anymore. (~60mb to start, just for the runtime)
- You will be distributing against a specific platform (Operating System), and may need multiple packages, one per Operating System target.
It is up to you to decide the trade-offs that make the most sense for your application.
Show me the Code!
mkdir MySelfContainedApp
cd MySelfContainedApp
mkdir MyApp
cd MyApp
dotnet new console
cd ../
dotnet new sln
dotnet sln add .\MyApp\MyApp.csproj
dotnet build
Once we have an app to use, this is how we package it with its own runtime:
dotnet publish --self-contained --runtime win7-x64
(Not using Windows? Other runtime values include osx.10.12-x64
and ubuntu.16.04-x64
. A full list and description of them all can be found in the rid catalog)
The runtime identifier win7-x64
means we will package with a runtime compatible with the Windows 7(& Windows Server 2008 R2) Operating System, 64-bit Architecture. This means that it will run on those Operating Systems, and should be forward-compatible to newer systems (compatibility). (It also means there may be performance optimisations from newer systems like Win8
and Win10
that won’t be taken advantage of when it runs in Compatability mode on the newer OS versions.) It also states that it requires a 64-bit Architecture. no 32-bit Architecture machines will be able to run the application. (You may recall building apps in the past that could target AnyCPU
. This isn’t it.)
In general, you can identify Operating System, OS Version, and architecture from the parts of an rid. This is representative, and should not be relied on as a composition of any OS/Version/Arch into an rid. Use the reference guide to ensure you are using valid identifiers.
As well as specific runtimes, SDK 2.0 introduces Portable rids. Win-x64
, linux-x64
and osx-x64
give you version-agnostic Operating System Targets. (Most useful for Linux, since it is across multiple distros - Ubuntu, Tizen, Red Hat, Fedora etc etc.) This doesn’t necessarily affect the size, mostly just the compatibility. (Affects Performance/Optimisations? Possibly. Perf test if you want to find out! 😉 )
Let’s take a look at the output from our publish command:
MySelfContainedApp
|-- MySelfContainedApp.sln
+---MyApp
|-- MyApp.csproj
|-- Program.cs
+-- bin
+-- Debug
+-- netcoreapp2.0
|-- MyApp.deps.json
|-- MyApp.dll
|-- MyApp.pdb
|-- MyApp.runtimeconfig.dev.json
|-- MyApp.runtimeconfig.json
+-- win7-x64
|-- hostfxr.dll
|-- hostpolicy.dll
|-- MyApp.deps.json
|-- MyApp.dll
|-- MyApp.exe
|-- MyApp.pdb
|-- MyApp.runtimeconfig.dev.json
|-- MyApp.runtimeconfig.json
|--
+-- publish
|-- api-ms-win-core-console-l1-1-0.dll
|-- api-ms-win-core-datetime-l1-1-0.dll
|-- api-ms-win-core-debug-l1-1-0.dll
|-- api-ms-win-core-errorhandling-l1-1-0.dll
|-- api-ms-win-core-file-l1-1-0.dll
|-- api-ms-win-core-file-l1-2-0.dll
|-- api-ms-win-core-file-l2-1-0.dll
|-- api-ms-win-core-handle-l1-1-0.dll
|-- api-ms-win-core-heap-l1-1-0.dll
|-- api-ms-win-core-interlocked-l1-1-0.dll
|-- api-ms-win-core-libraryloader-l1-1-0.dll
|-- api-ms-win-core-localization-l1-2-0.dll
|-- api-ms-win-core-memory-l1-1-0.dll
|-- api-ms-win-core-namedpipe-l1-1-0.dll
|-- api-ms-win-core-processenvironment-l1-1-0.dll
|-- api-ms-win-core-processthreads-l1-1-0.dll
|-- api-ms-win-core-processthreads-l1-1-1.dll
|-- api-ms-win-core-profile-l1-1-0.dll
|-- api-ms-win-core-rtlsupport-l1-1-0.dll
|-- api-ms-win-core-string-l1-1-0.dll
|-- api-ms-win-core-synch-l1-1-0.dll
|-- api-ms-win-core-synch-l1-2-0.dll
|-- api-ms-win-core-sysinfo-l1-1-0.dll
|-- api-ms-win-core-timezone-l1-1-0.dll
|-- api-ms-win-core-util-l1-1-0.dll
|-- api-ms-win-crt-conio-l1-1-0.dll
|-- api-ms-win-crt-convert-l1-1-0.dll
|-- api-ms-win-crt-environment-l1-1-0.dll
|-- api-ms-win-crt-filesystem-l1-1-0.dll
|-- api-ms-win-crt-heap-l1-1-0.dll
|-- api-ms-win-crt-locale-l1-1-0.dll
|-- api-ms-win-crt-math-l1-1-0.dll
|-- api-ms-win-crt-multibyte-l1-1-0.dll
|-- api-ms-win-crt-private-l1-1-0.dll
|-- api-ms-win-crt-process-l1-1-0.dll
|-- api-ms-win-crt-runtime-l1-1-0.dll
|-- api-ms-win-crt-stdio-l1-1-0.dll
|-- api-ms-win-crt-string-l1-1-0.dll
|-- api-ms-win-crt-time-l1-1-0.dll
|-- api-ms-win-crt-utility-l1-1-0.dll
|-- clrcompression.dll
|-- clretwrc.dll
|-- clrjit.dll
|-- coreclr.dll
|-- dbgshim.dll
|-- hostfxr.dll
|-- hostpolicy.dll
|-- Microsoft.CSharp.dll
|-- Microsoft.DiaSymReader.Native.amd64.dll
|-- Microsoft.VisualBasic.dll
|-- Microsoft.Win32.Primitives.dll
|-- Microsoft.Win32.Registry.dll
|-- mscordaccore.dll
|-- mscordaccore_amd64_amd64_4.6.00001.0.dll
|-- mscordbi.dll
|-- mscorlib.dll
|-- mscorrc.debug.dll
|-- mscorrc.dll
|-- MyApp.deps.json
|-- MyApp.dll
|-- MyApp.exe
|-- MyApp.pdb
|-- MyApp.runtimeconfig.json
|-- netstandard.dll
|-- sos.dll
|-- SOS.NETCore.dll
|-- sos_amd64_amd64_4.6.00001.0.dll
|-- System.AppContext.dll
|-- System.Buffers.dll
|-- System.Collections.Concurrent.dll
|-- System.Collections.dll
|-- System.Collections.Immutable.dll
|-- System.Collections.NonGeneric.dll
|-- System.Collections.Specialized.dll
|-- System.ComponentModel.Annotations.dll
|-- System.ComponentModel.Composition.dll
|-- System.ComponentModel.DataAnnotations.dll
|-- System.ComponentModel.dll
|-- System.ComponentModel.EventBasedAsync.dll
|-- System.ComponentModel.Primitives.dll
|-- System.ComponentModel.TypeConverter.dll
|-- System.Configuration.dll
|-- System.Console.dll
|-- System.Core.dll
|-- System.Data.Common.dll
|-- System.Data.dll
|-- System.Diagnostics.Contracts.dll
|-- System.Diagnostics.Debug.dll
|-- System.Diagnostics.DiagnosticSource.dll
|-- System.Diagnostics.FileVersionInfo.dll
|-- System.Diagnostics.Process.dll
|-- System.Diagnostics.StackTrace.dll
|-- System.Diagnostics.TextWriterTraceListener.dll
|-- System.Diagnostics.Tools.dll
|-- System.Diagnostics.TraceSource.dll
|-- System.Diagnostics.Tracing.dll
|-- System.dll
|-- System.Drawing.dll
|-- System.Drawing.Primitives.dll
|-- System.Dynamic.Runtime.dll
|-- System.Globalization.Calendars.dll
|-- System.Globalization.dll
|-- System.Globalization.Extensions.dll
|-- System.IO.Compression.dll
|-- System.IO.Compression.FileSystem.dll
|-- System.IO.Compression.ZipFile.dll
|-- System.IO.dll
|-- System.IO.FileSystem.AccessControl.dll
|-- System.IO.FileSystem.dll
|-- System.IO.FileSystem.DriveInfo.dll
|-- System.IO.FileSystem.Primitives.dll
|-- System.IO.FileSystem.Watcher.dll
|-- System.IO.IsolatedStorage.dll
|-- System.IO.MemoryMappedFiles.dll
|-- System.IO.Pipes.dll
|-- System.IO.UnmanagedMemoryStream.dll
|-- System.Linq.dll
|-- System.Linq.Expressions.dll
|-- System.Linq.Parallel.dll
|-- System.Linq.Queryable.dll
|-- System.Net.dll
|-- System.Net.Http.dll
|-- System.Net.HttpListener.dll
|-- System.Net.Mail.dll
|-- System.Net.NameResolution.dll
|-- System.Net.NetworkInformation.dll
|-- System.Net.Ping.dll
|-- System.Net.Primitives.dll
|-- System.Net.Requests.dll
|-- System.Net.Security.dll
|-- System.Net.ServicePoint.dll
|-- System.Net.Sockets.dll
|-- System.Net.WebClient.dll
|-- System.Net.WebHeaderCollection.dll
|-- System.Net.WebProxy.dll
|-- System.Net.WebSockets.Client.dll
|-- System.Net.WebSockets.dll
|-- System.Numerics.dll
|-- System.Numerics.Vectors.dll
|-- System.ObjectModel.dll
|-- System.Private.CoreLib.dll
|-- System.Private.DataContractSerialization.dll
|-- System.Private.Uri.dll
|-- System.Private.Xml.dll
|-- System.Private.Xml.Linq.dll
|-- System.Reflection.DispatchProxy.dll
|-- System.Reflection.dll
|-- System.Reflection.Emit.dll
|-- System.Reflection.Emit.ILGeneration.dll
|-- System.Reflection.Emit.Lightweight.dll
|-- System.Reflection.Extensions.dll
|-- System.Reflection.Metadata.dll
|-- System.Reflection.Primitives.dll
|-- System.Reflection.TypeExtensions.dll
|-- System.Resources.Reader.dll
|-- System.Resources.ResourceManager.dll
|-- System.Resources.Writer.dll
|-- System.Runtime.CompilerServices.VisualC.dll
|-- System.Runtime.dll
|-- System.Runtime.Extensions.dll
|-- System.Runtime.Handles.dll
|-- System.Runtime.InteropServices.dll
|-- System.Runtime.InteropServices.RuntimeInformation.dll
|-- System.Runtime.InteropServices.WindowsRuntime.dll
|-- System.Runtime.Loader.dll
|-- System.Runtime.Numerics.dll
|-- System.Runtime.Serialization.dll
|-- System.Runtime.Serialization.Formatters.dll
|-- System.Runtime.Serialization.Json.dll
|-- System.Runtime.Serialization.Primitives.dll
|-- System.Runtime.Serialization.Xml.dll
|-- System.Security.AccessControl.dll
|-- System.Security.Claims.dll
|-- System.Security.Cryptography.Algorithms.dll
|-- System.Security.Cryptography.Cng.dll
|-- System.Security.Cryptography.Csp.dll
|-- System.Security.Cryptography.Encoding.dll
|-- System.Security.Cryptography.OpenSsl.dll
|-- System.Security.Cryptography.Primitives.dll
|-- System.Security.Cryptography.X509Certificates.dll
|-- System.Security.dll
|-- System.Security.Principal.dll
|-- System.Security.Principal.Windows.dll
|-- System.Security.SecureString.dll
|-- System.ServiceModel.Web.dll
|-- System.ServiceProcess.dll
|-- System.Text.Encoding.dll
|-- System.Text.Encoding.Extensions.dll
|-- System.Text.RegularExpressions.dll
|-- System.Threading.dll
|-- System.Threading.Overlapped.dll
|-- System.Threading.Tasks.Dataflow.dll
|-- System.Threading.Tasks.dll
|-- System.Threading.Tasks.Extensions.dll
|-- System.Threading.Tasks.Parallel.dll
|-- System.Threading.Thread.dll
|-- System.Threading.ThreadPool.dll
|-- System.Threading.Timer.dll
|-- System.Transactions.dll
|-- System.Transactions.Local.dll
|-- System.ValueTuple.dll
|-- System.Web.dll
|-- System.Web.HttpUtility.dll
|-- System.Windows.dll
|-- System.Xml.dll
|-- System.Xml.Linq.dll
|-- System.Xml.ReaderWriter.dll
|-- System.Xml.Serialization.dll
|-- System.Xml.XDocument.dll
|-- System.Xml.XmlDocument.dll
|-- System.Xml.XmlSerializer.dll
|-- System.Xml.XPath.dll
|-- System.Xml.XPath.XDocument.dll
|-- ucrtbase.dll
\-- WindowsBase.dll
That publish folder is 62.7 MB! Compare that to the size of the files in MySelfContainedApp\MyApp\bin\Debug\netcoreapp2.0
, which are only 5.76 KB. Compared to the app, that is a lot of Runtime to carry around. (The application is less than a percent of the total distributable size)
This is a considerable tradeoff and should be considered carefully when making the decision between packing portable or distributing the runtime.
The Cake is a Lie
I said earlier that one of the benefits of distributing the runtime was We do not require anything* to be pre-installed on the target machine.
The * was “with a few exceptions”.
Windows being Windows, you still actually need one more thing. Microsoft Visual C++ 2015 Redistributable Update 3 is needed for the packaged runtime to work on windows. This gets installed if required by the dotnet installer, but won’t be in your self-contained package. (It also isn’t installed if you use the dotnet-install.ps1
script from earlier, either.) It is likely that something else will require this Redistributable on windows anyway, so this isn’t a big deal, really.
You can take a look at Prerequisites for .NET Core on Windows for more information on this.
Linux doesn’t escape unscathed either. Dependency graphs on Linux distros can get a bit overwhelming at the best of times. That is why we have package managers and installers. Due to the variance of distros, I can’t tell you what you will or won’t need here. First, see Prerequisites for .NET Core on Linux for more information in general. Then specifically you will want to check out the Self-contained Linux applications documentation.
For some reason, MacOS is fine? Apart from a minimum version (macOS 10.12 “Sierra” and later) and something about increasing the open file limit, there isn’t much else you need to do. Something to be said for standard software and hardware, I suppose.
Wrap up
So there you have it. You have the choice available to you. You can package one ~5kb portable application (in reality maybe ~5mb) and distribute it, requiring the correct runtime version to be already installed, or you can package in the correct runtime with you app for an extra ~60mb, and build and package at least 3 times to cover windows, Linux and Mac operating systems. (And still require some dependencies anyway).
IF you are bundling an app(game?) that includes a lot of assets and is already distributing 2GB of software, that extra 60Mb is not a big deal, but in general that ratio is rather large.
Of course, this is more than about size, because we can guarantee the compatibility as well because we will have tested our application with exactly the right runtime version that we distribute with.
The choice is yours.
At some point, I will have to experiment and write about how we might get that size down (there are a few ways I am aware of) so we may still get the best of both worlds.
The End
Thank you so much for getting this far. It has been a long journey from Getting Started with 1.0 almost 2 years ago to get here across these new 8 articles. Thanks for sticking with it. I hope you look forward to more .Net Core blog articles to come. Maybe even a revised 3.* version? (Take your time, MS)
You can find related articles using the dotnetcore tag.