Uvažujme následující scénář. Ve své aplikaci máme nějakou část, kterou potřebujeme umět customizovat nebo rozšiřovat pomoci externích knihoven. Tím myslím to, že bude nějaké výchozí chování (např. to, že daná funkcionalita nebude dostupná). Pokud ale k aplikaci umístíme jednu nebo více assembly (dále je budu označovat custom assembly), výchozí chování se změní nebo daná funkcionalita doplní. Například se může jednat o zákaznické (či uživatelské) knihovny pro tiskové sestavy, změnu výchozího worlflow za nějaké jiné navržené speciálně na zakázku (oba tyto scénáře jsem v praxi již řešil) či přímo variantu klasických addin doplňků.
Ok nějakou představu snad máme, dále uvedu, že se nebudeme zabývat tím, jak bude z aplikace prováděno samotné volání části implementované v custom assembly. Typicky to může být tak, že custom assembly i samotná aplikace budou referencovat assembly s definicí nějakého společného interface a custom assembly bude obsahovat třídu nebo třídy implementující tento interface. Tyto třídy se pak budou z aplikace volat pomoci třídy Activator.
To čím se ale zabývat budeme je jak custom assembly najít na file systému a následně načíst. Dále také budeme z důvodu obecnosti předpokládat, že těchto souboru může být více než pouze assembly jediná. Ještě je asi důležité zmínit, že jednou z možností, které se ale nyní věnovat nebudu, je použít řešení v rámci MEF (Managed Extensibility Framework).
Teď už k samotnému řešení, shrňme si co musíme vyřešit. Jednak musíme vyhledat příslušné soubory na disku. K tomu potřebujeme znát v jakém adresáři a jaké soubory budeme hledat. Pro náš příklad zvolíme adresář stejný jako adresář, ve kterém je umístěna vlastní aplikace (nepředpokládáme zde instalaci aplikace do GAC). Filtr pro hledání souboru je jednoduchý, protože chceme vyhledat soubory assembly, bude se jednat o *.dll “natvrdo”.
Dále musíme každou vyhledanou assembly načíst a potom nějakou podmínkou rozhodnout, zda je to knihovna, která nás zajímá. Touto podmínkou může být např. existence nějakého resource assembly, existence nějakého typu podle jména nebo existence typu, který implementuje nějaký interface. V našem příkladu si ukážeme variantu z hledáním resource.
Poslední problém k vyřešení je to, že celý tento proces nebude nejrychlejší a proto budeme chtít, aby se spouštěl pouze jedenkrát, a pak se již použili stejné výsledky. Důsledkem toho bude, že custom assembly nepůjde dynamicky vyměňovat za běhu aplikace, budeme předpokládat, že nám toto nevadí.
Po provedení předchozí analýzy je vytvořit výsledný kód již poměrně jednoduché.
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Collections.Generic;
using System.Collections.ObjectModel;
internal static class CustomAssemblyFinder
{
#region constructors and destructors
private const string cCustomAssemblyResourceName = "..."; //Resource name
#endregion
#region member varible and default property initialization
private static readonly object s_SyncRoot = new object();
private static ReadOnlyCollection<Assembly> s_CustomAssemblies;
#endregion
#region property getters/setters
public static IEnumerable<Assembly> CustomAssemblies
{
get
{
if (s_CustomAssemblies == null)
{
lock (s_SyncRoot)
{
if (s_CustomAssemblies == null)
{
s_CustomAssemblies = (from assembly in GetAssemblies(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))
where assembly.GetManifestResourceInfo(cCustomAssemblyResourceName) != null
select assembly).ToList().AsReadOnly();
}
}
}
return s_CustomAssemblies;
}
}
#endregion
#region private member functions
private static IEnumerable<Assembly> GetAssemblies(string path)
{
foreach (string filename in Directory.EnumerateFiles(path, "*.dll"))
{
Assembly assembly = LoadAssemblyGuarded(filename);
if (assembly != null)
{
yield return assembly;
}
}
}
private static Assembly LoadAssemblyGuarded(string assemblyFilePath)
{
var assemblyName = AssemblyName.GetAssemblyName(assemblyFilePath);
try
{
return Assembly.Load(assemblyName);
}
catch (FileNotFoundException)
{
return null;
}
catch (FileLoadException)
{
return null;
}
catch (BadImageFormatException)
{
return null;
}
catch (ReflectionTypeLoadException)
{
return null;
}
}
#endregion
}
Pouze upozorním na několik zajímavých skutečnosti: Pro vyhledávání souborů používám metodu Directory.EnumerateFiles, která je nově dostupná od .NET Frameworku 4.0. U načítání assembly je ošetřeno několik možných chyb pro ignorování souboru např. v případě kdyby se vůbec nejednalo o assembly nebo by assembly byla přeložená pro jinou platformu apod. Výslednou kolekci CustomAssemblies vracíme jako objekt typu ReadOnlyCollection<T>, aby jsme zabránili její případné modifikaci zvenku.