In this article, I will look at the components of the .NET Core 2.0 platform that are required to load and run .NET Core applications, as well as the artifacts for two possible types of deployment.
The text is voluminous and designed for:
- novice developers who are just familiar with the .NET Core platform;
- experienced developers performing the role of DevOps-engineers in the production environment.
The article does not mention the process of creating applications using the SDK (dotnet CLI), but this information will be useful for understanding how the SDK works, namely its main component (core) - “driver” dotnet.dll, since this library is a managed assembly and running on .NET core.
')
Examples of execution processes are described for Windows OS, but they work on the same principle on other OSs (taking into account various extensions of executable files and native libraries).
0. Pay-for-Play

Every .NET developer knows from the cradle: to run any .NET application, the .NET Framework, namely CLR + BCL, must be installed on the target computer.
BCL is located in the GAC, from where the applications load the dependencies necessary for the operation.
In general, the .NET Core architecture looks the same: .NET Core = Core CLR + Core FX (new name for BCL), but differs in the way these components are enabled, as well as in the way of loading the runtime environment (CLR). Instead of the header in the managed assembly MyApp.exe in the .NET Framework, in .NET Core, MyApp.exe itself is the native Core CLR download program.
In .NET Core, all program components that we define at compile time are application dependencies (including Core CLR, JIT) that the .NET Core infrastructure treats as packages. Such a package is called
asset , and it can be either a NuGet package or a regular file.
Examples of components that are shipped via NuGet:
- Microsoft.NETCore.Runtime.CoreCLR - Core CLR.
- Microsoft .NETCore.Jit - JIT compiler.
- System.Private.CoreLib - the base types are System.Object, System.Int32, System.String (analog mscorlib.dll).
- System.Console - console access.
When the application is launched, these uncompressed dependencies must be located in one of the specific directories (the .NET Core framework - Core FX folder, the application folder, or any NuGet cache).
Thanks to this model, the .NET Core application consists of a frighteningly huge number of small modules, but this is done to reduce the amount of unnecessary dependencies.
This approach is called pay-for-play; in other words, applications download only the functionality that they need, but each such functionality is contained in a separate assembly.
1. FDD vs SCD
There are
two types of deployment of .NET Core applications :
- Portable (Framework-dependent deployment - FDD)
- Standalone (Self-contained deployment - SCD)
A portable (FDD) application is similar to a traditional .NET Framework application. In this case, a specific version of the .NET Core framework (the terms shared framework, .NET Core Runtime, redist are also used) must reside on the target computer, and when it starts, the host process will load Core CLR, Core FX from the framework folder.
In the
Standalone (SCD) application, all components for execution (CoreCLR, CoreFX), as well as third-party libraries, that is, absolutely all dependencies, are shipped with the application itself (most often in the same folder).
It is important to understand that a standalone application is tied to a specific OS and architecture (for example, Windows 7 x64 or OSX 10.12 x64). This identifier is called the
Runtime identifier (RID) . Each OS / architecture has its own version of the Core CLR library (and other native components), so for the Standalone applications, at the compilation stage, in the RuntimeIdentifier property, you must specify the target system parameters (RID).
Such an application will work on any computer with a specific OS / architecture, regardless of whether .NET Core is installed or not.
2. .NET Core Runtimes (shared frameworks)
To run portable applications, at least one
.NET Core Runtime (shared framework) must be installed on the target machine.
The .NET Core Runtime is installed in the
C: \ Program Files \ dotnet folder :

The framework files (s) are stored in the
C: \ Program Files \ dotnet \ shared folder.
The main components of the .NET Core Runtime:
- "Utility" dotnet.exe to run. NET Core-applications. It is called a multiplexer (muxer) , and is the main driver of the .NET Core infrastructure. This program serves as an “entry point” for launching any applications and executing development commands. if the .NET Core SDK is installed, that is, the host process of any application is corehost .
- Runtime components (CoreCLR, CoreFX, etc.) are installed into a separate folder of the C: \ Program Files \ dotnet \ shared \ [Framework name] \ [Framework version] framework.
- Host framework resolver - the native library, located in the folder
C: \ Program Files \ dotnet \ host \ [version] \ hostfxr.dll. When the application is launched, the maximum version of this library performs the resolution of the framework version for the subsequent execution of the application.
File structure when installing the .NET Core Runtime .
You can install multiple versions of the framework:

To execute a portable application, you must run the dotnet.exe host process and pass the path to the managed assembly as an argument.
"C: \ Program Files \ dotnet" is added to the value of the PATH environment variable, so that Portable applications can now be run from the command line:
> dotnet path/to/App.dll
In the application folder (where [AppName] .dll is located) the [AppName] .runtimeconfig.json file should be located. It contains the name and version of the framework that should be used to run the Portable application. For example:
MyApp.runtimeconfig.json { "runtimeOptions": { "framework": { "name": "Microsoft.NETCore.App", "version": "2.0.0" } } }
This file is required for portable applications.
Having the above configuration, the runtime components will be downloaded from the
C: \ Program Files \ dotnet \ shared \ Microsoft.NETCore.App \ 2.0.0 folder.
3. Structure of a Portable (FDD) .NET Core application
Any Portable .NET Core application consists of the following required files:
- [AppName] .dll - application IL, entry point.
- [App dependencies] *. Dll - all application dependencies that are not part of CoreFX (project builds, third-party libraries, FCL).
- [AppName] .runtimeconfig.json - runtime configuration , here are the name and version of the .NET Core framework (runtime components). The file is something like MyApp.exe.config in .NET Frameowork. This configuration can be changed if you need to explicitly specify a specific framework.
- [AppName] .deps.json - a list of all application dependencies. It is not recommended to change this file because it is generated at compilation. The file is not mandatory, but if you delete it, the host process at startup will not be able to check the paths of all dependency files, and execution will start at your own risk.
DocumentationArtifacts of the same Portable application for different versions of the .NET Core platform:

The reduction in the number of files is due to the fact that many libraries were missing from Core FX 1.0, so they went as part of the application, like normal dependencies. In Core FX 2.0, these assemblies have been added, so they are no longer shipped with the application, but are taken from the framework folder.
4. Standalone (SCD) .NET Core Application Structure
Same as for a Portable (FDD) application, but additionally contains all the runtime components (CoreCLR, CoreFX) and its own dotnet.exe
multiplexer , renamed [AppName] .exe. For .NET Core pre-2.0, the multiplexer for running a standalone application is identical to C: \ Program Files \ dotnet.exe (the same file, just renamed). For .NET Core 2.0, a multiplexer from the Microsoft .NETCore.DotNetAppHost NuGet is used. The package contains one apphost.exe file, which, when compiled, is “stitched” in the assembly name (MyApp.dll), and the file itself is renamed to MyApp.exe. When you start a standalone application, the binding of the executable file (MyApp.exe) to the name of the assembly that it can run (MyApp.dll) is checked.
The contents of the same Standalone application for different versions of the .NET Core platform:

There is a picture opposite of Portable applications - the more Core FX becomes, the more files comes with the application.
Deployment Type Guidelines- Always give preference to a portable deployment, because this type is much smaller and more stable when running large applications with a large number of dependencies. In addition, portable applications are easier to set up, because they do not depend on the RID.
- Choose Standalone if it is not possible to install the .NET Core Runtime, or if the duration of the application launch is critical. In the Standalone version, you can win 1-2 seconds at startup by deleting the configuration file [AppName] .deps.json (remember, this also makes you responsible for having all the dependency files).
5. Runtime Configuration Files
The [AppName] .runtimeconfig.json and [AppName] .deps.json files are called the
Runtime Configuration Files (* .deps.json are called the dependency manifest file). They are created during the compilation process and contain all the information necessary to run dotnet.exe and run the application.
In
[AppName] .runtimeconfig.json, the name and version of .NET Core runtime are specified (it also indicates whether the patch version (
SemVer ) will be taken into account when searching for the framework), and the Core CLR operation parameters (garbage collector operation mode) are set. This file is required for Portable and optional for Standalone applications.
dotnet.exe ([AppName] .exe) uses the
[AppName] .deps.json file to determine the absolute paths of all dependencies of the application when it is started.
Structure
[AppName] .deps.json :
- Targets section
The term target refers to the target platform (name and version) on which this application should run (for example, .NET Framework 4.6.2, .NET Core App 1.1, Xamarin.Mac 1.0, .NET Standard 1.6). This configuration is similar to the NuGet target framework .
The targets section defines a platform and a dependency tree for it in the format
[Dependency ID (package)] / [version]: {
dependencies: {list of dependencies (packages) of this package},
relative paths to the managed and native files of this package
}
To run any application, the target must contain the RID, for example .NETCoreApp, Version = v1.1 / win10-x64 . The standalone application's deps.json file is always one and contains the target platform RID. For a portable application, there are two deps.json files - one in the framework folder, the second in the application folder. The RID for Portable Applications is listed in the [FrameworkName] .deps.json file in the framework folder. After dotnet.exe has defined a framework for executing an application, it first loads the deps file for this framework (for example, C: \ Program Files \ dotnet \ shared \ Microsoft.NETCore.App \ 2.0.0 \ Microsoft.NETCore.App. deps ), and then the deps file of the application. Deps-file application has a higher priority.
Let's take a closer look at the contents of the Standalone application's deps.json file:
SampleApp.deps.json "targets": { ".NETCoreApp,Version=v1.1/win7-x64": { ... "libuv/1.9.1": { "dependencies": { "Microsoft.NETCore.Platforms": "1.1.0" }, "native": { "runtimes/win7-x64/native/libuv.dll": {} } }, ... "system.data.sqlclient/4.3.0": { "dependencies": { "System.Data.Common": "4.3.0", "System.IO.Pipes": "4.3.0", "System.Text.Encoding.CodePages": "4.3.0", "runtime.native.System.Data.SqlClient.sni": "4.3.0" }, "runtimeTargets": { "runtimes/unix/lib/netstandard1.3/System.Data.SqlClient.dll": { "rid": "unix", "assetType": "runtime" }, "runtimes/win/lib/netstandard1.3/System.Data.SqlClient.dll": { "rid": "win", "assetType": "runtime" } } }, ... "runtime.win7-x64.microsoft.netcore.runtime.coreclr/1.1.1": { "runtime": { "runtimes/win7-x64/lib/netstandard1.0/SOS.NETCore.dll": {}, "runtimes/win7-x64/lib/netstandard1.0/System.Private.CoreLib.dll": {}, "runtimes/win7-x64/lib/netstandard1.0/mscorlib.dll": {} }, "native": { "runtimes/win7-x64/native/System.Private.CoreLib.ni.dll": {}, "runtimes/win7-x64/native/clretwrc.dll": {}, "runtimes/win7-x64/native/coreclr.dll": {}, "runtimes/win7-x64/native/dbgshim.dll": {}, "runtimes/win7-x64/native/mscordaccore.dll": {}, "runtimes/win7-x64/native/mscordbi.dll": {}, "runtimes/win7-x64/native/mscorlib.ni.dll": {}, "runtimes/win7-x64/native/mscorrc.debug.dll": {}, "runtimes/win7-x64/native/mscorrc.dll": {}, "runtimes/win7-x64/native/sos.dll": {} } }
The dependencies property lists dependencies (packages) for a particular package.
The runtimeTargets property is used in the deps file of the Portable application and determines the library file paths for a particular RID. Such RID-specific libraries are shipped with the Portable application in the runtimes folder .
The runtime and native properties contain relative paths of managed and native libraries, respectively. The resources property contains relative paths and localities of localized resource assemblies.
Paths are relative to the NuGet package cache, not the deps file.
You can add a third-party deps file by passing the value of the argument --additional-deps or the environment variable DOTNET_ADDITIONAL_DEPS .
This feature is available only for Portable applications.
The value of the argument can contain the full path to the deps-file, as well as the path to the directory where the common deps-files are located. Inside this directory, deps files must be located in the \ shared \ [FX name] \ [FX version] \ *. Deps structure. For example, shared \ Microsoft.NETCore.App \ 2.0.3 \ MyAdditional.deps.json .
This approach uses Visual Studio to implicitly add to the Application Insights project through a file.
C: \ Program Files \ dotnet \ additionalDeps \ Microsoft.AspNetCore.ApplicationInsights.HostingStartup \
shared \ Microsoft.NETCore.App \ 2.0.3 \ Microsoft.AspNetCore.ApplicationInsights.HostingStartup.deps.json
When dotnet.exe (MyApp.exe) defines application dependency paths, a list of runtime and native paths is compiled for each individual library.
If there is a library in runtimeTargets for a specific RID, it is added to the runtime or native list based on the specified assetType .
- RuntimeTarget section
contains the name and version of the target platform to execute. The targets section actually contains two elements - for compiling (without RID) and execution (always with RID). The runtimeTarget section is used for convenience and duplicates the value from the targets section so that dotnet.exe does not waste time processing the targets section. As already mentioned, for the Standalone application, the RID target OS is contained in the deps file of the application, and for Portable, in the deps file of the framework.
- Libraries section
defines a list of all application dependencies (in package ID / version format: {metadata}) and contains metadata about each of them. Metadata indicates:
- dependency type (project, package, reference),
- serviceable (for the package type only) —an indicator of whether the package is Serviceable (determines if the package build can be patched (replaced) by external services, Windows Update or .NET Core Servicing Index ).
- package hash (for package dependencies)
- other data
6. The process of launching a portable .NET Core application
The target computer must have a .NET Core Runtime installed that matches the configuration of the application being launched.
6.1. Application launchis performed using the multiplexer (muxer) from the command line (the same on any OS).
> dotnet path\to\MyApp.dll
dotnet.exe - renamed
corehost.exe , this program is the host process of any .NET Core-application, it starts the startup process.
6.2. [corehost] Search and download Framework Resolver (hostfxr.dll)At this point, dotnet.exe goes to the [own directory]
/ host / fxr / folder . For portable applications, this library is located in the shared folder
C: \ Program Files \ dotnet \ host \ fxr \ [FXR version] \ hostfxr.dll . If there are several versions, dotnet.exe will always use the latest one.
After downloading
hostfxr.dll (Framework Resolver), the startup process
goes into the scope of this library.
6.3. [hostfxr] Determining the execution mode (standalone, muxer, split / FX)The first task of hostfxr is to
determine the mode in which the host process will work and thus the type of application - Portable (FDD) or Standalone (SCD). In Portable (FDD) mode, it also determines whether it is a running application or an SDK command.
The type of execution (program or SDK command) is determined as follows :
- if among the arguments there is one whose value ends in .dll or .exe - the launch process will continue in the mode of execution of the specified file. If there is no such argument, the SDK will pass control. To do this, the dotnet.dll (as Portable application) will be launched from the [own directory] \ sdk \ [version] folder (if it exists), and the arguments of the current host process will be passed to this assembly.
Also for a Portable (FDD) application, the hostfxr determines the framework (.NET Core Runtime) from which the components will be loaded for execution.
The verification algorithm is very simple - if coreclr.dll or [AppName] .dll is
missing from the folder from which the [AppName] .exe multiplexer (in our case dotnet.exe) was launched, then the Portable application. If one of these two files exists, then there is a check - the Portable (split / FX) or Standalone application. If there is [AppName] .dll, then the application Standalone, otherwise - Portable (split / FX).
Split / FX mode is used to run xunit and means that the application is launched as Portable, with its own hostfxr.dll. This mode is not used in .NET Core 2.0.
The launch of a portable application can also be carried out in the so-called
Exec mode .
To do this, the start command must contain exec
C: \> dotnet exec ... as the first argument.
When running in this mode, you can explicitly specify the paths to the configuration files:
--depsfile <PAH>
--runtimeconfig <TH>which will be used instead of the files in the application folder.
6.4. [hostfxr] .NET Core Runtime DefinitionFirst of all, hostfxr detects and loads the deps and runtimeconfig configuration files. If nothing is overridden in the arguments, these files are taken from the application folder.
At the current stage, hostfxr determines (
according to the configuration file ) whether the application is Portable or Standalone.
After loading the configuration files and determining the mode, hostfxr
determines the framework folder (.NET Core Runtime).
To do this, hostfxr will first determine which versions are installed in the shared folder, and then select a release version from this list,
taking into account the values ​​in [AppName] .runtimeconfig.json .
When choosing a version, the
Roll Forward On No Candidate Fx parameter is taken into account, which indicates the severity of compliance with the specified version and those available on the machine.
6.5. [hostfxr] Search and download hostpolicy.dllAt the current stage, everything is ready to determine the paths of runtime components. This task is handled by the
hostpolicy.dll library, which is called the Host library.
The process of searching for hostpolicy.dll is in sequential checks of various locations. But first, the hostpolicy version is determined from the framework's deps file (eg
C: \ Program Files \ dotnet \ shared \ Microsoft.NETCore.App \ 2.0.0 \ Microsoft.NETCore.App.deps ). A package named
Microsoft .NETCore.DotNetHostPolicy will be found in this file and its version will be taken.
Then, a patch (replacement) of hostpolicy.dll (taking into account the version, if it was defined in the previous step, and RID) is searched in the .NET Core Servicing folder (for
Windows , in the C: \ Program Files [(x86)] \
coreservicing \ folder pkgs ). If such a file is found, it is downloaded for future use.
If the file was not found in the previous step, the hostpolicy.dll will be found in the framework folder.
Once hostpolicy.dll is defined, hostfxr loads this library and
transfers control to it .
6.6. [hostpolicy] Defining a dependency listThe
hostpolicy.dll library
is responsible for determining the absolute paths of all dependencies of the application.
First of all, hostpolicy
will create a component called Dependencies Resolver, which in turn will
load two deps files - the framework file and the application file.
First, the list is loaded from the deps file of the framework, where dependencies such as CoreCLR and CoreFX libraries will be defined. Then a list from the deps-file of the application, which shows the assembly of our application and their dependencies.
For each deps file, Dependency Resolver
lists all dependencies for the specified
runtimeTarget .
For each package, a list of files from all runtimeTargets (RID specific dependencies) sections is first compiled, then a list of all files from the native and runtime sections. Such a combined list of relative paths of all dependencies in a conditional format.
Package ID - RID - type of asset (runtime, native) - file paths are called Target assets.
After these two lists of dependency files (RID and non-RID) have been compiled, a process called
Reconciling libraries with targets is performed. It lies in the fact that for each package from the libraries section it is checked whether there are RID specific files that should be overridden by regular ones.
6.7. [hostpolicy] TPA, Core CLR and CLR Jit Path DefinitionNext, the Dependency resolver lists the absolute paths of the files of the managed assemblies — application dependencies. This list is called TPA (Trusted Platform Assemblies) and is passed to the Core CLR to configure the AppDomain. It also compiles a list of absolute paths of directories in which the rest of the dependency files are located (except coreclr, corejit).
The determination of the absolute paths of the managed assemblies is done by searching the files in the Probe paths. By default, there are two of them - the framework folder and the application folder, and they are based on the location of the deps-files. You can also add additional paths:
1) passing the argument
--additionalprobingpath , for example
--additionalprobingpath %UserProfile%\\.nuget\\packages
2) specifying in the file [AppName] .runtimeconfig.json (priority is lower than the argument), for example
{ "runtimeOptions": { "additionalProbingPaths": [ "C:\\Users\\username\\.nuget\\packages" ] } }
In the framework and application folder, the presence of the file is checked (provided that it was specified in the corresponding deps file) without regard to the relative path, in other directories with respect to the path, because these directories are considered as a cache of the NuGet package.
Search sequence:
- application folder;
- framework folder
- Probe paths
If the deps file of the application is missing, then all the files with the extension .ni.dll, .dll, .ni.exe, .exe from the application folder are included in the TPA.
After compiling the TPA list, the CoreCLR and CLRJit paths are determined.
If there is no application deps file, dotnet.exe will first try to find these libraries in [app directory] \ lib \. During normal execution, the paths are taken from the framework folder (discarding the relative path and taking only the file name).
The following CoreCLR settings are set:
- TRUSTED_PLATFORM_ASSEMBLIES - a list of absolute paths of all managed application libraries.
- NATIVE_DLL_SEARCH_DIRECTORIES - absolute paths of directories where native dependencies are found.
- PLATFORM_RESOURCE_ROOTS - absolute paths of directories where resource dependencies are found
- AppDomainCompatSwitch is a constant "UseLatestBehaviorWhenTFMNotSpecified".
- APP_CONTEXT_BASE_DIRECTORY - application folder.
- APP_CONTEXT_DEPS_FILES - absolute paths of the deps-files of the application and the framework.
- FX_DEPS_FILE - the absolute path of the framework's deps file.
- PROBING_DIRECTORIES - additional probing paths (if they were specified).
Then control passes to coreclr.dll.
7. The process of starting Standalone (SCD) .NET Core applications
The process of launching a Standalone application differs from Portable only in the initial stage, as well as in the location of the components, which by default should be located in the application folder.
7.1. Application launchperformed by running your own multiplexer MyApp.exe. In .NET Core <2.0, this multiplexer is the renamed common dotnet.exe multiplexer. Starting with .NET Core 2.0, a separate multiplexer apphost.exe is used (slightly modified version of dotnet.exe).
This file (apphost.exe) is supplied via NuGet in the Microsoft .NETCore.DotNetAppHost package.
Inside the file is a text placeholder (its value is the SHA-256 hash of the foobar string).
When executing the
dotnet build command, the value of the placeholder is changed to the name of the assembly being launched (for example, MyApp.dll), and apphost.exe is renamed to MyApp.exe. Thus, the executable file is bound to the assembly. When you run a .NET Core> = 2.0 application, this “binding” is first checked.
7.2. Startup processit is the same as with a Portable application, except that there is only one deps file and all dependencies are searched in the application folder or by the specified - additionalprobepaths.
8. To summarize
- The component model .NET Core (Runtime, BCL) consists entirely of NuGet packages.
- There are two types of deployment - FDD and SCD. Whenever possible, it is recommended to use Framework Dependent-Deployment to avoid difficulties with platform-dependent components and not to deliver unnecessary dependencies.
- As we could see, there are many opportunities to influence the launch process on the target machine, and if necessary, override / patch the dependency files, as well as add implicit (dynamically started) dependencies.
- Dependency manifest (*.deps.json) .
- --additional-deps --additionalprobepaths runtime- .
- Exec mode .
- Trace- , COREHOST_TRACE=1