129

I've got a "diagnostics" page in my ASP.NET application which does things like verify the database connection(s), display the current appSettings and ConnectionStrings, etc. A section of this page displays the Assembly versions of important types used throughout, but I could not figure out how to effectively show the versions of ALL of the loaded assemblies.

What is the most effective way to figure out all currently referenced and/or loaded Assemblies in a .NET application?

Note: I'm not interested in file-based methods, like iterating through *.dll in a particular directory. I am interested in what the application is actually using right now.

starblue
  • 55,348
  • 14
  • 97
  • 151
Jess Chadwick
  • 2,373
  • 2
  • 21
  • 24

2 Answers2

203

Getting loaded assemblies for the current AppDomain:

var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();

Getting the assemblies referenced by another assembly:

var referencedAssemblies = someAssembly.GetReferencedAssemblies();

Note that if assembly A references assembly B and assembly A is loaded, that does not imply that assembly B is also loaded. Assembly B will only be loaded if and when it is needed. For that reason, GetReferencedAssemblies() returns AssemblyName instances rather than Assembly instances.

Kent Boogaart
  • 175,602
  • 35
  • 392
  • 393
  • 2
    Well I need something like this - given a .net solution I want to find out all the referenced assemblies in all the projects. Ideas? – Kumar Vaibhav Jul 09 '13 at 06:01
  • Please note that both methods only list dll's that are actually used. Obviously it makes no sense to have references in solutions that are not used, but this might be confusing when someone is trying to speculatively scan through ALL assemblies. All assemblies might just not show up. – Pompair Nov 06 '13 at 16:01
  • 3
    OP asks _currently loaded_ assemblies not referenced assemblies. This answers the question. Exactly what I was looking for. – MikeJansen Oct 25 '16 at 19:06
  • 1
    event for know when Assembly B is loaded? – Kiquenet Aug 08 '19 at 15:49
28

This extension method gets all referenced assemblies, recursively, including nested assemblies.

As it uses ReflectionOnlyLoad, it loads the assemblies in a separate AppDomain, which has the advantage of not interfering with the JIT process.

You'll notice that there is also a MyGetMissingAssembliesRecursive. You can use this to detect any missing assemblies that are referenced, but not present in the current directory for some reason. This is incredibly useful when using MEF. The return list will give you both the missing assembly, and who owns it (its parent).

/// <summary>
///     Intent: Get referenced assemblies, either recursively or flat. Not thread safe, if running in a multi
///     threaded environment must use locks.
/// </summary>
public static class GetReferencedAssemblies
{
    static void Demo()
    {
        var referencedAssemblies = Assembly.GetEntryAssembly().MyGetReferencedAssembliesRecursive();
        var missingAssemblies = Assembly.GetEntryAssembly().MyGetMissingAssembliesRecursive();
        // Can use this within a class.
        //var referencedAssemblies = this.MyGetReferencedAssembliesRecursive();
    }

    public class MissingAssembly
    {
        public MissingAssembly(string missingAssemblyName, string missingAssemblyNameParent)
        {
            MissingAssemblyName = missingAssemblyName;
            MissingAssemblyNameParent = missingAssemblyNameParent;
        }

        public string MissingAssemblyName { get; set; }
        public string MissingAssemblyNameParent { get; set; }
    }

    private static Dictionary<string, Assembly> _dependentAssemblyList;
    private static List<MissingAssembly> _missingAssemblyList;

    /// <summary>
    ///     Intent: Get assemblies referenced by entry assembly. Not recursive.
    /// </summary>
    public static List<string> MyGetReferencedAssembliesFlat(this Type type)
    {
        var results = type.Assembly.GetReferencedAssemblies();
        return results.Select(o => o.FullName).OrderBy(o => o).ToList();
    }

    /// <summary>
    ///     Intent: Get assemblies currently dependent on entry assembly. Recursive.
    /// </summary>
    public static Dictionary<string, Assembly> MyGetReferencedAssembliesRecursive(this Assembly assembly)
    {
        _dependentAssemblyList = new Dictionary<string, Assembly>();
        _missingAssemblyList = new List<MissingAssembly>();

        InternalGetDependentAssembliesRecursive(assembly);

        // Only include assemblies that we wrote ourselves (ignore ones from GAC).
        var keysToRemove = _dependentAssemblyList.Values.Where(
            o => o.GlobalAssemblyCache == true).ToList();

        foreach (var k in keysToRemove)
        {
            _dependentAssemblyList.Remove(k.FullName.MyToName());
        }

        return _dependentAssemblyList;
    }

    /// <summary>
    ///     Intent: Get missing assemblies.
    /// </summary>
    public static List<MissingAssembly> MyGetMissingAssembliesRecursive(this Assembly assembly)
    {
        _dependentAssemblyList = new Dictionary<string, Assembly>();
        _missingAssemblyList = new List<MissingAssembly>();
        InternalGetDependentAssembliesRecursive(assembly);

        return _missingAssemblyList;
    }

    /// <summary>
    ///     Intent: Internal recursive class to get all dependent assemblies, and all dependent assemblies of
    ///     dependent assemblies, etc.
    /// </summary>
    private static void InternalGetDependentAssembliesRecursive(Assembly assembly)
    {
        // Load assemblies with newest versions first. Omitting the ordering results in false positives on
        // _missingAssemblyList.
        var referencedAssemblies = assembly.GetReferencedAssemblies()
            .OrderByDescending(o => o.Version);

        foreach (var r in referencedAssemblies)
        {
            if (String.IsNullOrEmpty(assembly.FullName))
            {
                continue;
            }

            if (_dependentAssemblyList.ContainsKey(r.FullName.MyToName()) == false)
            {
                try
                {
                    var a = Assembly.ReflectionOnlyLoad(r.FullName);
                    _dependentAssemblyList[a.FullName.MyToName()] = a;
                    InternalGetDependentAssembliesRecursive(a);
                }
                catch (Exception ex)
                {
                    _missingAssemblyList.Add(new MissingAssembly(r.FullName.Split(',')[0], assembly.FullName.MyToName()));
                }
            }
        }
    }

    private static string MyToName(this string fullName)
    {
        return fullName.Split(',')[0];
    }
}

Update

To make this code thread safe, put a lock around it. It's not currently thread safe by default, as it references a shared static global variable to do its magic.

Contango
  • 76,540
  • 58
  • 260
  • 305
  • I just rewrote this to be thread safe, so it can be called from many different threads simultaneously (not sure why you would want that, but hey, its safer). Let me know if you want me to post the code. – Contango Oct 27 '14 at 19:56
  • 2
    @Contango Could you post your Thread safe version, or if you have written a blog about it, post that? – Robert Mar 11 '15 at 14:05
  • 2
    The naive way to make this thread safe is to put a `lock` around the whole thing. The other method that I used eliminated the dependency on the global static "_dependentAssemblyList", so it becomes thread safe without needing a `lock`, which has some slight speed advantages if multiple threads are trying to simultaneously determine what assemblies are missing (this is a bit of a corner case). – Contango Mar 12 '15 at 21:33
  • 3
    adding a `lock` isn't going to add much in the way of "thread safe". Sure, that makes that block of code execute only one at a time; but other threads can load assemblies any time they like and that could cause problems with some of the `foreach` loops. – Peter Ritchie Mar 12 '15 at 21:39
  • 1
    @Peter Ritchie There is a shared static global variable which is used during recursion, so adding a lock around all accesses to that will make that portion thread safe. It's just good programming practice. Usually all required assemblies are loaded on startup, unless something like MEF is used, so thread safety is not really an issue in practice. – Contango Mar 14 '15 at 22:17
  • MEF issues with assemblies ? – Kiquenet Aug 08 '19 at 15:48
  • ReflectionOnlyLoad is not available in .net core 3.1/.net 5 and causes PlatformNotSupportedException – magicandre1981 Oct 28 '20 at 07:37
  • when I use [MetadataLoadContext](https://learn.microsoft.com/en-us/dotnet/standard/assembly/inspect-contents-using-metadataloadcontext), I later get MetadataLoadContextDisposed (this object is no longer valid because the MetadataLoadContext that created it has been disposed) when I store that list and enum it later. Do you have any idea how to get this working in .net core 3.1? – magicandre1981 Oct 28 '20 at 08:07
  • ok, I need to keep the MetadataLoadContext object alive and avoid the using statement and manually dispose it before I close my application now I can query the list and do what I want like with your old solution that worked fine for years in classic .net framework – magicandre1981 Oct 29 '20 at 09:32