Tuesday, October 3, 2017

Microsoft 70-487: Share assemblies between multiple applications and servers

Exam Objectives

Prepare the environment for use of assemblies across multiple servers (interning); sign assemblies by using a strong name; deploy assemblies to the global assembly cache; implement assembly versioning; create an assembly manifest; configure assembly binding redirects (for example, from MVC4 to MVC5)


Quick Overview of Training Materials



Prepare Environment (interning)


Before diving into the aspnet_intern tool, I think it's importnat to answer the question:

   What is interning?

I've heard of the term in the context of strings, and the MSDN documentation for the String.Intern() methods provides a pretty good overview of what interning means.  Interning of Strings basically takes a literal string, say "Hello World" and creates a single instance of it.  If I then assign that string to two variables, not only will their values be equal, but they will actually both refer to the same object.  (If you are curious, Java has the same capability).


static void Main(string[] args)
{
    Console.WriteLine("Enter two strings: ");
    string one = Console.ReadLine();
    string two = Console.ReadLine();
    Console.WriteLine("Before Interning...");
    Console.WriteLine("Same value:  " + (one == two));
    Console.WriteLine("Same object: " + ((Object)one == (Object)two));
    Console.WriteLine("After Interning...");
    string three = String.Intern(two);
    string four = String.Intern(one);
    Console.WriteLine("Same value:  " + (four == three));
    Console.WriteLine("Same object: " + ((Object)four == (Object)three));

    Console.ReadLine();
}



In ASP.NET 4.5, this concept was applied to shared assemblies.  Normally, when you have multiple projects using the same libraries, each reference is treated as a separate, unique library.  Each assembly will be loaded into memory independently, and as a result you will have multiple copies of the same assembly resident in memory at the same time.  The aspnet_intern utility will analyze a project and look for these multiple uses, and if it finds (by default) three or more references, it moves the assembly to a "shared" location and replaces all the original references with symbolic links.

The canonical demo for this is to create a class library and three console applications, add a reference from all three console apps to the class library, and run the tool (Both the Shane B. and Sateesh blog posts go this route).  So lets run through it real quick.  I found the aspnet_intern tool under the Windows SDK folder:

"C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.2 Tools"


I ran the tool from the root of the project:

aspnet_intern -mode exec -sourcedir "." -interndir "./Common"



And you can see in this before-after shot that the .dll files have been replaces with a symbolic link:



ClassLibrary1.dll now only exists as a single copy in the "Common" folder in the root of the project (because that is where I told the tool to put it with the -interndir argument):


One thing to keep in mind is that if the shared library changes, when you rerun the build the new .dll is copied into the individual projects again.  Thus you would need to run aspnet_intern again.  This suggests to me that this is probably something that would be well suited to integration into a build pipeline, rather than having a developer run it locally.



Creating Signed Assemblies


Using a strong name on an assembly creates a cryptographic guarantee of the assemblies integrity.  A strong name consists of the simple name of the assembly, the version number, culture, and a public key and digital signature.  The documentation for Strong-Named Assemblies warns that strong names should not be relied upon for security, they only provide a unique identity.

One example of where you may have seen a strong assembly name is when referencing types in other assemblies in configuration files.  For example, this appears in the config for one of my WCF demos:



Probably the easiest way to create a strong name for an assembly is to "sign" it using Visual Studio. Right-click the project file and select properties, then select the "Signing" option.  This menu will allow you to create a new strong name for the assembly:



Going through Visual Studio like this will create a .snk file in the root of your project.  Now when you reference this assembly in other projects, information about the assembly and runtime version will come alone with it.  Contrast this with a library without a strong name; in the following example, ClassLibrary1 has been signed with a strong name, and ClassLibrary2 has not, but they are otherwise basically identical template code:

The Version and Runtime info for #2 showed up after it was built


The How To documentation also describes how to use the Assembly Linker to sign an assembly with a strong name, but leaves out the fact that you have to use the Strong Name (sn.exe) tool to create the public-private key pair first for this to work.  Both tools are available from a Visual Studio "Developer Command Prompt".  It also sort of hand waves over the fact that you need a .netmodule file to sign... which isn't supported by Visual Studio... so you have yet another step (and tool, csc.exe).  So once you've created the .netmodule with csc, and created a key pair with sn, then you can create the signed assembly with assembly linker:



The final way to sign an assembly is to use the AssemblyKeyFile attribute (part of System.Reflection). This takes the relative path to a strong name key file, similar to the above examples.  Reading through the documentation it wasn't clear to me if it mattered where this attribute is placed, I only had one class file so I dropped it there:

using System.Reflection;

[assembly: AssemblyKeyFile("keyPair.snk")]
namespace ClassLibrary3
{
    public class Class1
    {
        //...
    }
}



Deploy Assemblies to GAC


The Global Assembly Cache, or GAC, is a machine-wide cache of assemblies shared by the applications on the computer.  Microsoft's documentation cautions against installing software into the GAC unless it is really necessary, instead preferring to keep assembly dependencies private.

The GAC is really just a collection of file system folders, located under the Winodows/Micrsoft.NET/assembly folder.  This folder contains the folders GAC_32, GAC_64, and GAC_MSIL, which contain assemblies corresponding to which architecture they support.  Deploying assemblies to the GAC is not just a matter of copying them to these folders, however; they need to be installed.  This can be done two ways:  with an installer, or with gacutil. Both of these methods are explained in the How To article.  In order to install an assembly into the GAC, it must be signed with a strong name as described earlier.

For Visual Studio 2010-2015, you can get InstallShield Limited Edition as a free download (you just have to enter register basically).  This isn't available for VS 2017 (yet).  First you have to install the "Installer Projects" for Visual Studio from the VS Marketplace, then you can install InstallShield, which will give you the option to create installer projects:



Once in the InstallShield project, find "Specify Application Data" in Solution Explorer and open "Files".  Right click on "Destination Computer", then select "Show Predefined Folder", then check [GlobalAssemblyCache]:



Now, under "Source computer's folders" in the same screen, select the project corresponding to the assembly you want to install into the GAC.  In the files pane, drag "Primary output" down to the destination computer's files:



Apparently there is more configuration involved to get an InstallShield project to work.  There is no point in me spending a ton of time troubleshooting this project because A) I don't use InstallShield for anything and B) apparently neither does anyone else.  I can understand why everyone uses gacutil, because InstallShield is a pain.  Installing with gacutil is a one liner in the Dev Command Prompt (make sure to remember to Run As Administrator):

gacutil /i myAssembly.dll



But, ya know, why do it the easy way when you can futz around for an hour trying to make InstallShield work? </sarcasm>...

gacutil also allows for uninstalling assemblies from the cache, just make user to drop the .dll extension and just use the assembly name.  You can also list the assemblies matching a given name using the /l command.

gacutil /u myAssembly




Implement Assembly Versioning


Part of the process for creating a strong name for an assembly is assigning a version number.  According to Microsoft's documentation, only strongly named assemblies are versioned at the assembly level (You can assign a version number through Visual Studio as well, by navigating to the project properties and opening up the "Assembly Information" menu:



Like the key file attribute, there is an assembly attribute for setting the version. Incidentally, playing with this is how I found out where these attributes are typically located... in AssemblyInfo.cs... imagine that!  I tried to put the AssemblyVersion attribute in another file and got a build error complaining about a duplicate...

using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("ClassLibrary1")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("ClassLibrary1")]
[assembly: AssemblyCopyright("Copyright ©  2017")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

// Setting ComVisible to false makes the types in this assembly not visible
// to COM components.  If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]

// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("76c277b5-13c8-4249-918c-3cd60ce85c03")]

// Version information for an assembly consists of the following four values:
//
//      Major Version
//      Minor Version
//      Build Number
//      Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.1.0.0")]
[assembly: AssemblyFileVersion("1.1.0.0")]


If this assembly is signed, then the version number you set here in Visual Studio will become part of the strong name.  It's annoyingly difficult to get the full strong name for an assembly, as the strong name tool will only give you the public token or verify that the assembly has a strong name.  One hack is to install the .dll into the GAC and then uninstall it (but this is dumb...):



You can also view the strong name with a tool like ILSpy (which is what the StackOverflow answers all suggests).  ILSpy is a free tool for browsing and decompiling .NET assemblies.



Assembly versions play a role in the way the .NET runtime locates and loads assemblies.  The details of this process are explained in the How the Runtime Locates Assemblies article, and is illustrated (quite humorously) by Jamie King in his video on assembly versioning.  When assemblies are not signed with a strong name, the assembly is looked up using only the assembly name (version is ignored and the public key doesn't exist, by definition).  For signed assemblies, applications will only run with the assembly versions they were built with unless this is overridden by configuration (assembly redirection is covered below).



Create Assembly Manifest


The assembly manifest is a collection of metadata about the assembly.  For single file assemblies (.dll and .exe files), the manifest is part of the file, or for multifile assemblies the manifest can be a stand alone file.  For most .NET projects, a default manifest is automatically embedded in the output. Assembly manifests should not be confused with application manifests, which apparently serve a different purpose.  Adding to the confusion is the fact that COM and Win32 assemblies also use application and assembly manifests...

The manifest that can be added as a project item is an application manifest.  To manipulate the assembly manifest it is necessary to use the [assembly:] attributes, which are typically collected in the AssemblyInfo.cs file.  The snippet above shows pretty much every available assembly attribute, so not going to repeat it here.

It is possible to view the assembly manifest using the ildasm (IL Disassembler).  From a dev command prompt, run "ildasm <assembly file>", and click on the MANIFEST item:



The information is also available using dotPeak, which is a good all-around tool for decompiling .NET projects, but it's not all in one nice handy package like ildasm.  ILSpy displays what looks like the AssemblyInfo file:



The Exam Ref, in class fashion, basically copy/pastes this section right out of the Microsoft documentation on assembly manifests.  The list of purposes served by the assembly manifest is copied verbatim, but neither really discusses how or why these are important:

  • Enumerates the files that make up the assembly
  • Maps references to the types and resources to the files containing them
  • Enumerates the other assemblies upon which this one depends
  • Provides a level of indirection between consumers of the assembly and the implementation
  • Renders the assembly self describing.



Configure Assembly Redirects


The Microsoft articles on How the Runtime Locates Assemblies and Redirecting Assembly Versions provide a thorough treatment of the various levers at our disposal for customizing the .NET assembly loading behavior.  The three main tools for doing assembly redirects are the app config file, publisher policy, and the machine config file.

I think it's useful to think about how the runtime locates and loads assemblies before examining how that process is affected by redirection. The Back to Basics blog post is an excellent companion to the above two documents for building up the full picture, though it's worth noting the difference in focus between that post (which is dealing with loading indirect dependencies) and the MS documentation which deal with direct dependencies.

The process starts when the runtime attempts to resolve an assembly reference. Preferably this reference is a full strong name (a static reference), but a partial reference (meaning only the assembly name without version, culture, or public key token) can also be used (this is called a dynamic reference).  It is possible to use the <qualifyAssembly> element to add strong name references via the configuration file.

The first step in the assembly resolution process is to determine the correct assembly version.  Well, sort of... if the referenced assembly has a strong name, this matters, but as Jamie King demonstrated in his video on assembly versioning, assemblies without strong names are not checked for version.  The version checking process is explained in detail in the Assembly Versioning document (with a flowchart even!).  Here are the steps:

  1. Checks the original assembly reference
  2. Checks for application policy, publisher policy, and admin policy
  3. Determines correct version to bind
  4. Probes GAC, CodeBases, and application sub-directories for assembly file


Assembly version redirection takes place in the various policy files. All three types (application, publisher, administrator) are XML configuration based, but vary on which config file includes the relevant elements.

Publisher Policy is a way for the vendor of an assembly to redirect users of an older assembly to a backward compatible new version.  A Publisher Policy is a config file packaged as an assembly, that is installed into the GAC (explained in this How To article). The How To provides this example of what a publisher policy config file actually contains:

<configuration>  
   <runtime>  
      <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">  
       <dependentAssembly>  
         <assemblyIdentity name="myAssembly"  
                           publicKeyToken="32ab4ba45e0a69a1"  
                           culture="en-us" />  
         <!-- Redirecting to version 2.0.0.0 of the assembly. -->  
         <bindingRedirect oldVersion="1.0.0.0"  
                          newVersion="2.0.0.0"/>  
       </dependentAssembly>  
      </assemblyBinding>  
   </runtime>  
</configuration>


The same set of elements can be used in the applications configuration file (app.config or web.config) to effect similar redirection, though this applies only to this application and not to every consumer of the assembly (as happens with the publisher policy).  This is referred to as the "application policy".  It is possible to target redirections at a specific version of the .NET framework by using the "appliesTo" attribute on the <assemblyBinding> element.  If specifying multiple redirects based on the framework version, the MS documentation suggests putting these in ascending framework order (so 3.5, then 4, then 4.5, etc), with a "catchall" redirect at the end. The first matching redirect will be the one applied.


<runtime>  
  <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"   
    appliesTo="v3.5">  
    <dependentAssembly>   
      <!-- assembly information goes here -->  
    </dependentAssembly>  
  </assemblyBinding>  
</runtime>


The application policy can also be used to bypass publisher policy.  This is done by adding <publisherPolicy apply="no"/> as a child element to the <dependantAssembly/> element.  The docs suggest that this should only be used as a stop gap to fix an application when a vendor version update breaks existing code, and to report the problem to the vendor ASAP. The "How the Runtime Locates Assemblies" document refers to this as "Safe Mode".


<runtime>  
  <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">  
    <dependentAssembly>   
       <publisherPolicy apply="no"/>  
    </dependentAssembly>  
  </assemblyBinding>  
</runtime>


Finally, redirects can be specified at the machine level by adding the above configuration to the Machine.config file (this is called the "Adminstrator Policy").  This is located in the Config subdirectory under the runtime folder, which usually lives somewhere like C:\Windows\Microsoft.NET\Framework\<version>



Since redirection can be set in any or all of these configuration files, some settings will override other based on precedence.  The Administrator policy (machine.config) will override the publisher and application policies, and the publisher policy overrides the application policy unless it has been bypassed.

Once the correct policy is applied and the correct version is determined, the runtime checks if the assembly has been bound before.  If so, it simply use that assembly. If that binding attempt failed, then the current binding attempt also fails immediately (i.e. failures are also cached).  If the assembly has not been bound before, the runtime begins looking for the assembly files.

For strongly named assemblies, the first stop is the GAC.  For other assemblies (and those not found in the GAC), the runtime will look in two kinds of locations:


  • path contained in a <codeBase> element
  • subdirectories in the application folder structure (which may include culture specific assemblies)
  • privatePath specified with the <probing> element

If a codeBase is specified, that is the only place the runtime will look for the assembly.  When looking in the application folder structure, there are various combinations of the binary path, culture, and assembly name that can make up the path that the runtime will look for:

[application root]/[privatePath?]/[culture?]/[assembly name?]/[assembly name].dll



For more info, the Best Practices for Assembly Loading doc is probably a good follow up...

No comments:

Post a Comment