.NET: Dependency Treatment

Who has not encountered problems due to assembly redirect? Most likely, everyone who developed a relatively large application will sooner or later face this problem.

Now I work at JetBrains, in the JetBrains Rider project, and am involved in the task of migrating Rider to .NET Core. Previously engaged in shared infrastructure in Circuit, a cloud-based application hosting platform.



Under the cutscene is the transcript of my report from the DotNext 2019 Moscow conference, where I talked about the difficulties when working with assemblies in .NET and showed with practical examples what happens and how to deal with it.


In all the projects where I worked as a .NET developer, I had to deal with various problems with connecting dependencies and loading assemblies. We’ll talk about this.

Post structure:


  1. Dependency Issues
  2. Strict rig loading

  3. .NET Core

  4. Debug assembly downloads


What are some dependency issues?


When they started developing the .NET Framework in the early 2000s, the Dependency hell problem was already known, when in all libraries developers allow breaking changes, and these libraries become incompatible for use with already compiled code. How to solve such a problem? The first solution is obvious. Always maintain backward compatibility. Of course, this is not very realistic, because breaking change is very easy to put into code. For example:



Breaking changes and .NET libraries

This is an example specific to .NET. We have a method, and we decided to add a parameter with a default value to it. The code will continue to compile if we reassemble it, but binary it will be two completely different methods: one method has zero arguments, the second method has one argument. If the developer inside the dependency broke backward compatibility in this way, then we will not be able to use the code that was compiled with this dependency on the previous version.

The second solution to dependency problems is to add versioning of libraries, assemblies - anything. There may be different versioning rules, the point is that we can somehow distinguish different versions of the same library from each other, and you can understand if the update will break or not break. Unfortunately, as soon as we introduce the versions, a different sort of problem appears.



Version hell is the inability to use a dependency that is binary compatible, but at the same time has a version that did not fit the runtime or another component that checks these versions. In .NET, a typical manifestation of version hell is FileLoadException, although the file lies on the disk, but for some reason it is not loaded with runtime.



In .NET, assemblies have many different versions - they tried to fix version hells in various ways, and see what happened. We have a package System.Collections.Immutable. Many people know him. He has the latest version of the NuGet package 1.6.0. It contains a library, an assembly with version 1.2.4.0. You have received that you do not have a build library version 1.2.4.0. How to understand that it lies in the 1.6.0 NuGet package? It will not be easy. In addition to Assembly Version, this library has several more versions. For example, Assembly File Version, Assembly Information Version. This NuGet package actually contains three different assemblies with the same versions (for different versions of the .NET Standard).

.NET Documentation
Opbuild standard

A lot of documentation has been written on how to work with assemblies in .NET. There is a .NET Guide for developing modern applications for .NET taking into account the .NET Framework, .NET Standard, .NET Core, open source and all that can be. About 30% of the entire document is devoted to loading assemblies. We will analyze specific problems and examples that may arise.

Why is all this necessary? Firstly, to avoid stepping on a rake. Secondly, you can make life easier for users of your libraries because with your library they will not have the dependency problems they are used to. It will also help you cope with the migration of complex applications to .NET Core. And to top it all off, you can become an SRE, this is a Senior (Binding) Redirect engineer, to which everyone in the team comes and asks how to write another redirect.

Strict assembly Loading


Strict assembly loading is the main problem that developers on the .NET Framework are facing. It is expressed in FileLoadException. Before moving on to Strict assembly loading itself, let me remind you of a few basic things.

When you build a .NET application, you end up with some artifact, which is usually located in Bin / Debug or Bin / Release, and contains a certain set of assembly assemblies and configuration files. Assemblies will refer to each other by name, Assembly name. It is important to understand that assembly links are located directly in the assembly that references this assembly; there are no magic configuration files where assembly references are written. Even though it may seem to you that such files exist. References are in the assemblies themselves in binary form.

In .NET, there is an assembly resolving process - this is when the assembly definition is already converted to a real assembly, which is on disk or loaded somewhere in memory. Assembly resolving is performed twice: at the build stage, when you have references in * .csproj, and at runtime, when you have references inside the assemblies, and by some rules they turn into assemblies that can be downloaded.

// Simple name
MyAssembly, Version = 6.0.0.0,
Culture = neutral, PublicKeyToken = null

// Strong name
Newtonsoft.Json, Version = 6.0.0.0,
Culture = neutral, PublicKeyToken = 30ad4fe6b2a6aeed // PublicKey


Let's move on to the problem. Assembly name there are two main types. The first kind of assembly name is Simple name. They are easy to identify by the fact that they have PublicKeyToken = null. There is a Strong name, it is easy to identify them by the fact that their PublicKeyToken is not null, but some value.



Let's take an example. We have a program that depends on the library with MyUtils utilities, and the version of MyUtils is 9.0.0.0. The same program has a link to another library. This library also wants to use MyUtils, but version 6.0.0.0. MyUtils version 9.0.0.0, and version 6.0.0.0 have PublicKeyToken = null, that is, they have a Simple name. Which version will fall into the binary artifact, 6.0.0.0 or 9.0.0.0? 9th version. Can MyLibrary use MyUtils version 9.0.0.0, which got into the binary artifact?



In fact, it can, because MyUtils has a Simple name and, accordingly, the Strict assembly loading does not exist for it.



Another example. Instead of MyUtils, we have a full library from NuGet, which has a Strong name. Most libraries in NuGet have a Strong name.



At the build stage, version 9.0.0.0 is copied to BIN, but in runtime we get the famous one FileLoadException. In order for MyLibrary, which wants version 6.0.0.0 to Newtonsoft.Json, to be able to use version 9.0.0.0, you have to go and write Binding redirect to App.config.

Binding redirects





Redirecting assembly versions

It states that an assembly with such a name and such publicKeyToken should be redirected from such a range of versions to such a range of versions. It seems to be a very simple record, but nevertheless it is located here App.config, but could be in other files. There is a file machine.configinside the .NET Framework, inside the runtime, in which some standard set of redirects is defined, which may differ from version to version of the .NET Framework. It may happen that on 4.7.1 nothing works for you, but on 4.7.2 it already works, or vice versa. You need to keep in mind that redirects can come not only from yours .App.config, and this should be taken into account when debugging.

We simplify the writing of redirects


No one wants to write Binding redirects with their hands. Let's give this task to MSBuild!



How to enable and disable automatic binding redirection

A few tips on how to simplify working with Binding redirect. Tip One: Enable Binding redirect auto-generation in MSBuild. Turned on by property in *.csproj. When building a project, it will fall into a binary artifact App.config, which indicates redirects to versions of libraries that are in the same artifact. This only works for running applications, console application, WinExe. For libraries, this does not work, because for librariesApp.configmost often it’s simply not relevant, because it is relevant for an application that launches and loads assemblies itself. If you made a config for the library, then in the application some dependencies may also differ from those that were when building the library, and it turns out that the config for the library does not make much sense. Nevertheless, sometimes for libraries configs still make sense.



The situation when we write tests. Tests are usually found in ClassLibrary and they also need redirects. Test frameworks are able to recognize that the library with tests has a dll-config, and exchange the redirects that are in them for the code from the tests. You can generate these redirects automatically. If we have an old format*.csproj, not SDK-style, you can go the simple way, change the OutputType to Exe and add an empty entry point, this will force MSBuild to generate redirects. You can go the other way and use the hack. You can add another property to *.csproj, which makes MSBuild consider that for this OutputType you still need to generate Binding redirects. This method, although it looks like a hack, will allow you to generate redirects for libraries that cannot be redone in Exe, and for other types of projects (except tests).

For the new format, *.csprojredirects will be generated themselves if you use modern Microsoft.NET.Test.Sdk.

Third tip: do not use Binding redirect generation with NuGet. NuGet has the ability to generate Binding redirect for libraries that pass from packages to the latest versions, but this is not the best option. All of these redirects will have to be added to App.configand committed, and if you generate redirects using MSBuild, then redirects are generated during the build. If you commit them, you may have merge conflicts. You yourself can simply forget to update the Binding redirect in the file, and if they are generated during the build, you will not forget.



Resolve Assembly Reference
Generate Binding Redirects

Homework for those who want to better understand how the generation of Binding redirects works: find out how it works, see this in the code. Go to the .NET directory, go bump everywhere with the name property, which is used to enable generation. This is generally such a common approach, if there is some strange property for MSBuild, you can go and take advantage of its use. Fortunately, property is usually used in XML configs, and you can easily find their use.

If you examine what is in these XML targets, you will see that this property triggers two MSBuild tasks. The first task is called ResolveAssemblyReferences, and it generates a set of redirects that are written to files. The second task GenerateBindingRedirectswrites the results of the first task toApp.config. There is XML logic that slightly corrects the operation of the first task and removes some unnecessary redirects, or adds new ones.

Alternative to XML Configs


It is not always convenient to keep redirects in the XML config. We may have a situation where the application downloads the plugin, and this plugin uses other libraries that require redirects. In this case, we may not be aware of the set of redirects that we need, or we may not want to generate XML. In such a situation, we can create an AppDomain and, when it is created, still transfer to it where the XML with the necessary redirects is located. We can also handle assembly loading errors right in runtime. Rantime .NET gives such an opportunity.

AppDomain.CurrentDomain.AssemblyResolve += (sender, eventArgs) => 
{ 
   var name = eventArgs.Name; 
   var requestingAssembly = eventArgs.RequestingAssembly; 
   
   return Assembly.LoadFrom(...); // PublicKeyToken should be equal
};


It has an event, it is called CurrentDomain.AssemblyResolve. By subscribing to this event, we will receive errors about all failed assembly downloads. We get the name of the assembly that did not load, and we get the assembly assembly that requested the first assembly to load. Here we can manually load the assembly from the right place, for example, dropping the version, just taking it from the file, and returning this event from the handler. Or return null if we have nothing to return, if we cannot load the assembly. PublicKeyToken should be the same, assemblies with different PublicKeyToken in no way are friends with each other.



This event applies to only one application domain. If our plugin creates an AppDomain inside itself, then this redirect in the runtime will not work in them. You need to somehow subscribe to this event in all the AppDomain that the plugin created. We can do this using the AppDomainManager.

AppDomainManager is a separate assembly that contains a class that implements a specific interface, and one of the methods of this interface will allow you to initialize any new AppDomain that is created in the application. Once the AppDomain is created, this method will be called. In it you can subscribe to this event.

Strict assembly loading & .NET Core


In .NET Core there is no problem called “Strict assembly loading”, which is due to the fact that signed assemblies require exactly the version that was requested. There is another requirement. For all assemblies, regardless of whether they are signed by Strong name or not, it is checked that the version that was loaded in runtime is greater than or equal to the previous one. If we are in a situation of an application with plugins, we may have such a situation that the plugin was built, for example, from a new version of the SDK, and the application into which it is downloaded uses the old version of the SDK so far, and instead of falling apart, we can also subscribe to this event, but already in .NET Core, and also load the assembly that we have. We can write this code:
AppDomain.CurrentDomain.AssemblyResolve += (s, eventArgs) => 
{ 
     CheckForRecursion(); 
     var name = eventArgs.Name;
     var requestingAssembly = eventArgs.RequestingAssembly; 
    
     name.Version = new Version(0, 0); 
     
     return Assembly.Load(name); 
};


We have the name of the assembly that did not boot, we nullify the version and call it Assembly.Loadfrom the same version. There will be no recursion here, because I already checked the recursion.



It was necessary to download MyUtils version 0.0.2.0. In BIN, we have MyUtils version 0.0.1.0. We made a redirect from version 0.0.2.0 to version 0.0. Version 0.0.1.0 will not load with us. An exit will fly out for us that it was not possible to load the assembly with version 0.0.2 16–1 . 2 16–1 .

new Version(0, 0) == new Version(0, 0, -1, -1) 

class Version { 
     readonly int _Build; 
     readonly int _Revision; 
     readonly int _Major; 
     readonly int _Minor; 
} 
(ushort) -1 == 65535


In the Version class, not all components are mandatory, and instead of optional components –1 are stored, but somewhere inside, an overflow occurs, and the very 2 16–1 are obtained . If interested, you can try to find exactly where the overflow occurs.



If you work with reflection assemblies and want to get all types, it may turn out that not all types can get your GetTypes method. An assembly has a class that inherits from another class that is in an assembly that is not loaded.

static IEnumerable GetTypesSafe(this Assembly assembly) 
{ 
    try 
    { 
        return assembly.GetTypes(); 
    }
    catch (ReflectionTypeLoadException e) 
   { 
        return e.Types.Where(x => x != null); 
    } 
}



In this case, the problem will be that a ReflectionTypeLoadException will be thrown. Inside ReflectionTypeLoadExceptionthere is a property in which there are those types that still managed to be loaded. Not all popular libraries take this thing into account. AutoMapper, at least one of its versions, if faced with ReflectionTypeLoadException, just fell, instead of going and picking the types from the inside of the exception.

Strong naming


Strong-named assemblies

Let's talk about what causes Strict assembly loading, this is Strong name.
Strong Name is the signature of the assembly by some private key using asymmetric encryption. PublicKeyToken is the public key hash of this assembly.

Strong Naming allows you to distinguish between different assemblies that have the same name. For example, MyUtils is not some unique name, there may be several assemblies with that name, but if you sign Strong name, they will have different PublicKeyToken and we can distinguish them in this way. Strong name is required for some assembly loading scenarios.

For example, in order to install an assembly in the Global Assembly Cache or to download several versions of side-by-side at once. Most importantly, strong named assemblies can only reference other strong named assemblies. Since some users want to sign their builds with Strong name, the library developers also sign their libraries, so that it is easier for users to install them, so that users do not have to re-sign these libraries.

Strong name: Legacy?


Strong naming and .NET libraries

Microsoft explicitly says on MSDN that you should not use Strong name for security purposes, that they provide only to distinguish different assemblies with the same name. The assembly key cannot be changed in any way; if you changed it, then you will break redirects to all your users. If you have a private part of the key for Strong name leaked to public access, then you can’t withdraw this signature in any way. The SNK file format in which Strong name is located does not provide such an opportunity, and other formats for storing keys at least contain a link to the CRL Certificate Revocation List, by which it can be understood that this certificate is no longer valid. There is nothing like that in SNK.

The Open-source guide has the following recommendations. Firstly, additionally for security purposes use other technologies. Secondly, if you have an open source library, it is generally suggested that you commit the private part of the key to the repository, so that it is easier for people to fork your library, rebuild it and put it in a ready-made application. Thirdly, never change Strong name. Too destructive. Despite the fact that it is too destructive and is written about it in the Open-source guide, Microsoft sometimes has problems with its own libraries.



There is a library called System.Reactive. Previously, these were several NuGet packages, one of them is Rx-Linq. This is just an example, the same for the rest of the packages. In the second version, it was signed with a Microsoft key. In the third version, he moved to the repository in the github.com/dotnet project and began to have a .NET Foundation signature. The library, in fact, has changed Strong name. The NuGet package was renamed, but the assembly is called inside exactly the same as before. How to redirect from the second version to the third? This redirect can not be done.

Strong name validation


How to: Disable strong name bypass feature

Another argument that Strong name is already something that is a thing of the past and remains purely formal is that they are not validated. We have a signed assembly and we want to fix some kind of bug in it, but we do not have access to the sources. We can just take dnSpy - this is a utility that allows you to decompile and fix already compiled assemblies. Everything will work for us. Because by default, Strong name validation bypass is enabled, that is, it only checks that the PublicKeyToken is equal, and the integrity of the signature itself is not checked. There may be environmental studies in which the signature is still verified, and here a vivid example is IIS. Signature integrity is checked on IIS (Strong name validation bypass is disabled by default), and everything will break if we edit the signed assembly.

Addition:You can disable signature verification for the assembly using public sign. With it, only the public key is used for signing, which ensures the safety of the assembly name. The public keys used by Microsoft are posted here .
In Rider, public sign can be enabled in the project properties.





When to change fileassembly versions

The open-source guide also offers some Versioning policy, the purpose of which is to reduce the number of necessary Binding redirects and changes to them for users on the NET Framework. This Versioning policy is that we should not change Assembly Version constantly. This, of course, can lead to problems with installation in the GAC, so that the installed native image may not correspond to the assembly and you will have to perform JIT compilation again, but, in my opinion, this is less evil than problems with versioning. In the case of CrossGen, native assemblies are not installed globally - there will be no problems.

For example, the NuGet package Newtonsoft.Json, it has several versions: 12.0.1, 12.0.2, and so on - all of these packages have an assembly with version 12.0.0.0. The recommendation is that the Assembly Version should be updated when a major version of the NuGet package changes.

findings


Follow the tips for the .NET Framework: generate redirects manually and try to use the same version of dependencies in all projects in your solution. This should significantly minimize the number of redirects. You need Strong naming only if you have a specific build loading scenario where it is needed, or you are developing a library and want to simplify the life for users who really need Strong naming. Do not change Strong name.

.NET Standard


We pass to .NET Standard. It is pretty closely related to Version hell in the .NET Framework. .NET Standard is a tool for writing libraries that are compatible with various implementations of the .NET platform. Implementations refer to the .NET Framework, .NET Core, Mono, Unity, and Xamarin.



* Link to documentation

This is the .NET Standard support table for various versions of different versions of runtimes. And here we can see that the .NET Framework in no way supports the .NET Standard version 2.1. The release of the .NET Framework, which will support the .NET Standard 2.1 and later, is not yet planned. If you are developing a library and want it to work for users on the .NET Framework, you will have to have a target for .NET Standard 2.0. Besides the fact that the .NET Framework does not support the latest version of the .NET Standard, let's pay attention to the asterisk. The .NET Framework 4.6.1 supports .NET Standard 2.0, but with an asterisk. There is such a footnote directly in the documentation, where did I get this table.



Consider an example project. An application on the .NET Framework that has one dependency targeting the .NET Standard. Something like this: ConsoleApp and ClassLibrary. Target Library .NET Standard. When we put this project together, it will be like this in our BIN.



We will have a hundred DLLs there, of which only one related to the application, everything else came in order to support the .NET Standard. The fact is that .NET Standard 2.0 appeared later than the .NET Framework 4.6.1, but at the same time they turned out to be API compatible, and the developers decided to add Standard 2.0 support to .NET 4.6.1. We did it not natively (by inclusion netstandard.dllin the runtime itself), but in such a way that .NET Standard * .dll and all other assembly facades are placed directly in BIN.



If we look at the dependencies of the version of the .NET Framework that we target and the number of libraries that fell into the BIN, we will see that there are not so many of them in 4.7.1, and since 4.7.2 there are no additional libraries at all, and .NET Standard is supported there natively.



This is a tweet from one of the .NET developers, which describes this problem and recommends using the .NET Framework version 4.7.2 if we have .NET Standard libraries. Not even with version 2.0 here, but with version 1.5.

findings


If possible, raise the Target Framework in your project to at least 4.7.1, preferably 4.7.2. If you are developing a library to make life easier for library users, make a separate Target for the .NET Framework, it will avoid a huge number of dlls that can conflict with something.

.NET Core


Let's start with a general theory. We will discuss how we launched JetBrains Rider on .NET Core, and why we should talk about it at all. Rider is a very large project, it has a huge enterprise solution with a large number of different projects, a complex system of dependencies, you cannot just take it and migrate to another runtime at one time. To do this, we have to use some hacks, which we will also analyze.

.NET Core application


What does a typical .NET Core application look like? Depends on how exactly it is deployed, what it is ultimately going to. We can have several scenarios. The first is a Framework-dependent deployment. This is the same as in the .NET Framework when the application uses the runtime pre-installed on the computer. It can be a Self-contained deployment, this is when the application carries a runtime. And there may be a Single-file deployment, this is when we get one exe-file, but in the case of .NET Core inside this exe-file there is an artifact of Self-contained application, this is a self-extracting archive.



We will only consider Framework-dependent deployment. We have a dll with the application, there are two configuration files, the first of which is required, this runtimeconfig.jsonanddeps.json. Starting with .NET Core 3.0, an exe file is generated that is needed to make the application more convenient to run, so that you do not need to enter the .NET command if we are on Windows. Dependencies fall into this artifact, starting with .NET Core 3.0, in .NET Core 2.1 you need to publish or use another property in *.csproj.

Shared frameworks, .runtimeconfig.json





.runtimeconfig.jsoncontains the runtime settings that are needed to run it. It indicates under which Shared Framework the application will be launched, and it looks like this. We indicate that the application will run under “Microsoft.NETCore.App” version 3.0.0, there may be other Shared Framework. Other settings may also be here. For example, you can enable the server Garbage collector.



.runtimeconfig.jsongenerated during the assembly of the project. And if we want to include the server GC, then we need to somehow modify this file in advance, even before we assemble the project, or add it by hand. You can add your settings here like this. We can either include property in *.csproj, if such property is provided by .NET developers, or if property is not provided, we can create a file calledruntimeconfig.template.jsonand write the necessary settings here. During assembly, other necessary settings will be added to this template, for example, the same Shared Framework.



The Shared Framework is a set of runtime and libraries. In fact, the same thing as the .NET Framework runtime, which used to be just installed once on the machine and for all was one version. Shared Framework, and, unlike a single .NET Framework runtime, can be versioned, different applications can use different versions of installed runtimes. Also Shared Framework can be inherited. The Shared Framework itself can be viewed in such locations on the disk as are generally installed on the system.



There are several standard Shared Framework, for example, Microsoft.NETCore.App, which runs conventional console applications, AspNetCore.App, for web applications, and WindowsDesktop.App, the new Shared Framework in .NET Core 3, which runs desktop applications. on Windows Forms and WPF. The last two Shared Framework essentially complement the first one needed for console applications, that is, they do not carry a whole new runtime, but simply supplement the existing one with the necessary libraries. This inheritance looks like there are also in the Shared Framework directories runtimeconfig.jsonin which the base Shared Framework is specified.

Dependency manifest ( .deps.json)



Default probing - .NET Core The

second configuration file is this .deps.json. This file contains a description of all the dependencies of the application or the Shared Framework, or the library, the libraries .deps.jsonalso have it. It contains all the dependencies, including transitive ones. And the behavior of the .NET Core runtime differs depending on whether .deps.jsonthe application has it or not. If .deps.jsonnot, the application will be able to load all the assemblies that are in its Shared Framework or in its BIN directory. If there .deps.jsonis, then validation is enabled. If one of the assemblies that is listed in .deps.jsonis not, then the application simply will not start. You will see the error presented above. If the application tries to load some assembly in runtime, which.deps.json if, for example, using Assembly load methods or during the resolve process of assemblies, you will see an error very similar to Strict assembly loading.

Jetbrains rider


Rider is a .NET IDE. Not everyone knows that Rider is an IDE consisting of a frontend based on IntelliJ IDEA and written in Java and Kotlin, and a backend. The backend is essentially R #, which can communicate with IntelliJ IDEA. This backend is a cross-platform .NET application now.
Where does it run? Windows uses the .NET Framework, which is installed on the user's computer. On other information systems, on Linux and Mac, Mono is used.

This is not an ideal solution when there are different runtimes everywhere, and I want to come to the next state so that Rider runs on .NET Core. In order to improve performance, because in .NET Core all the latest features are associated with this. To reduce memory consumption. Now there is a problem with how Mono works with memory.

Switching to .NET Core will allow you to abandon legacy, unsupported technologies and allow to fix some fixes for the problems that were found in runtime. Switching to .NET Core will allow you to control the version of the runtime, that is, Rider will no longer run on the .NET Framework that is installed on the user's computer, but on a specific version of .NET Core, which can be banned, as a self-contained deployment. The transition to .NET Core will eventually allow the use of new APIs that are imported specifically in Core.

Now the goal is to launch a prototype, launch it, just to check how it will work, what are the potential points of failure, which components will have to be rewritten again, which will require global processing.

Features that make translating Rider to .NET Core difficult


Visual Studio, even if R # is not installed in it, crashes from Out Of Memory on large solutions, inside which there are projects with SDK-style * .csproj . SDK-style * .csproj is one of the main conditions for a full .NET Core relocation.

This is a problem because Rider is based on R #, they live in the same repository, R # developers want to use Visual Studio to develop their own product in their product in order to make it food. In R # there are links specific libraries for the framework with which you need to do something. On Windows, we can use the Framework for desktop applications, and on Linux and Mac, Mock is already used for Windows libraries with minimal functionality.

Decision


We decided to stay on the old ones for now *.csproj, assemble under the full Framework, but since the assemblies of the Framework and Core are binary compatible, run them on Core. We do not use incompatible features, add all the necessary configuration files manually and download special versions of dependencies for .NET Core, if any.

What hacks did you have to go to?


One hack: we want to call a method that is available only in the Framework, for example, this method is needed in R #, but not on Core. The problem is that if there is no method, then the method that calls it during JIT compilation will fall earlier MissingMethodException. That is, a method that does not exist has ruined the method that calls it.

static void Method() { 
  if (NetFramework) 
     CallNETFrameworkOnlyMethod();

  ... 
} 
[MethodImpl(MethodImplOptions.NoInlining)] 
static void CallNETFrameworkOnlyMethod() { 
  NETFrameworkOnlyMethod(); 
}


The solution is here: we make calls to incompatible methods into separate methods. There is one more problem: such a method may become inline, therefore we mark it with an attribute NoInlining.

Hack number two: we need to be able to load assemblies in relative paths. We have one assembly for the Framework, there is a special version for .NET Core. How do we download the .NET Core version for .NET Core?



They will help us .deps.json. Let's look at .deps.jsonthe System.Diagnostics.PerformanceCounter library. Such a library is remarkable in terms of its.deps.json. It has a runtime section, in which one version of the library with its relative path is indicated. This library, the assembly will be loaded on all runtimes, and it just throws the executions. If, for example, it loads on Linux, the PerformanceCounter does not work on design on Linux, and a PlatformNotSupportedException flies from there. There is also .deps.jsona runtimeTargets section in this and here is already indicated the version of this assembly specifically for Windows, where PerformanceCounter should work.

If we take the runtime section and write in it the relative path to the library we want to load, this will not help us. The runtime section actually sets the relative path inside the NuGet package, and not relative to the BIN. If we look for this assembly in BIN, only the file name will be used from there. The runtimeTargets section already contains an honest relative path, an honest path relative to BIN. We will prescribe a relative path for our assemblies in the runtimeTargets section. Instead of the runtime identifier, which is “win” here, we can take another that we like. For example, we will write the runtime identifier “any”, and this assembly will be loaded generally on all platforms. Or we will write “unix”, and it will boot on Linux, and on Mac, and so on.

Next hack: we want to download on Linux and on Mac Mock to build WindowsBase. The problem is that the assembly named WindowsBase is already present in the Shared Framework Microsoft.NETCore.App, even if we are not on Windows. On the Windows Shared Framework, Microsoft.WindowsDesktop.AppWindowsBase redefines the version that is in NETCore.App. Let's look at .deps.jsonthese Framework, more precisely at those sections that describe WindowsBase.



Here is the difference:



If some library conflicts and is present in several .deps.json, then the maximum of them is selected for the pair consisting of assemblyVersionand fileVersion. The .NET guide says that fileVersionit’s only needed to show it in Windows Explorer, but it’s not, it falls into.deps.json. This is the only case that I know of when the version prescribed in .deps.json, assemblyVersionand fileVersion, are actually used. In all other cases, I saw a behavior that no matter what versions .deps.jsonwere written in, the assembly would continue to load anyway.



Fourth hack. Task: we have a .deps.json file for the previous two hacks, and we need it only for specific dependencies. Since they are .deps.jsongenerated in semi-manual mode, we have a script that, according to some description of what should get there, generates it during the build, we want to keep this as .deps.jsonminimal as possible so that we can understand what is in it. We want to disable validation and allow the download of assemblies that are in the BIN but are not described in .deps.json.

Solution: enable custom configuration in runtimeconfig. This setting is actually needed for backward compatibility with .NET Core 1.0.

findings


So, .runtime.jsonand .deps.jsonon .NET Core - these are kind of analogues App.config. App.configlet you do the same things, for example, load assemblies in relative ways. Using .deps.json, rewriting it manually, you can customize the loading of assemblies on .NET Core, if you have a very complex scenario.

Debug assembly downloads


I talked about some types of problems, so you need to be able to debug problems with loading assemblies. What can help with this? First, runtimes write logs about how they load assemblies. Secondly, you can look more closely at the executions that fly to you. You can also focus on runtime events.

Fusion logs





Back to Basics: Using Fusion Log Viewer To Debug Obscure Errors
Fusion

The mechanism for loading assemblies in the .NET Framework is called Fusion, and it knows how to log what it did to the disk. To enable logging, you need to add special settings to the registry. This is not very convenient, so it makes sense to use utilities, namely Fusion Log Viewer and Fusion ++. Fusion Log Viewer is a standard utility that comes with Visual Studio and can be launched from the Visual Studio command line, Visual Studio Developer Command Prompt. Fusion ++ is an open source analogue of this tool with a nicer interface.



Fusion Log Viewer looks like this. This is worse than WinDbg because this window does not even stretch. Nevertheless, you can pierce the checkmarks here, although it is not always obvious which set of checkmarks is correct.



Fusion ++ has one “Start Logging” button, and then the “Stop Logging” button appears. In it, you can see all the records about loading assemblies, read the logs about what exactly was happening. These logs look something like this in a concise way.



This is an exemption from Strict assembly loading. If we look at the Fusion logs, we will see that we needed to download version 9.0.0.0 after we processed all the configs. We found a file in which it is suspected that we have the assembly we need. We saw that version 6.0.0.0 is in this file. We have a warning that we compared the full names of the assemblies, and they differ in the major version. And then an error occurred - version mismatch.

Runtime events





Logging Runtime Events

On Mono, you can enable logging using environment variables, and the logs will eventually be written to stdoutand stderr. Not so convenient, but the solution is working.



Default probing - .NET Core
Documentation / design docs / host tracing

. .NET Core also has a special environment variable COREHOST_TRACEthat includes logging in stderr. With .NET Core 3.0, you can write logs to a file by specifying the path to it in a variable COREHOST_TRACEFILE.


There is an event that fires when the assemblies fail to load. This is an event AssembleResolve. There is a second useful event, this FirstChanceException. You can subscribe to it and get an error about loading assemblies, even if someone wrote try..catch and missed all the executions in the place whereFileLoadExceptionoccurred. If the application has already been compiled, you can start it perfview, and it can monitor .NET executions, and there you can find those related to download files.

findings


Transfer work to tools, to development tools, to an IDE, to MSBuild, which allows you to generate redirects. You can switch to .NET Core, then you will forget what Strict Assembly Loading is, and you will be able to use the new API just like we want to achieve it in Rider. If you connect the .NET Standard library, then raise the target version of the .NET Framework to at least 4.7.1. If you seem to be in a hopeless situation, then look for hacks, use them, or come up with your own hacks for hopeless situations. And arm yourself with debugging tools.

I strongly recommend that you read the following links:



DotNext 2020 Piter . , 8 JUG Ru Group.

All Articles