Skip to content

Instantly share code, notes, and snippets.

@cajuncoding
Last active April 1, 2020 16:41
Show Gist options
  • Save cajuncoding/d1c1174c552bff6cf9ae07631c4be299 to your computer and use it in GitHub Desktop.
Save cajuncoding/d1c1174c552bff6cf9ae07631c4be299 to your computer and use it in GitHub Desktop.
Need to search for Attributed classes in your assembly and other related assemblies the local bin folder? This small class provides low level plumbing for things like Dynamic Factory methods (using Attributes), dynamic Plugins, etc.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace System.Reflection.AttributeSearch
{
/// <summary>
/// BBernard
/// Generic helper to dynamically find, filter, and initialize classes based on Custom Attributes.
/// NOTE: This supports the forced eager loading of local class libraries in the same folder as the
/// specified Assembly to ensure that related assemblies are loaded and available to be searched.
/// NOTE: This helper has no coupling to any other business logic, and is intended ONLY to encapsulate the work
/// needed to work with the .Net reflection and processing of attributes and related assemblies.
/// </summary>
public static class AssemblyAttributeSearch<TAttribute> where TAttribute: Attribute
{
public const string CLASS_LIBRARY_FILE_EXTENSION = "dll";
private static readonly ConcurrentDictionary<string, Lazy<List<AttributedClassInfo<TAttribute>>>> attributedClassCache = new ConcurrentDictionary<string, Lazy<List<AttributedClassInfo<TAttribute>>>>();
/// <summary>
/// BBernard
/// Convenience Method for searching through all Types in the Application Domain for the specified Class Type
/// & Attribute generic type arguments.
/// </summary>
/// <typeparam name="TClassFilter"></typeparam>
/// <typeparam name="TAttributeFilter"></typeparam>
/// <returns></returns>
public static List<AttributedClassInfo<TAttribute>> FindAllAttributedClasses(Assembly rootAssembly, Type classFilterType, bool forceLoadLocalClassLibraries = true)
{
var rootFolder = new FileInfo(rootAssembly.Location).Directory;
return FindAllAttributedClasses(rootFolder, classFilterType, forceLoadLocalClassLibraries);
}
/// <summary>
/// BBernard
/// Convenience Method for searching through all Types in the Application Domain for the specified Class Type
/// & Attribute generic type arguments.
/// </summary>
/// <typeparam name="TClassFilter"></typeparam>
/// <typeparam name="TAttributeFilter"></typeparam>
/// <returns></returns>
public static List<AttributedClassInfo<TAttribute>> FindAllAttributedClasses(DirectoryInfo rootFolder, Type classFilterType, bool forceLoadLocalClassLibraries = true)
{
var attributeFilterType = typeof(TAttribute);
var key = $"Attribute [{attributeFilterType.Name}] :: Class [{classFilterType.Name}] :: ForceLoadLibraries [{forceLoadLocalClassLibraries}]";
//BBernard
//Use the internal Lazy cache to ensure that we don't process this more than once; because we assume for our use-cases that
// the Reflection results will NOT change.
var lazyResult = attributedClassCache.GetOrAdd(key, new Lazy<List<AttributedClassInfo<TAttribute>>>(() =>
{
//FIRST we must force all local (i.e. likely referenced assemblies) to be loaded...
if (forceLoadLocalClassLibraries) ForceLoadClassLibraries(rootFolder);
//SECOND we can now access & retrieve all Types from all loaded Assemblies in the current App Domain!
//NOTE: Dynamic assemblies cannot be processeed (exceptions may occur) therefore we filter them out!
var allReferencedAssemblies = AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic);
var allReferendedTypes = allReferencedAssemblies.SelectMany(a => a.GetTypes());
// Please note that this query is a bit simplistic. It doesn't
// handle error reporting.
var resultsList = (
from type in allReferendedTypes
//BBernard - Filter by the Class/Interface type specified; all resulting Types must be instantiable (e.g. cannot be Abstract, Interface, etc.)
where classFilterType.IsAssignableFrom(type) && !type.IsAbstract && !type.IsInterface
//BBernard - Filter by the Attribute filter type specified
let attribute = type.GetCustomAttributes(attributeFilterType, false)?.FirstOrDefault() as TAttribute
where attribute != null
select new AttributedClassInfo<TAttribute>(attribute, type)
).ToList();
return resultsList;
}));
return lazyResult.Value;
}
/// <summary>
/// Force load of all local class library files that exist in the same folder as the assembly specified!
/// </summary>
/// <param name="rootAssembly"></param>
/// <returns></returns>
public static List<Assembly> ForceLoadClassLibraries(Assembly rootAssembly, bool searchFileSystemAssembliesRecursively = false)
{
var rootFolder = new FileInfo(rootAssembly.Location).Directory;
return ForceLoadClassLibraries(rootFolder, searchFileSystemAssembliesRecursively);
}
/// <summary>
/// Force load of all local class library files that exist in the root folder specified!
/// </summary>
/// <param name="rootFolder"></param>
/// <returns></returns>
public static List<Assembly> ForceLoadClassLibraries(DirectoryInfo rootFolder, bool searchFoldersRecursively = false)
{
var searchOption = searchFoldersRecursively ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
var localClassLibraryFilePaths = rootFolder.GetFiles($"*.{CLASS_LIBRARY_FILE_EXTENSION}", searchOption).ToList();
return ForceLoadClassLibraries(localClassLibraryFilePaths);
}
/// <summary>
/// Force load of all local class library files in the list specified. This overload puts the control of creating the list
/// of assembly file paths in control of the caller with custom logic!
/// </summary>
/// <param name="rootAssembly"></param>
/// <returns></returns>
public static List<Assembly> ForceLoadClassLibraries(List<FileInfo> filePathsList)
{
var loadedAssemblyList = new List<Assembly>();
if (filePathsList == null || !filePathsList.Any()) return loadedAssemblyList;
//BBernard
//Eager/Greedily load all local class library references; the default .Net behaviour is to Lazy load on-demand only.
//NOTE: We must force them to load so that we can access their types via Reflection! Not all assemblies will be visible/avialable
// to Reflection API's until they are first loaded up-front here...
//NOTE: Adapted and optimized from code found in the best viable info. on doing this at the StackOverflow post here:
// https://stackoverflow.com/a/2384679/7293142
//NOTE: Dynamic assemblies cannot be force loaded (exceptions will occur) therefore we must filter them out!
var appDomainLoadedAssemblyPaths = AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic).Select(a => a.Location);
var assemblyNamesToLoad = filePathsList
.Where(f => f.Extension.Equals($".{CLASS_LIBRARY_FILE_EXTENSION}", StringComparison.OrdinalIgnoreCase) && f.Exists)
.Select(f => f.FullName)
.Except(appDomainLoadedAssemblyPaths, StringComparer.OrdinalIgnoreCase)
.Select(p => AssemblyName.GetAssemblyName(p));
foreach (var assemblyName in assemblyNamesToLoad)
{
var newAssembly = AppDomain.CurrentDomain.Load(assemblyName);
loadedAssemblyList.Add(newAssembly);
}
//Return the list of all assemblies loaded by this process...
return loadedAssemblyList;
}
}
/// <summary>
/// BBernard
/// Wrapper class to encapsulate the results of a unique combination of Attribute & Class,
/// and provide convenience methods for that particular combination (e.g. CreateInstance()).
/// </summary>
/// <typeparam name="TAttribute"></typeparam>
public class AttributedClassInfo<TAttribute> where TAttribute : class
{
public AttributedClassInfo(TAttribute attribute, Type instantiationClassType)
{
this.InstanticationClassType = instantiationClassType;
this.Attribute = attribute;
}
public TAttribute Attribute { get; protected set; }
public Type InstanticationClassType { get; protected set; }
public TClass CreateInstance<TClass>()
{
var instance = Activator.CreateInstance(this.InstanticationClassType) ?? default(TClass);
return (TClass)instance;
}
public override string ToString()
{
return $"Attribute [{this.Attribute?.ToString()}]; Class [{this.InstanticationClassType?.Name}]";
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment