简体   繁体   中英

Reflection not working on assembly that is loaded using Assembly.LoadFrom

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;
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM