Introduction
When I was working on an application which had about 10 (or more) projects (including layes and plugins), I needed a way to run some tasks on application-start from plugins, layers and other projects than startup project.
My scenario as a solution was to make an abstraction for StartTask, make any number of implementations anywhere I need, in-start of application search and find all derived classes of StartTask abstraction, and then run them.
Background
We have 3 projects in our solution:
CommonProject contains shared elements StartupProject (reference to: CommonProject, AnotherProject) is the start up project! :) AnotherProject (reference to: CommonProject) is another project which needs to set some tasks on application-start
Let's Start
We have 4 steps to do:
- Creating
IStartTask interface (in CommonProject) - Creating derived classes of
IStartTask (in StartupProject and AnotherProject) - Creating
TypeDetector class (in StartupProject) - Using
TypeDetector to find all derived classes of IStartTask and running theme in application start (in StartupProject)
Step 1) Creating IStartTask Interface
IStartTask has one method named Run() to call it on start of application. It also has a read-only integer property named Order, that will tell us which derived class must be run earlier:
namespace CommonProject
{
public interface IStartTask
{
void Run();
int Order { get;}
}
}
Step 2) Creating Derived Classes of IStartTask
In StartupProject, I created the following class, which derived from IStartTask with returning 1 as Order property value:
using System;
using CommonProject;
namespace StartupProject
{
public class StartupStartTask: IStartTask
{
public void Run()
{
Console.WriteLine("This message in from STARTUP project!");
}
public int Order
{
get { return 1; }
}
}
}
And created another derived class of IStartTask in AnotherProject with 2 Order:
using System;
using CommonProject;
namespace AnotherProject
{
public class AnotherStartTask : IStartTask
{
public void Run()
{
Console.WriteLine("This message in from ANOTHER project!");
}
public int Order
{
get { return 2; }
}
}
}
Step 3) Creating TypeDetector Class
Now, let's make another class in StartupProject which can search and find all implementations of passed type (in this example: IStartupTask). I named the class TypeDetector. In DetectClassesOfType() function, we get all assemblies, look for target type in all of them and return a list of detected types:
namespace StartupProject
{
public class TypeDetector
{
public IEnumerable<Type> DetectClassesOfType(Type type)
{
var foundTypes = new List<Type>();
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (Assembly assembly in assemblies)
{
var types = assembly.GetTypes();
foreach (var t in types)
{
if (!t.IsInterface &&
!type.IsGenericTypeDefinition &&
type.IsAssignableFrom(t))
{
foundTypes.Add(t);
}
}
}
return foundTypes;
}
}
}
Some assemblies may deny GetTypes() and throw an exception, so we add a try-catch block to prevent process failing:
namespace StartupProject
{
public class TypeDetector
{
public IEnumerable<Type> DetectClassesOfType(Type type)
{
var foundTypes = new List<Type>();
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (Assembly assembly in assemblies)
{
try
{
var types = assembly.GetTypes();
foreach (var t in types)
{
if (!t.IsInterface &&
!type.IsGenericTypeDefinition &&
type.IsAssignableFrom(t))
{
foundTypes.Add(t);
}
}
}
catch (Exception)
{
}
}
return foundTypes;
}
}
}
For better performance, we exclude all system and global assemblies, third parties or any other assemblies which are impossible to contain any implementation of our types.
It's possible to extend excluded assemblies by adding more items in _excludedAssemblies array:
namespace StartupProject
{
public class TypeDetector
{
private readonly string[] _excludedAssemblies = new string[]
{
"Microsoft.CSharp",
"Microsoft.VisualStudio.Debugger.Runtime",
"Microsoft.VisualStudio.HostingProcess.Utilities",
"Microsoft.VisualStudio.HostingProcess.Utilities.Sync",
"mscorlib",
"System",
"System.Core",
"System.Data",
"System.Data.DataSetExtensions",
"System.Drawing",
"System.Windows.Forms",
"System.Xml",
"System.Xml.Linq",
"vshost32"
};
public IEnumerable<Type> DetectClassesOfType(Type type)
{
var foundTypes = new List<Type>();
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (Assembly assembly in assemblies)
{
try
{
if (!_excludedAssemblies.Contains(assembly.FullName.Split(',')[0]))
{
var types = assembly.GetTypes();
foreach (var t in types)
{
if (!t.IsInterface &&
!type.IsGenericTypeDefinition &&
type.IsAssignableFrom(t))
{
foundTypes.Add(t);
}
}
}
}
catch (Exception)
{
}
}
return foundTypes;
}
}
}
Step 4) Finding All Derived Classes of IStartTask and Run Them in Application-start
Now, we have all requirements and it's time to use them in start place. In this example, StartupProject is a Console Application and its start place is Main() method of Program.cs (In Web Applications, we can use Application_Start() method of Global.asax).
In Main() method, we must have:
namespace StartupProject
{
class Program
{
static void Main(string[] args)
{
TypeDetector typeDetector = new TypeDetector();
IEnumerable<Type> detectedClasses =
typeDetector.DetectClassesOfType(typeof(IStartTask)).ToList();
List<IStartTask> instances = new List<IStartTask>();
foreach (var detectedClass in detectedClasses)
{
var instance = (IStartTask) Activator.CreateInstance(detectedClass);
instances.Add(instance);
}
instances = instances.AsQueryable().OrderBy(t => t.Order).ToList();
foreach (var instance in instances)
{
instance.Run();
}
Console.ReadLine();
}
}
}
Now we done! and it's time to run the application:

Oops.. where is the StartTask of AnotherProject?
The problem is in step 3 where TypeDetector doesn't get the assembly of AnotherProject from AppDomain.Current.GetAssemblies().
Why? Look at this link.
Quote:
The .NET CLR uses Just-In-Time compilation. Among other things, this means it loads assemblies on first use. So, despite assemblies being referenced by an assembly in use, if the references haven't yet been needed by the CLR to execute the program, they're not loaded and so will not appear in the list of assemblies in the current AppDomain.
Where we used AnotherProject in StartupProject? Nowhere. So CLR doesn't load the assembly. What should we do? Using AnotherProject in StartupProject? It's not a good idea.
We should load every .dll file of Bin directory which is not loaded by CLR. I added a GetAssemblies() function in TypeDetector and used it in DetectClassesOType() function:
namespace StartupProject
{
public class TypeDetector
{
private readonly string[] _excludedAssemblies = new string[]
{
"Microsoft.CSharp",
"Microsoft.VisualStudio.Debugger.Runtime",
"Microsoft.VisualStudio.HostingProcess.Utilities",
"Microsoft.VisualStudio.HostingProcess.Utilities.Sync",
"mscorlib",
"System",
"System.Core",
"System.Data",
"System.Data.DataSetExtensions",
"System.Drawing",
"System.Windows.Forms",
"System.Xml",
"System.Xml.Linq",
"vshost32"
};
public IEnumerable<Type> DetectClassesOfType(Type type)
{
var foundTypes = new List<Type>();
var assemblies = GetAssemblies();
foreach (Assembly assembly in assemblies)
{
try
{
if (!_excludedAssemblies.Contains(assembly.FullName.Split(',')[0]))
{
var types = assembly.GetTypes();
foreach (var t in types)
{
if (!t.IsInterface &&
!type.IsGenericTypeDefinition &&
type.IsAssignableFrom(t))
{
foundTypes.Add(t);
}
}
}
}
catch (Exception)
{
}
}
return foundTypes;
}
private IEnumerable<Assembly> GetAssemblies()
{
List<Assembly> assemblies = AppDomain.CurrentDomain.GetAssemblies().ToList();
List<string> assembliesFullNames = new List<string>();
foreach (var assembly in assemblies)
assembliesFullNames.Add(assembly.FullName);
string binPath = AppDomain.CurrentDomain.BaseDirectory;
string[] allDllFilePaths = Directory.GetFiles(binPath, "*.dll");
foreach (var dllFilePath in allDllFilePaths)
{
var fullName = AssemblyName.GetAssemblyName(dllFilePath).FullName;
if (!assembliesFullNames.Contains(fullName))
assemblies.Add(Assembly.LoadFile(dllFilePath));
}
return assemblies;
}
}
}
Now run again:

OK! Now It Works Correctly.
Let's change the Order property of the AnotherStartTask from 2 to 0 which is smaller than 1 which is Order property of StartupStartTask. Result is:

AnotherStartTask executed before StartupStartTask.
Good job!
Points of Interest
In StartupProject, I referenced AnotherProject, but in fact, there is no need to reference it explicitly, just copying the AnotherProject.dll to bin folder of StartupProject is enough. It helps us if we have a deployed and installed application and we need to add some tasks to the application, we must create a new project and just reference CommonProject and after ending up the project, just copy the .dll file of the project to deployed and installed path and restart the application to affect without the need of changing anything in StartupProject. It may be useful in plugin-based applications.