I have a library that contains some reflection code which inspects an Asp.Net's primary assembly, any referenced assemblies and does cool stuff. I'm trying to get the same exact code to execute in a console application while still reflecting on an Asp.Net's assemblies and I'm seeing odd results. I've got everything wired up and the code executes, however the reflection code returns false when I know it should be returning true as I'm stepping through it in the debugger.. It's driving me nuts and I can't figure out why reflection is exhibiting different behavior when running from the console app.
Here's a perfect example of some reflection code that gets all of the types that are area registrations in an Asp.Net application (type.IsSubclassOf(typeof(System.Web.Mvc.AreaRegistration))
). This returns true for several types when executing in the app domain of an Asp.Net application, however it returns false for those same types when executed under the console application, but still reflecting on those same Asp.Net types.
I've also tried using the Assembly.ReflectionOnlyLoadFrom
method but even after writing all the code to manually resolve referenced assemblies the reflection code shown below returns false on types that it should be returning true for.
What can I try to make this work?
public static Assembly EntryAssembly { get; set; } // this is set during runtime if within the Asp.Net domain and set manually when called from the console application.
public CodeGenerator(string entryAssemblyPath = null)
{
if (entryAssemblyPath == null) // running under the Asp.Net domain
EntryAssembly = GetWebEntryAssembly(); // get the Asp.Net main assembly
else
{
// manually load the assembly into the domain via a file path
// e:\inetpub\wwwroot\myAspNetMVCApp\bin\myApp.dll
EntryAssembly = Assembly.LoadFrom(entryAssemblyPath);
}
var areas = GetAreaRegistrations(); // returns zero results under console app domain
... code ...
}
private static List<Type> GetAreaRegistrations()
{
return EntryAssembly.GetTypes().Where(type => type.IsSubclassOf(typeof(System.Web.Mvc.AreaRegistration)) && type.IsPublic).ToList();
}
This has to do with the assembly context in which LoadFrom
loads assemblies. Dependencies loaded during LoadFrom
will not be used when resolving "regular" assemblies in the Load
context.
The same appies the ReflectionOnly
overloads, which load into the ReflectionOnly context.
For detailed information see https://stackoverflow.com/a/2493855/292411, and Avoid Assembly.LoadFrom; instead use Assembly.Load for an issue with LoadFrom
similar to yours.
When I ran into this issue I switched to using Load
and demanded "plugin" assemblies to be in the same path as the executable; I don't know if there are tricks to make things work if the assemblies are in different paths.
Ok, after a lot of debugging I've got this working! It turned out that my library project was compiling against Asp.Net MVC 4.0 even though Nuget and the properties window claimed 5.1. Nuget/MS fail again. The Asp.Net MVC application that my library is reflecting on is using MVC 5.1 so when the Assembly.LoadFrom
and the AssemblyResolve
event ran it was loading two versions of System.Web.Mvc.dll
into the LoadFrom
context (4.0 & 5.1) and this caused the IsSubclassOf()
method to return false when the expected result should have been true.
The very odd error I mentioned in the comments above while debugging: The type 'System.Web.Mvc.AreaRegistration' exists in both 'System.Web.Mvc.dll' and 'System.Web.Mvc.dll'
now makes sense, but only after the fact.
The way I finally tracked this down was by writing out all of the assemblies that AssemblyResolve
was called upon to resolve and noticed that System.Web.Mvc.dll
was not in the list. I fired up the Assembly Binding Log Viewer and was clearly able to see that System.Web.Mvc.dll
was being loaded twice.
In retrospect, one should just skip all the custom logging and just use the Assembly Binding Log Viewer to verify only one of each assembly is being loaded and that it's the correct version your expecting.
Figuring out how to use AssemblyResolve
properly was a nightmare so here is my unfinished, but working code for posterity.
public class CodeGenerator
{
public static string BaseDirectory { get; set; }
public static string BinDirectory { get; set; }
static CodeGenerator()
{
BinDirectory = "bin";
// setting this in a static constructor is best practice
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
}
public CodeGenerator(string entryAssemblyPath = null, string baseDirectory = null, string binDirectory = null)
{
if (string.IsNullOrWhiteSpace(baseDirectory))
BaseDirectory = AppDomain.CurrentDomain.BaseDirectory;
else
BaseDirectory = baseDirectory;
if (string.IsNullOrWhiteSpace(binDirectory) == false)
BinDirectory = binDirectory;
if (entryAssemblyPath == null) // running under the Asp.Net domain
EntryAssembly = GetWebEntryAssembly(); // get the Asp.Net main assembly
else
{
// manually load the assembly into the domain via a file path
// e:\inetpub\wwwroot\myAspNetMVCApp\bin\myApp.dll
EntryAssembly = Assembly.LoadFrom(entryAssemblyPath);
}
var areas = GetAreaRegistrations(); // reflect away!
... code ...
}
static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
try
{
if (args == null || string.IsNullOrWhiteSpace(args.Name))
{
Logger.WriteLine("cannot determine assembly name!", Logger.LogType.Debug);
return null;
}
AssemblyName assemblyNameToLookFor = new AssemblyName(args.Name);
Logger.WriteLine("FullName is {0}", Logger.LogType.Debug, assemblyNameToLookFor.FullName);
// don't load the same assembly twice!
var domainAssemblies = AppDomain.CurrentDomain.GetAssemblies();
var skipLoading = false;
foreach (var dAssembly in domainAssemblies)
{
if (dAssembly.FullName.Equals(assemblyNameToLookFor.FullName))
{
skipLoading = true;
Logger.WriteLine("skipping {0} because its already loaded into the domain", Logger.LogType.Error, assemblyNameToLookFor.FullName);
break;
}
}
if (skipLoading == false)
{
var requestedFilePath = Path.Combine(Path.Combine(BaseDirectory, BinDirectory), assemblyNameToLookFor.Name + ".dll");
Logger.WriteLine("looking for {0}...", Logger.LogType.Warning, requestedFilePath);
if (File.Exists(requestedFilePath))
{
try
{
Assembly assembly = Assembly.LoadFrom(requestedFilePath);
if (assembly != null)
Logger.WriteLine("loaded {0} successfully!", Logger.LogType.Success, requestedFilePath);
// todo: write an else to handle load failure and search various probe paths in a loop
return assembly;
}
catch (FileNotFoundException)
{
Logger.WriteLine("failed to load {0}", Logger.LogType.Error, requestedFilePath);
}
}
else
{
try
{
// ugh, hard-coding, but I need to get on with the real programming for now
var refedAssembliesPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), @"Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5.1");
requestedFilePath = Path.Combine(refedAssembliesPath, assemblyNameToLookFor.Name + ".dll");
Logger.WriteLine("looking for {0}...", Logger.LogType.Warning, requestedFilePath);
Assembly assembly = Assembly.LoadFrom(requestedFilePath);
if (assembly != null)
Logger.WriteLine("loaded {0} successfully!", Logger.LogType.Success, requestedFilePath);
// todo: write an else to handle load failure and search various probe paths in a loop
return assembly;
}
catch (FileNotFoundException)
{
Logger.WriteLine("failed to load {0}", Logger.LogType.Error, requestedFilePath);
}
}
}
}
catch (Exception e)
{
Logger.WriteLine("exception {0}", Logger.LogType.Error, e.Message);
}
return null;
}
}