Reputation: 429
I'm having an assembly resolve issue that's driving me nuts.
So my company's product can have many plug in services. My team works on these services. We have a bunch of reusable code in a utility assembly (Utility.dll).
From time to time we make improvements or add new features to Utility.dll. We have very strict backward compatibility testing to make sure the new version of that assembly works with all older plug in services.
The problem I am having is resolving the currently deployed version of Utility.dll.
Let me give you an example.
Utility.dll is currently at version 1.0.0.0
We create a service ServiceA built by referencing Utility.dll version 1.0.0.0
We then update Utility.dll and update the version to 2.0.0.0
Next we create a service ServiceB that uses Utility.dll 2.0.0.0.
Utility.dll version 2.0.0.0 is compatible with both ServiceA and ServiceB.
I launch our application. The deployed version of Utility.dll gets loaded.
I launch ServiceB. My implementation of the AppDomain.CurrentDomain.AssemblyResolve event gets fired and Utility.dll 2.0.0.0 gets returned.
I then launch ServiceA. AppDomain.CurrentDomain.AssemblyResolve never gets fired but I get an a FileNotFoundException for Utility.dll 1.0.0.0. At this point I want to resolve with Utility 2.0.0.0. If version 1.0.0.0 isn't found why doesn't the AssemblyResolve event get fired?
Also, I can do it in reverse order by launching ServiceA first and AssemblyResolve gets fired and Utilty.dll 2.0.0.0 is resolved for ServiceA(that was originally built with Utilty.dll 1.0.0.0). However, if I then launch ServiceB (which was built with Utilty.dll 2.0.0.0) the AssemblyResolve event never gets fired and a FileNotFoundException is thrown for Utility.dll version 1.0.0.0.
What is going on? I just want to use the currently deployed version of Utility.dll for all services.
******UPDATED******
public class UtilityLoader
{
private IServiceContext context;
public UtilityLoader(IServiceContext context)
{
this.context = context;
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
}
private bool Loaded
{
get
{
context.Application.Log.WriteInfo("Checking for Utility...");
Assembly asmFound = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(asm => asm.FullName.Contains("Utility"));
context.Application.Log.WriteInfo(string.Format("Utility {0} loaded.{1}", asmFound == null ? "is not" : "is", asmFound == null ? string.Empty : string.Format(" Version: {0}", asmFound.GetName().Version)));
return asmFound != null;
}
}
public bool Load()
{
if (Loaded)
return true;
string utilityPath = Path.Combine(Session.DataDirectory, "Utility.dll");
if (File.Exists(utilityPath))
{
context.Application.Log.WriteInfo(string.Format("Utility.dll was found."));
FileStream stream = File.OpenRead(utilityPath);
byte[] assemblyData = new byte[stream.Length];
stream.Read(assemblyData, 0, assemblyData.Length);
stream.Close();
try
{
Assembly.Load(assemblyData);
}
catch (Exception ex)
{
context.Application.Log.WriteInfo(string.Format("Could not load Utility: {0}", ex.Message));
throw;
}
return true;
}
return false;
}
private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
AssemblyName asmName = new AssemblyName(args.Name);
if (asmName.Name == "Utility")
{
context.Application.Log.WriteInfo("Resolving Utility");
Assembly nuAsm = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(asm => asm.FullName.Contains("Utility"));
context.Application.Log.WriteInfo(string.Format("Utility {0} already loaded.", nuAsm == null ? "is not" : "is"));
return nuAsm;
}
return null;
}
}
I am then calling it like this in each of my service plugins.
public void Execute(IServiceContext context, ServiceParameters serviceParams)
{
UtilityLoader loader = new UtilityLoader(context);
if (!loader.Load())
{
MessageBox.Show("Utility not loaded.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
StartService(context, serviceParams);
}
The first service plugin that gets called loads fine as well as any other service plugin that was built referencing the same version of Utility.dll as the first service plugin that was loaded. If a service plugin is called that was built with a different version of Utility.dll then a FileLoadException is thrown.
Another example
ServiceA built with Utility 1.0.0.0 ServiceB built with Utility 1.0.0.0 ServiceB built with Utility 2.0.0.0
Utility 2.0.0.0 is deployed.
User launches ServiceA first. Utility 2.0.0.0 is loaded. AssemblyResolve event is fired. Assembly is resolved. Service launches.
User, within the same session, launches ServiceB. Service B uses the same version of Utility as ServiceA. Service B launches.
User, within the same session, launches ServiceC. ServiceC was built with different version of Utility. ServiceC fails to load.
User restarts the application and attempts to load ServiceC first. ServiceC loads fine. Then, within the same session, user tries to loan ServiceA or ServiceB and both fail.
For some reason when a service is loaded after the initial one it doesn't fire AssemblyResolve again even when the assembly can't actually be resolved. If the event fired my handler would return the currently loaded version of Utility.
Upvotes: 1
Views: 2318
Reputation: 1066
Difficult to know whats going out without more details (are Service A and Service B independent .dll?, or they are in the same .exe assembly?) but, I will make it in this way:
1) I will use AppDomain.CurrentDomain.AssemblyResolve mechanism only for failure detecting. But I will try to get it first without using .Net AppDomain logic.
MyDomain.AssemblyResolve += new ResolveEventHandler(Loader_AssemblyResolve);
if (System.IO.File.Exists(pathToAssembly) == false)
{
System.IO.File.Copy(KnownPathToLastVersion, pathToAssembly)
}
_assembly = Assembly.Load(AssemblyName.GetAssemblyName(pathToAssembly));
2) In my Loader_AssemblyResolve I would use the best known practices that I some time learn from a web page, but right now, I am shameful lazy for look for the URL (Welcome contributions!):
private Assembly Loader_AssemblyResolve(object sender, ResolveEventArgs args)
{
Assembly assembly = null;
//1. Disconnect the event AssemblyResolve
_Domain.AssemblyResolve -= new ResolveEventHandler(Loader_AssemblyResolve);
try
{
//2. Do not try to get the file without looking first
// in memory. AssemblyResolve could fire even when the
// Assembly is already loaded
assembly = System.Reflection.Assembly.Load(args.Name);
if (assembly != null)
{
_Domain.AssemblyResolve += new ResolveEventHandler(Loader_AssemblyResolve);
return assembly;
}
}
catch
{ // ignore load error
}
//3. Then try to get it from file
string FileName=GetFileName(args.Name);
try
{
assembly = System.Reflection.Assembly.LoadFrom(FileName);
if (assembly != null)
{
_Domain.AssemblyResolve += new ResolveEventHandler(Loader_AssemblyResolve);
return assembly;
}
}
catch
{ // ignore load error
}
//Be sure to reconnect the event
_Domain.AssemblyResolve += new ResolveEventHandler(Loader_AssemblyResolve);
return assembly;
}
I hope it helps you.
Upvotes: 1