📜 ⬆️ ⬇️

Structure and execution model of .NET Core applications

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:


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:


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 :


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:


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:


Documentation

Artifacts 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


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 :


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 launch
is 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 Definition
First 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.dll
At 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 list
The 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 Definition
Next, 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:


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:


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 launch
performed 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 process
it 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


Source: https://habr.com/ru/post/327686/


All Articles