Tuesday, December 29, 2009

Loading assemblies from a shared location

Recently we had a restriction on deploying assemblies to GAC. Our application structure is quite complex where in my team owns the root website and the framework and lots of applications are configured as sub applications (or even multi level sub applications). It is impossible to release the assemblies to individual teams and coordinate. So for solving this issue we have decided to deploy the shared assemblies to a common location and load it dynamically from there.

The approach we have taken is as follows.
Create an http module called SharedAssemblyLauncher. This assembly will be released to all teams and they have to define this module in their web.config.
The root web.config will define a key "SharedAssemblyBaseDirectory" to specify the base shared assembly location from where the probing starts. Each application web.config can override this too. For supporting multiple versions, you can create folders with the version number and then copy the assemblies to that folder. The probling rule is as follows
1. Look under the folder with version number.
2. Look under folder Common.
3. Base directory

We start with creating a new handler for AssemblyResolve for current domain.

public class SharedAssemblyLauncher : IHttpModule
{
   private static string _BaseAssemblyFolder = "";

   static SharedAssemblyLauncher()
   {
      if (HttpContext.Current != null)
      {
         string appRoot = HttpContext.Current.Server.MapPath("~/");
         string webRoot = HttpContext.Current.Server.MapPath("/");
         if (appRoot != webRoot)
            _BaseAssemblyFolder = GetSharedAssemblyBaseDirectoryFromConfigFile(GetSlashEndedFolder(appRoot) + "web.config");
         if (_BaseAssemblyFolder.Trim() == "")
            _BaseAssemblyFolder = GetSharedAssemblyBaseDirectoryFromConfigFile(GetSlashEndedFolder(webRoot) + "web.config");
      }
      AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
   }

   public void Dispose()
   {
   }

   public void Init(HttpApplication context)
   {
   }
}

In assembly resolve handler, the first thing what we will do is loop through the loaded assemblies to look for a match. We've noticed that, in case for a request for embedded resource the version number was not passed, only the assembly name is passed. So if we probe, we might not find any matching assembly. So it is very important to loop through and find a match before the actual probing starts. If no match found on loaded assemblies, probe based on the sequence defined earlier. If no match found while probing, return null so that the framework does its own probing.
static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
   Assembly[] loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
   foreach (Assembly assembly in loadedAssemblies)
   {
      if (assembly.FullName == args.Name || assembly.FullName.Contains(args.Name))
         return assembly;
   }

   if (_BaseAssemblyFolder.Trim() == "")
      return null;

   string assemblyFileName = GetAssemblyFileName(args.Name);
   string assemblyVersion = GetAssemblyVersion(args.Name);
   try
   {
      string fileName = string.Format("{0}{1}\\{2}.dll", _BaseAssemblyFolder, assemblyVersion, assemblyFileName);
      if (File.Exists(fileName))
         return GetAssemblyFromPath(fileName);
      fileName = string.Format("{0}Common\\{1}.dll", _BaseAssemblyFolder, assemblyFileName);
      if (File.Exists(fileName))
         return GetAssemblyFromPath(fileName);
      fileName = string.Format("{0}{1}.dll", _BaseAssemblyFolder, assemblyFileName);
      if (File.Exists(fileName))
         return GetAssemblyFromPath(fileName);
   }
   catch { }
   return null;
}

private static string GetSlashEndedFolder(string baseFolder)
{
   return (baseFolder.Trim().EndsWith("\\") ? baseFolder.Trim() : baseFolder.Trim() + "\\");
}

private static string GetSharedAssemblyBaseDirectoryFromConfigFile(string configPath)
{
   XmlDocument configXmlDoc = new XmlDocument();
   if (!File.Exists(configPath))
      return "";
   configXmlDoc.Load(configPath);
   XmlNode baseDirectoryNode = configXmlDoc.SelectSingleNode("/configuration/appSettings/add[@key=\"SharedAssemblyBaseDirectory\"]");
   if (baseDirectoryNode == null)
      return "";
   return baseDirectoryNode.Attributes["value"].Value;
}

private static string GetAssemblyFileName(string assemblyName)
{
   return assemblyName.Split(",".ToCharArray())[0];
}

private static string GetAssemblyVersion(string assemblyName)
{
   string[] parts = assemblyName.Split(",".ToCharArray());
   for (int i = 0; i < parts.Length; i++)
   {
      if (parts[i].Trim().StartsWith("Version="))
      {
         string[] versionParts = parts[i].Split("=".ToCharArray());
         return versionParts[1].Trim();
      }
   }
   return "";
}

private static Assembly GetAssemblyFromPath(string assemblyPath)
{
   return Assembly.LoadFile(assemblyPath);
}

One important thing to remeber here is that never try to read configuration values using ConfigurationManager in this module.

Happy sharing!!!

No comments:

Post a Comment