Handling Assemblies that Won't Load: Method 2.1

Yesterday, I showed an alternate to the shim method of gracefully failing from an assembly using an AppDomainManager.  However, as David pointed out  this method isn't particularly useful if you're going to be using it to detect failure to load your main assembly.  The reasoning is that AppDomainManagers need FullTrust to run.  If your application supplied AppDomainManager has FullTrust then either you're in a FullTrust location, or you were able to run some sort of setup to get the AppDomainManager trusted.  And if you were able to run a setup, why not just trust your application?

So it looks like the shim is still the best way to go if you need to detect your main assembly's failure to load.  That being said, the AppDomainManager approach can be used for all of the other assemblies that your application uses.  Using an AppDomainManager instead of putting try ... catch blocks around each assembly load has several advantages.

  1. If you're not using Assembly.Load, you're not going to be sure exactly where the exception will occur
  2. The AppDomainManager will work even for third party code that you don't control
  3. You only need the code in one location, instead of scattering it around your application for each assembly load.  This means you have only one spot to get bug-free, and you won't forget to put try .. catch code at each required spot.

In order to pull this off, we'll need to make some modifications to yesterday's AppDomainManager.  The technique that yesterday's version used was to search through all unhandled exceptions to see if the exception being thrown was that the entry assembly could not be loaded.  That was a rather indirect way of getting the information, a more sensible approach would be to check each assembly as it loads to see if we are granting enough permissions.

In order to do this, we need to have some code execute whenever we're determine the PermissionSet that will be granted to an assembly.  From looking at our HostSecurityManager, it seems that HostSecurityMnaager::ResolvePolicy is exactly what we need.  ResolvePolicy gives us some evidence, and we give it back a permission set.

Most overrides of ResolvePolicy will be interested in modifying the permission set that's granted to an assembly .... however we don't care about that, we can just rely on the default implementation to do the actual policy resolution.  Instead, once we've gotten the PermissionSet that will be granted, we need to figure out if that's enough.  In order to do that, we need to know the minimum set of permissions the assembly requires to load.  That information is conveniently bundled up for us in a PermissionRequestEvidence object attached to the evidence.

So all we need to do is loop over the evidence looking for the PermissionRequestEvidence and the Url (so we know what assembly we're talking about).  If we find PermissionRequestEvidence, and it has a minimum request set, then we need to check if it's a subset of the permissions that were granted.  If it is, then we're fine.  However, if it isn't this assembly won't load, exactly the case we're interested in.

The simple UnableToLoadHostSecurityManager below simply prints an error message and quits in this instance, however that's probably not the route that a real application would want to take.  Instead, you might show the user a dialog box explaining what happened, and maybe fire an event that would let the rest of the application know the problem.

The code to pull this off is actually pretty straightforward:

using System;
using System.Collections;
using System.Reflection;
using System.Security;
using System.Security.Policy;

[assembly: AssemblyVersion("1.0.0.0")]

public sealed class UnableToLoadAppDomainManager : AppDomainManager
{
    private UnableToLoadHostSecurityManager hostSecurityManager = new UnableToLoadHostSecurityManager();
    
    public override HostSecurityManager HostSecurityManager
    {
        get
        {
            return hostSecurityManager;
        }
    }
}

public class UnableToLoadHostSecurityManager : HostSecurityManager
{
    public const int FailCode = 0;
    
    public override HostSecurityManagerOptions Flags
    {
        get { return HostSecurityManagerOptions.HostResolvePolicy; }
    }

    public override PermissionSet ResolvePolicy(Evidence evidence)
    {
        // allow the policy to determine what the entry assembly will get
        PermissionSet entryAssemblyPermissions = base.ResolvePolicy(evidence);
        
        // find its minimum grant set and name
        PermissionRequestEvidence permissionRequest = null;
        Url url = null;
        
        IEnumerator evidenceEnumerator = evidence.GetHostEnumerator();
        while(evidenceEnumerator.MoveNext())
        {
            if(permissionRequest == null)        
                permissionRequest = evidenceEnumerator.Current as PermissionRequestEvidence;
            if(url == null)
                url = evidenceEnumerator.Current as Url;
        }
        
        if(permissionRequest != null && permissionRequest.RequestedPermissions != null)
        {
            // check to see if the required permissions were granted
            if(!permissionRequest.RequestedPermissions.IsSubsetOf(entryAssemblyPermissions))
            {
                string assembly = (url != null) ? url.Value : "assembly";
                
                Console.WriteLine("Could not load {0}, possibly due to security issues. Please copy to a local location.", assembly);
                   Environment.Exit(FailCode);
            }
        }

        // just return the grant set that policy allowed
        return entryAssemblyPermissions;
    }
}