Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Implement Tasks for Executing on Application-Start from Other Modules and Layers: C# .NET

9 Jan 2016CPOL4 min read 11.2K   62  
This code makes a mechanism to implement some tasks for executing on Application-Start from anywhere.

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:

  1. Creating IStartTask interface (in CommonProject)
  2. Creating derived classes of IStartTask (in StartupProject and AnotherProject)
  3. Creating TypeDetector class (in StartupProject)
  4. 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:

C#
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:

C#
using System;
using CommonProject;

namespace StartupProject
{
    public class StartupStartTask: IStartTask
    {
        public void Run()
        {
            //Write your codes here
            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:

C#
using System;
using CommonProject;

namespace AnotherProject
{
    public class AnotherStartTask : IStartTask
    {
        public void Run()
        {
            //Write your codes here
            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:

C#
namespace StartupProject
{
    public class TypeDetector
    {
        public IEnumerable<Type> DetectClassesOfType(Type type)
        {
            var foundTypes = new List<Type>();

            //Get all assemblies and look for classes of passed type
            var assemblies = AppDomain.CurrentDomain.GetAssemblies();
            foreach (Assembly assembly in assemblies)
            {
                var types = assembly.GetTypes();
                foreach (var t in types)
                {
                    //founded types should be a simple class not Interface not Generic
                    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:

C#
namespace StartupProject
{
    public class TypeDetector
    {
        public IEnumerable<Type> DetectClassesOfType(Type type)
        {
            var foundTypes = new List<Type>();

            //Get all assemblies and look for classes of passed type
            var assemblies = AppDomain.CurrentDomain.GetAssemblies();
            foreach (Assembly assembly in assemblies)
            {
                try //Some assemblies may deny GetTypes() or any other reflection activity
                {
                    var types = assembly.GetTypes();
                    foreach (var t in types)
                    {
                        //founded types should be a simple class not Interface not Generic
                        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:

C#
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>();

            //Get all assemblies and look for classes of passed type
            var assemblies = AppDomain.CurrentDomain.GetAssemblies();
            foreach (Assembly assembly in assemblies)
            {
                try //Some assemblies may deny GetTypes() or any other reflection activity
                {
                   //Check assembly is not one of our excluded assemblies
                    //FullName example: "mscorlib, Version=4.0.0.0, 
                    //Culture=neutral, PublicKeyToken=b77a5c561934e089"
                    if (!_excludedAssemblies.Contains(assembly.FullName.Split(',')[0])) 
                    {
                        var types = assembly.GetTypes();
                        foreach (var t in types)
                        {
                            //founded types should be a simple class not Interface not Generic
                            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:

C#
namespace StartupProject
{
    class Program
    {
        static void Main(string[] args)
        {

            TypeDetector typeDetector = new TypeDetector();

            //Get all derived classes of IStartTask
            IEnumerable<Type> detectedClasses = 
		typeDetector.DetectClassesOfType(typeof(IStartTask)).ToList();

            //Create instances of detectedClasses
            List<IStartTask> instances = new List<IStartTask>();
            foreach (var detectedClass in detectedClasses)
            {
                var instance = (IStartTask) Activator.CreateInstance(detectedClass);
                instances.Add(instance);
            }

            //sorting instances by Order
            instances = instances.AsQueryable().OrderBy(t => t.Order).ToList();

            //execute all Run() methods of instances
            foreach (var instance in instances)
            {
                instance.Run();
            }

            Console.ReadLine();
        }
    }
}

Now we done! and it's time to run the application:

first run

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:

C#
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>();
           
            //Get all assemblies and look for classes of passed type
            var assemblies = GetAssemblies();
            foreach (Assembly assembly in assemblies)
            {
                try //Some assemblies may deny GetTypes() or any other reflection activity
                {
                    //Check assembly is not one of our excluded assemblies
                   //FullName example: "mscorlib, Version=4.0.0.0, 
                   //Culture=neutral, PublicKeyToken=b77a5c561934e089"
                    if (!_excludedAssemblies.Contains(assembly.FullName.Split(',')[0])) 
                    {
                        var types = assembly.GetTypes();
                        foreach (var t in types)
                        {
                            //founded types should be a simple class not Interface not Generic
                            if (!t.IsInterface &&
                                !type.IsGenericTypeDefinition &&
                                type.IsAssignableFrom(t))
                            {
                                foundTypes.Add(t);
                            }
                        }
                    }
                }
                catch (Exception)
                {
                    
                }
            }

            return foundTypes;
        }

        private IEnumerable<Assembly> GetAssemblies()
        {
            //Get all assemblies loaded by CLR
            List<Assembly> assemblies = AppDomain.CurrentDomain.GetAssemblies().ToList();

            //Make a list of Fullnames of loaded assembly
            List<string> assembliesFullNames = new List<string>();
            foreach (var assembly in assemblies)
                assembliesFullNames.Add(assembly.FullName);
            
            //Get Bin folder path
            string binPath = AppDomain.CurrentDomain.BaseDirectory;
            
            //Get paths of all .dll files which located in Bin folder
            string[] allDllFilePaths = Directory.GetFiles(binPath, "*.dll");

            foreach (var dllFilePath in allDllFilePaths)
            {
                //Get fullname of .dll file assembly
                var fullName = AssemblyName.GetAssemblyName(dllFilePath).FullName;
                //Check if the assembly is not loaded yet then load it.
                if (!assembliesFullNames.Contains(fullName))
                    assemblies.Add(Assembly.LoadFile(dllFilePath));
            }
            
            return assemblies;
        }
    }
}

Now run again:

Image 2

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:

Image 3

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)