This topic is well documented from Microsoft and I only want to summarize the key points.

You will find the complete documentation under

How the Runtime Locates Assemblies
https://docs.microsoft.com/en-us/dotnet/framework/deployment/how-the-runtime-locates-assemblies


To successfully deploy your .NET Framework application, you must understand how the common language runtime locates and binds to the assemblies that make up your application. By default, the runtime attempts to bind with the exact version of an assembly that the application was built with. This default behavior can be overridden by configuration file settings.

The common language runtime performs a number of steps when attempting to locate an assembly and resolve an assembly reference. Each step is explained in the following sections. The term probing is often used when describing how the runtime locates assemblies; it refers to the set of heuristics used to locate the assembly based on its name and culture.

You can view binding information in the log file using the Assembly Binding Log Viewer (Fuslogvw.exe), which is included in the Windows SDK.




Initiating the Bind

The process of locating and binding to an assembly begins when the runtime attempts to resolve a reference to another assembly. This reference can be either static or dynamic. The compiler records static references in the assembly manifest’s metadata at build time. Dynamic references are constructed on the fly as a result of calling various methods, such as Assembly.Load.

The preferred way to reference an assembly is to use a full reference, including the assembly name, version, culture, and public key token (if one exists). The runtime uses this information to locate the assembly, following the steps described later in this section. The runtime uses the same resolution process regardless of whether the reference is for a static or dynamic assembly.

You can also make a dynamic reference to an assembly by providing the calling method with only partial information about the assembly, such as specifying only the assembly name. In this case, only the application directory is searched for the assembly, and no other checking occurs. You make a partial reference using any of the various methods for loading assemblies such as Assembly.Load or AppDomain.Load.

Finally, you can make a dynamic reference using a method such as Assembly.Load and provide only partial information; you then qualify the reference using the <qualifyAssembly> element in the application configuration file. This element allows you to provide the full reference information (name, version, culture and, if applicable, the public key token) in your application configuration file instead of in your code. You would use this technique if you wanted to fully qualify a reference to an assembly outside the application directory, or if you wanted to reference an assembly in the global assembly cache but you wanted the convenience of specifying the full reference in the configuration file instead of in your code.

This type of partial reference should not be used with assemblies that are shared among several applications. Because configuration settings are applied per application and not per assembly, a shared assembly using this type of partial reference would require each application using the shared assembly to have the qualifying information in its configuration file.



The runtime uses the following steps to resolve an assembly reference:

  • 1. Determines the correct assembly version by examining applicable configuration files, including the application configuration file, publisher policy file, and machine configuration file. If the configuration file is located on a remote machine, the runtime must locate and download the application configuration file first.
  • 2. Checks whether the assembly name has been bound to before and, if so, uses the previously loaded assembly. If a previous request to load the assembly failed, the request is failed immediately without attempting to load the assembly.

    The caching of assembly binding failures is new in the .NET Framework version 2.0.
  • 3. Checks the global assembly cache. If the assembly is found there, the runtime uses this assembly.
  • 4. Probes for the assembly using the following steps:

    a. If configuration and publisher policy do not affect the original reference and if the bind request was created using the Assembly.LoadFrom method, the runtime checks for location hints.

    b. If a codebase is found in the configuration files, the runtime checks only this location. If this probe fails, the runtime determines that the binding request failed and no other probing occurs.

    c. Probes for the assembly using the heuristics described in the probing section. If the assembly is not found after probing, the runtime requests the Windows Installer to provide the assembly. This acts as an install-on-demand feature.

There is no version checking for assemblies without strong names, nor does the runtime check in the global assembly cache for assemblies without strong names.

Note!
https://docs.microsoft.com/en-us/dotnet/framework/app-domains/install-assembly-into-gac
You can install only strong-named assemblies into the global assembly cache.


Step 1: Examining the Configuration Files

Assembly binding behavior can be configured at different levels based on three XML files:

  • Application configuration file.
  • Publisher policy file.
  • Machine configuration file.

These files follow the same syntax and provide information such as binding redirects, the location of code, and binding modes for particular assemblies. Each configuration file can contain an <assemblyBinding> element that redirects the binding process. The child elements of the <assemblyBinding> element include the <dependentAssembly> element. The children of <dependentAssembly> element include the <assemblyIdentity> element, the <bindingRedirect> element, and the <codeBase> element.


Configuration information can be found in the three configuration files; not all elements are valid in all configuration files. For example, binding mode and private path information can only be in the application configuration file. For a complete list of the information that is contained in each file, see Configuring Apps by Using Configuration Files.


Application Configuration File

First, the common language runtime checks the application configuration file for information that overrides the version information stored in the calling assembly’s manifest. The application configuration file can be deployed with an application, but is not required for application execution. Usually the retrieval of this file is almost instantaneous, but in situations where the application base is on a remote computer, such as in an Internet Explorer Web-based scenario, the configuration file must be downloaded.

For client executables, the application configuration file resides in the same directory as the application’s executable and has the same base name as the executable with a .config extension. For example, the configuration file for C:\Program Files\Myapp\Myapp.exe is C:\Program Files\Myapp\Myapp.exe.config. In a browser-based scenario, the HTML file must use the <link> element to explicitly point to the configuration file.

The following code provides a simple example of an application configuration file. This example adds a TextWriterTraceListener to the Listeners collection to enable recording debug information to a file.

<configuration>
   <system.diagnostics>
      <trace useGlobalLock="false" autoflush="true" indentsize="0">
         <listeners>
            <add name="myListener" type="System.Diagnostics.TextWriterTraceListener, system version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" initializeData="c:\myListener.log" />
         </listeners>
      </trace>
   </system.diagnostics>
</configuration>



Publisher Policy File

Second, the runtime examines the publisher policy file, if one exists. Publisher policy files are distributed by a component publisher as a fix or update to a shared component. These files contain compatibility information issued by the publisher of the shared component that directs an assembly reference to a new version. Unlike application and machine configuration files, publisher policy files are contained in their own assembly that must be installed in the global assembly cache.

The following is an example of a Publisher Policy configuration file:

<configuration>
    <runtime>
        <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">

            <dependentAssembly>
                <assemblyIdentity name="asm6" publicKeyToken="c0305c36380ba429" />
                <bindingRedirect oldVersion="3.0.0.0" newVersion="2.0.0.0"/>
            </dependentAssembly>

        </assemblyBinding>
    </runtime>
</configuration>


To create an assembly, you can use the Al.exe (Assembly Linker) tool with a command such as the following:

Al.exe /link:asm6.exe.config /out:policy.3.0.asm6.dll /keyfile: compatkey.dat /v:3.0.0.0


compatkey.dat is a strong-name key file. This command creates a strong-named assembly you can place in the global assembly cache.

Publisher policy affects all applications that use a shared component.


The publisher policy configuration file overrides version information that comes from the application (that is, from the assembly manifest or from the application configuration file). If there is no statement in the application configuration file to redirect the version specified in the assembly manifest, the publisher policy file overrides the version specified in the assembly manifest. However, if there is a redirecting statement in the application configuration file, publisher policy overrides that version rather than the one specified in the manifest.

A publisher policy file is used when a shared component is updated and the new version of the shared component should be picked up by all applications using that component. The settings in the publisher policy file override settings in the application configuration file, unless the application configuration file enforces safe mode.


Machine Configuration File

Third, the runtime examines the machine configuration file. This file, called Machine.config, resides on the local computer in the Config subdirectory of the root directory where the runtime is installed. This file can be used by administrators to specify assembly binding restrictions that are local to that computer. The settings in the machine configuration file take precedence over all other configuration settings; however, this does not mean that all configuration settings should be put in this file. The version determined by the administrator policy file is final, and cannot be overridden. Overrides specified in the Machine.config file affect all applications. For more information about configuration files, see Configuring Apps by using Configuration Files.



Step 2: Checking for Previously Referenced Assemblies

If the requested assembly has also been requested in previous calls, the common language runtime uses the assembly that is already loaded. This can have ramifications when naming assemblies that make up an application. For more information about naming assemblies, see Assembly Names.

If a previous request for the assembly failed, subsequent requests for the assembly are failed immediately without attempting to load the assembly. Starting with the .NET Framework version 2.0, assembly binding failures are cached, and the cached information is used to determine whether to attempt to load the assembly.

To revert to the behavior of the .NET Framework versions 1.0 and 1.1, which did not cache binding failures, include the <disableCachingBindingFailures> Element in your configuration file.



Step 3: Checking the Global Assembly Cache

For strong-named assemblies, the binding process continues by looking in the global assembly cache. The global assembly cache stores assemblies that can be used by several applications on a computer.

All assemblies in the global assembly cache must have strong names.



Step 4: Locating the Assembly through Codebases or Probing

After the correct assembly version has been determined by using the information in the calling assembly’s reference and in the configuration files, and after it has checked in the global assembly cache (only for strong-named assemblies), the common language runtime attempts to find the assembly. The process of locating an assembly involves the following steps:

  • 1. If a <codeBase> element is found in the application configuration file, the runtime checks the specified location. If a match is found, that assembly is used and no probing occurs. If the assembly is not found there, the binding request fails.

  • 2. The runtime then probes for the referenced assembly using the rules specified later in this section.

If you have multiple versions of an assembly in a directory and you want to reference a particular version of that assembly, you must use the <codeBase> element instead of the privatePath attribute of the <probing> element. If you use the <probing> element, the runtime stops probing the first time it finds an assembly that matches the simple assembly name referenced, whether it is a correct match or not. If it is a correct match, that assembly is used. If it is not a correct match, probing stops and binding fails.



Locating the Assembly through Codebases

Codebase information can be provided by using a <codeBase> element in a configuration file. This codebase is always checked before the runtime attempts to probe for the referenced assembly. If a publisher policy file containing the final version redirect also contains a <codeBase> element, that <codeBase> element is the one that is used. For example, if your application configuration file specifies a <codeBase> element, and a publisher policy file that is overriding the application information also specifies a <codeBase> element, the <codeBase> element in the publisher policy file is used.

If no match is found at the location specified by the <codeBase> element, the bind request fails and no further steps are taken. If the runtime determines that an assembly matches the calling assembly’s criteria, it uses that assembly. When the file specified by the given <codeBase> element is loaded, the runtime checks to make sure that the name, version, culture, and public key match the calling assembly’s reference.

Referenced assemblies outside the application’s root directory must have strong names and must either be installed in the global assembly cache or specified using the <codeBase> element.



Locating the Assembly through Probing

If there is no <codeBase> element in the application configuration file, the runtime probes for the assembly using four criteria:

  • Application base, which is the root location where the application is being executed.
  • Culture, which is the culture attribute of the assembly being referenced.
  • Name, which is the name of the referenced assembly.
  • The privatePath attribute of the <probing> element, which is the user-defined list of subdirectories under the root location. This location can be specified in the application configuration file and in managed code using the AppDomainSetup.PrivateBinPath property for an application domain. When specified in managed code, the managed code privatePath is probed first, followed by the path specified in the application configuration file.


Probing the Application Base and Culture Directories

https://docs.microsoft.com/en-us/dotnet/framework/deployment/how-the-runtime-locates-assemblies#probing-the-application-base-and-culture-directories



Probing with the privatePath Attribute

https://docs.microsoft.com/en-us/dotnet/framework/deployment/how-the-runtime-locates-assemblies#probing-with-the-privatepath-attribute



Probing Examples

https://docs.microsoft.com/en-us/dotnet/framework/deployment/how-the-runtime-locates-assemblies#probing-examples



Multiple Assemblies with the Same Name

The following example shows how to configure multiple assemblies with the same name.

<dependentAssembly>
   <assemblyIdentity name="Server" publicKeyToken="c0305c36380ba429" />
   <codeBase version="1.0.0.0" href="v1/Server.dll" />
   <codeBase version="2.0.0.0" href="v2/Server.dll" />
</dependentAssembly>



Best Practices for Assembly Loading

https://docs.microsoft.com/en-us/dotnet/framework/deployment/best-practices-for-assembly-loading



Redirecting Assembly Versions

https://docs.microsoft.com/en-us/dotnet/framework/configure-apps/redirect-assembly-versions

You can redirect compile-time binding references to .NET Framework assemblies, third-party assemblies, or your own app’s assemblies. You can redirect your app to use a different version of an assembly in a number of ways: through publisher policy, through an app configuration file; or through the machine configuration file. This article discusses how assembly binding works in the .NET Framework and how it can be configured.



Application domain

https://en.wikipedia.org/wiki/Application_domain

An application domain is a mechanism (similar to a process in an operating system) used within the Common Language Infrastructure (CLI) to isolate executed software applications from one another so that they do not affect each other. Each application domain has its own virtual address space which scopes the resources for the application domain using that address space.

Inter-domain communications

Direct communication cannot be achieved across application domains. However, application domains can still talk to each other by passing objects via marshalling by value (unbound objects), marshalling by reference through a proxy (application-domain-bound objects). There is a third type of object called a context-bound object which can be marshalled by reference across domains and also within the context of its own application domain. Because of the verifiable type-safety of managed code, a CLI can provide fault isolation between domains at a much lower cost than an operating system process can. The static type verification used for isolation does not require the same process switches or hardware ring transitions that an operating system process requires.

Managed code

Application domains are a purely managed code concept. Any included native/unmanaged code (e.g., C++) is largely unaware of them. Static variables seem to be shared across domains, callbacks can be problematic, and any memory corruption bugs in one domain is likely to corrupt other domains.

https://en.wikipedia.org/wiki/Managed_code

Managed code is computer program code that requires and will execute only under the management of a Common Language Infrastructure (CLI); Virtual Execution System (VES); virtual machine, e.g. .NET Core, CoreFX, or .NET Framework; Common Language Runtime (CLR); or Mono. The term was coined by Microsoft.

Managed code is the compiler output of source code written in one of over twenty high-level programming languages, including C#, J# and Visual Basic .NET.


Links

Still Strong-Naming your Assemblies? You do know it’s 2016, right?
https://www.pedrolamas.com/2016/03/01/still-strong-naming-your-assemblies-you-do-know-its-2016-right/

Start Strong-Naming your Assemblies!
https://www.pedrolamas.com/2018/09/11/start-strong-naming-your-assemblies/