5

UPDATE:

Basically, this boils down to "how can I force class libraries to load at Application Start for an Web API site so I can reflect through them once and be sure I get all implementations of a certain class. Alternately, if there's no good way to do this, what's the best way to allow classes in that library to register themselves?

Original Question:

I'm trying to register all classes that implement a certain interface on application start in my Web API and put them in a list, so I can find them later without reflecting through the assembly on each call.

It seems fairly simple, although I've never done it before. So after a bit of googling and reading some other Stack Overflow questions, I created a container class and put a method to register all the concrete implementations. A simplified solution looks something like this:

public static void RegisterAllBots()
        {
            var type = typeof(IRobot);
            var types = AppDomain.CurrentDomain.GetAssemblies()
                .SelectMany(s => s.GetTypes())
                .Where(type.IsAssignableFrom);
            foreach (var t in types)
            {
                TypeRepo.Add(t.Name.ToLower(), t);
            }
        }

And then I put my RegisterAllBots() in Application_Start() of the Global.asax.

The problem is, sometimes (but not always) if I start debugging the solution "cold," it doesn't find the implementations, only the Interface itself. If I go to Build -> Rebuild Solution before running it, it finds them. So I'm assuming this is an issue with Visual Studio starting up the WebHost Project without rebuilding the other class library projects.

So I have a few questions here.

  1. Am I right about the cause here?
  2. How can I stop this?
  3. As of right now, can this happen in production? I've deployed it to a test site and it seemed to work, but that could just be luck since sometimes it does find the concrete implementations anyway.
Pharylon
  • 9,796
  • 3
  • 35
  • 59

5 Answers5

6

AppDomain.CurrentDomain.GetAssemblies() will only return assemblies which have been loaded into the current app domain, an assembly is loaded when a type from that assembly is used.

Whenever I have needed to do this in the past, I have simply maintained a list of assemblies, i.e.

var assemblies = new [] 
{
    typeof(TypeFromAssemblyA).Assembly,
    typeof(TypeFromAssemblyB).Assembly
};

This only needs to contain one type from each assembly to ensure that assembly gets loaded into the current app domain.

Another option is to force all referenced assemblies to be loaded using Assembly.GetReferencedAssemblies() and Assembly.Load(AssemblyName). See this question for more details about this approach. You may then use your existing code after this point as all of the assemblies will have been loaded into the app domain.

A third option would be to make use of something like the Managed Extensibility Framework (MEF) to Export and subsequently Import types from assemblies in a directory.

Which option you choose will depend upon your solution structure and how you want implementations to be discovered. I like the first approach as it is explicit about which assemblies will be scanned, but the last approach allows for much more extensibility without recompilation.

Community
  • 1
  • 1
Lukazoid
  • 19,016
  • 3
  • 62
  • 85
1

Here's a method I usually have in a utility class to do just that.

    public static List<Type> GetTypes<T>()
    {
        var results = new List<Type>();
        var assemblies = AppDomain.CurrentDomain.GetAssemblies();
        foreach (var assembly in assemblies)
        {
            var types = assembly.GetTypes()
                .Where(t => t.IsAbstract == false
                    && (typeof(T).IsInterface == false || t.GetInterfaces().Contains(typeof(T)))
                    && (typeof(T).IsClass == false || t.BaseType == typeof(T)))
                .ToList();
            results.AddRange(types);
        }
        return results;
    }
Jason W
  • 13,026
  • 3
  • 31
  • 62
  • What would make this code work where mine wouldn't? I don't see it doing anything particularly different, besides filtering out interfaces and such (which mine does do in practice, by the way, I was just presenting a simplified version). Really, I'm not sure any of my three questions are answered by this. – Pharylon Nov 20 '14 at 14:15
  • 1
    The AppDomain.CurrentDomain doesn't always have external class libraries loaded until you reference an object from the class libraries. You can either manually load those before you call this type of a method or put your code within the class library. I would expect your issue to occur in production, but it would begin working once someone hit code from the class library forcing that library to load into the current domain. – Jason W Nov 20 '14 at 14:35
0

First of all, you can't get all implementations of a class from every class library, there is always a possibility for class implementation to exist only in dll that is stored on thumb drive that is fall under the desk. Therefore, you have to determine source for those implementations to be looked for.

Your implementation has current AppDomain as the source, hence looking for implementations, that are already loaded by runtime. I guess that you are looking for those implementations, that are not loaded as well. If so, you have to limit yourself to some set of directories (or load that set description from external configuration ) to look for libraries that contain implementations ( Environment.CurrentDirectory may suffice). You can have two cases

You now about every implementation

For example, you can store a list of assembly names and full type identifiers in database or configuration file. If so, you can try to locate these specific assemblies and load desired implementations through Reflection API. There are, however, problems with incorrect information: you can have implementation, that are not mentioned in your list, or, other way around, you can have information in your list about implementation, that does not exist in runtime. Also, you can have auxiliary dll which references every implementation, so when you load it - you'll load every implementation into AppDomain as well ( from this point, you can use your initial solution)

You don't now about every implementation

In that case, there is only one way. For each dll in source directory:

  • Try to load assembly from that dll into current AppDomain
  • Get all types, defined in assembly to see if there is the expected implementation
  • If there is, load it into desired container. If not - unload the assembly, otherwise you'll have huge memory overhead without any benefit.

If you can enforce additional code support for implementations (it's your own code, or you have strict guideline for third-party implementors) you can use Management Extensibility Framework to handle this task for you: you will have to mark every implementation by Export attribute, define composition catalog to look for libraries containing implementations, and launch composition to load every implementation to composition container marked by ImportMany attribute. Otherwise, you will have to accurately implement these operations yourself, but beware of problematic situations:

  • Assembly can fail to load - so watch for exceptions
  • Assembly can reference another assembly, so when it is loaded, referenced assembly is loaded is well. It is easy to miss these assemblies when checking for implementations


I, personally, prefer to use second way even in first case, because I don't like an idea of introducing implicit dependency on children into parent.

Aloraman
  • 1,389
  • 1
  • 21
  • 32
0

You can use Assembly.LoadAssembly to load assemblies at runtime if they have not already been loaded. Very simple example:

        List<Assembly> loadedAssemblies = new List<Assembly>();
        DirectoryInfo di = new DirectoryInfo("path to my assemblies");

        foreach (FileInfo fi in di.GetFiles("*.dll"))
        {
            try
            {
                loadedAssemblies.Add(Assembly.LoadFrom(fi.FullName));
            }
            catch (Exception ex)
            {
                // handle problems loading the assemblies here - there are a boatload of possible failures
            }
        }

        foreach (Assembly a in loadedAssemblies)
        {
            // Use reflection to do whatever it is that you wanted to do
        }

Additional documentation is available on MSDN: http://msdn.microsoft.com/en-us/library/1009fa28%28v=vs.110%29.aspx

DVK
  • 2,726
  • 1
  • 17
  • 20
0

Simply use Scrutor's third-party library.

 services.Scan(scan => scan
.AddTypes(typeof(IRequest<>))
.AsSelf()
.WithScopedLifetime());
Ahmad Aghazadeh
  • 16,571
  • 12
  • 101
  • 98