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.