Blog Archives

Reflection (part 3, advanced, professional), plugin

We are progressing further.

Reflection tells us all we need for plugins. Once we know the assembly we can examine it like a poor frog on the dissection table. If the dissector cannot find something, then it is not the fault of the frog. Sorry for the macabre metaphor, I could not resist my ugly thoughts.

First of all we need an assembly. In the Solution Explorer right-click your Project. The tab-menu shows (Application, Build, Build Events, Debug …). Select Application. The first entry is “Assembly name:”. You can enter your prefered assembly name here. Let’s use “TestAssembly” for our example. Now the compiler will create an assembly with the name TestAssembly.

A short test program gives you an idea where your assembly is stored and what its name is.

Assembly lAssembly = Assembly.GetCallingAssembly();
Console.WriteLine(lAssembly.Location);  // prints the full path of your assembly
Console.WriteLine(lAssembly.FullName);

You can load assemblies into applications. We are concentrating on plugins today.
In theory you can get all required information to eg. call methods or properties via reflection itself. There is no need for extra definition files. But you can also make your life easier and predefine signatures, which are used by both the application and the plugin. Of course you are implementing constraints that reduce the flexibility, but on the other hand you reduce the error-proneness and save a lot of debugging time.
So we are going to use an interface to predefine signatures. It is important that you do not copy the interface source code into both projects. You should compile the interface into a DLL-library and access these definitions from outside in the two assemblies. Otherwise the compilers generate different identifiers (see GUID) and two equal interfaces cannot be recognized as the same type.

To clearly show the independence I want you to open Visual Studio three times. Please do not create three projects in one single solution.

Visual Studio 1

Create a library project and paste the following code. We don’t need any “using” or “namespace”. Five lines of code are enough in our example.

public interface IPlugin {
    string Name { get; }
    int Born { get; }
    double DoSomething(double d, string s);
} // interface

Compile the project and move the library DLL-file to your preferred location.

Visual Studio 2

Add a reference to our interface library. (Right click on References in your Solution Explorer. Add Reference, then browse for the library file.)
We are creating the plugin now. The method Test() prints a path that you will need in the source code for the calling application (just to make your life easier).

public class MyPlugin1 : IPlugin {
    public string Name { get { return "Diana Frances"; } }
    public int Born { get { return 1961; } }
    public double DoSomething(double d, string s) { return 1.0; }
} // class

public class MyPlugin2 : IPlugin {
    public string Name { get { return "Jeanne d'Arc"; } }
    public int Born { get { return 1412; } }
    public double DoSomething(double d, string s) { return 1.0; }
} // class

public static class TestClass {
    public static void Test() {
        Assembly lPluginAssembly = Assembly.GetCallingAssembly();
        // Assembly lPluginAssembly = Assembly.Load("TestAssembly");  // alternative
        Console.WriteLine(lPluginAssembly.Location);
        Console.WriteLine(lPluginAssembly.FullName);
        Console.ReadLine();
    } //
} // class

Visual Studio 3

The projects in Visual Studio 1 and Visual Studio 2 are compiled. You can close these two programs.
Once again add a reference to our interface library. In below example code you need to replace a path by the one, which was printed out in Visual Studio 2. (in line Assembly lPluginAssembly = Assembly.LoadFile(@”xxxxxx\TestAssembly.exe”);)

static void Main(string[] args) {
    Assembly lPluginAssembly = Assembly.LoadFile(@"xxxxxx\TestAssembly.exe");
    //Assembly lPluginAssembly = Assembly.Load("TestAssembly");   => if loaded already

    var lPlugins = from t in lPluginAssembly.GetTypes()
                    where typeof(IPlugin).IsAssignableFrom(t) && !t.IsInterface
                    select t;

    foreach (Type t in lPlugins) {
        IPlugin lPlugin = Activator.CreateInstance(t) as IPlugin;
        Console.WriteLine(lPlugin.Name + " was born in " + lPlugin.Born);
        double d = lPlugin.DoSomething(2.0, "hello");
    }
    Console.ReadLine();
} // main

output example:
Diana Frances was born in 1961
Jeanne d’Arc was born in 1412

The program loaded the assembly, created instances for each class and called the properties and the method. This basically is all you need for Plugins.

Of course there are also dirty ways to invoke plugin methods. One is:

Assembly lPluginAssembly = Assembly.LoadFile(@"xxxxxx\TestAssembly.exe");
Type t = lPluginAssembly.GetType("MyNamespaceName.MyPlugin1");
MethodInfo m = t.GetMethod("DoSomething");
double d = (double)m.Invoke(Activator.CreateInstance(t), new object[] { 2.0, "hello" });                       
Advertisements

Reflection (part 2, advanced), attributes

In my post Reflection I covered simple reflection to follow up on generic classes.

Each application in the .Net environment is not only consisting of the program code itself, it also consists of information about the code. This information is stored in the so-called metadata. I am going to show how to read metadata attributes at runtime today.

Attributes can be applied to: Assemblies, classes, class members, constructors, delegates, enums, events, fields, interfaces, methods, modules, parameters, properties, return values and structs. You can read attributes at runtime and act accordingly. They are like information tags.

Maybe a metaphor helps to describe attributes easily. When you go shopping and buy food you can find additional information on nearly all products:
– weight
– price
– expiry date
– nutritional values (eg. vitamins, proteins, fat, sugar)
– the brand
– any information the producer wants to let you know

This is only additional data. This is not the product itself.

Attributes do not change the code execution unless actively intended by a programmer. Some attributes are read by compilers (eg. [Flags] for enum), some by applications or libraries (eg. in JSON [DataContract]).
In source codes attributes are placed in square brackets [] above the declaration that you want the attributes to apply to. For instance the often applied [Serializable] attribute of the SerializableAttribute class indicates that a type can be serialized to be stored or transmitted somewhere else.

[Serializable]
class Animal {
    public string Name { get; set; }
    public int Age { get; set; }
} 

You can use multiple attributes and some can even be applied multiple times. By convention, all attribute names end with the word Attribute to distinguish them from other items. However, you do not need to specify the Attribute suffix when using attributes in source codes. For example, you can specify DescriptionAttribute as follows:

[Serializable]
[Description("http://www.GreenPeace.com")]
class Animal {
    public string Name { get; set; }
    public int Age { get; set; }
} 

// same as the following syntax
[Serializable, Description("http://www.GreenPeace.com")]
class Animal {
    public string Name { get; set; }
    public int Age { get; set; }
} 

// same as the following syntax
[Serializable, DescriptionAttribute("http://www.GreenPeace.com")]  // [Description] == [DescriptionAttribute]
class Animal {
    public string Name { get; set; }
    public int Age { get; set; }
}

// a custom attribute that would be used multiple times
[Changes(Version=1.1, Change="Added Age")]
[Changes(Version=1.2, Change="Added Gender")]
class Animal {
    public enum eGender { male, female, dunno };

    public string Name { get; set; }
    public int Age { get; set; }
    public eGender Gender { get; set; }
} 

You can specify the target of an attribute explicitly. The following code applies all attributes to the current assembly.

[assembly: AssemblyTitle("HelloWorld")]
[assembly: AssemblyDescription("Chapter1")]
[assembly: AssemblyCompany("Hardcore Ltd.")]

The System.Attribute class is the lowest base class for all attribute classes. It has a few static methods.

[Serializable]
public class Animal {
    public string Name { get; set; }
    public int Age { get; set; }

    public static bool IsSerializable {
        get { return Attribute.IsDefined(typeof(Animal), typeof(SerializableAttribute)); }  // returns true
    } //
} // class

How to read the property of attributes?

[Description("Only cute dogs"), Serializable]
public class Dogs {
    public static void PrintProperties() {
        Attribute[] lAttributes = Attribute.GetCustomAttributes(typeof(Dogs)) as Attribute[];
        foreach (Attribute lAttribute in lAttributes) {
            Console.WriteLine(lAttribute.GetType().Name.ToString());
            if (lAttribute is DescriptionAttribute) Console.WriteLine(((DescriptionAttribute)lAttribute).Description);
        }
        Console.ReadLine();
    } //
} // class

example output:
SerializableAttribute
DescriptionAttribute
Only cute dogs

To build our own attribute we have to inherit from an existing attribute, which is at least System.Attribute. The next code example is more complex. We use multiple attributes for AnyClass.Method3() and permit this with AllowMultiple = true in our AttributeUsage. On the same line we limit the usage of our custom attribute to classes, methods and properties. The ProgrammerAttribute class does nothing else than storing the values. There is no extensive code as you might expect from real program code.
By calling typeof(AnyClass).GetMethods() we enter the method level. The flags are set the way that we receive public and private methods. The result also includes inherited methods.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true)]
public class ProgrammerAttribute : Attribute {
    private readonly DateTime _Date;
    private readonly string _Name;
    private readonly string _Info;

    public ProgrammerAttribute(int xYear, int xMonth, int xDay, string xName, string xInfo) {
        _Name = xName;
        _Date = new DateTime(xYear, xMonth, xDay);
        _Info = xInfo;
    } // constructor

    public string Name { get { return _Name; } }
    public string Info { get { return _Info; } }
    public DateTime Date { get { return _Date; } }
} //

[Serializable]
[Programmer(2014, 1, 9, "Bastian M.K. Ohta", "added")]
public class AnyClass {
    [Programmer(2014, 1, 10, "Bastian M.K. Ohta", "bug fix: threading issue solved")]
    public double Method1(double x) { return 2 * x; }

    [Programmer(2014, 1, 12, "Bastian M.K. Ohta", "added")]
    public int Method2() { return 0; }

    [Programmer(2014, 1, 12, "Bastian M.K. Ohta", "under construction")]
    [Programmer(2014, 1, 14, "Bastian M.K. Ohta", "Programmer on vacation until Friday.")]
    private void Method3(double x) { }
} //


public static void Print(ProgrammerAttribute[] xProgrammers) {
    foreach (ProgrammerAttribute p in xProgrammers) {
        Console.WriteLine(p.Date.ToString("dd MMM yy ") + p.Name + ", " + p.Info);
    }    
} //

public static void Test() {
    ProgrammerAttribute[] lProgrammers;

    Console.WriteLine("Class level:");
    lProgrammers = Attribute.GetCustomAttributes(typeof(AnyClass), typeof(ProgrammerAttribute)) as ProgrammerAttribute[];
    Print(lProgrammers);

    Console.WriteLine(Environment.NewLine + "Methods:");
    MethodInfo[] lMethodInfos = typeof(AnyClass).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
    //MethodInfo[] lMethodInfos = typeof(AnyClass).GetMethods(BindingFlags.Instance | BindingFlags.Public); // filter: only public methods
    foreach (MethodInfo m in lMethodInfos) {
        Console.WriteLine("Found method " + m.Name);
        lProgrammers = m.GetCustomAttributes(typeof(ProgrammerAttribute) , false) as ProgrammerAttribute[];
        Print(lProgrammers);
        Console.WriteLine();
    }

    Console.ReadLine();
} //

example output:
Class level:
09 Jan 14 Bastian M.K. Ohta, added

Methods:
Found method Method1
10 Jan 14 Bastian M.K. Ohta, bug fix: threading issue solved

Found method Method2
12 Jan 14 Bastian M.K. Ohta, added

Found method Method3
12 Jan 14 Bastian M.K. Ohta, am on vacation until Friday
12 Jan 14 Bastian M.K. Ohta, under construction

Found method ToString

Found method Equals

Found method GetHashCode

Found method GetType

Found method Finalize

Found method MemberwiseClone

That’s it for today. No worries, there will be many follow-ups on reflection.