DEV Community

Cover image for Finding Types at Runtime in .NET Core
Bob Rundle
Bob Rundle

Posted on

Finding Types at Runtime in .NET Core

One of the best features of .NET has always been the type system. In terms of rigor I place it midway between the rigidness of C++ and the anything-goes of JavaScript which in my view makes it just right. However one of my frustrations over the years has been finding types at runtime.

At compile time you find the integer type like so…

            Type t00 = typeof(int);
Enter fullscreen mode Exit fullscreen mode

But at runtime this doesn't work…

            Type t01 = Type.GetType("int");  // null
Enter fullscreen mode Exit fullscreen mode

You need to do this…

            Type t02 = Type.GetType("System.Int32");
Enter fullscreen mode Exit fullscreen mode

Similarly for other system types such as DateTime…

            Type t10 = typeof(DateTime);
            Type t11 = Type.GetType("DateTime"); // null
            Type t12 = Type.GetType("System.DateTime");
Enter fullscreen mode Exit fullscreen mode

Let's say you created your own local type…

    public class ClassA : IClassAInterface
    {
        public string Hello()
        {
            return "In main program";
        }
    }
Enter fullscreen mode Exit fullscreen mode

Which references this interface in a separate assembly…

namespace ClassAInterface
{
    public interface IClassAInterface
    {
        public string Hello();
    }
}
Enter fullscreen mode Exit fullscreen mode

Again it is simpler to find it at compile time than run time.

            Type t20 = typeof(ClassA);
            Type t21 = Type.GetType("ClassA"); // null
            Type t22 = Type.GetType("TypeSupportExamples.ClassA");
Enter fullscreen mode Exit fullscreen mode

To find it at runtime you need to specify the full name of the type which includes the namespace. This seems wrong because the code in this case is being executed in the namespace which contains the type.

Finally if the user defined type you are looking for is defined in a different assembly you need to provide the assembly name…

            Type t30 = typeof(IClassAInterface);
            Type t31 = Type.GetType("IClassAInterface"); // null
            Type t32 = Type.GetType("ClassAInterface.IClassAInterface"); //null
            Type t34 = Type.GetType("ClassAInterface.IClassAInterface, ClassAInterface");

Enter fullscreen mode Exit fullscreen mode

What is happening of course is that the reason the compiler can find types so easily is because of the using statements at the top of the file…


    using ClassAInterface;
    using System;

Enter fullscreen mode Exit fullscreen mode

The using statements provide a scope that guides the compiler to finding the right type. No such scoping mechanism exists at runtime. Instead, at runtime, scoping is provided by the container the type is in. Types are contained in assemblies which in turn are contained within load contexts which in turn are contained within app domains. This strict top down hierarchy is not required of namespaces which can span multiple assemblies. The same type name might be used in multiple assemblies and the same assembly name might be used in multiple load contexts.

Even though the same type name might be used in multiple assemblies they are seen by the runtime as distinct types even though they might be identical. I explored the ramifications of this in my previous post https://dev.to/bobrundle/forward-and-backward-compatibility-in-net-core-3c52 .

A review of type names. There are 3 for each type: simple name, full name and assembly qualified name…

            // 3 Names of a type

            Console.WriteLine(t22.Name);
            Console.WriteLine(t22.FullName);
            Console.WriteLine(t22.AssemblyQualifiedName);
Enter fullscreen mode Exit fullscreen mode
ClassA
TypeSupportExamples.ClassA
TypeSupportExamples.ClassA, TypeSupportExamples, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Enter fullscreen mode Exit fullscreen mode

I set out to make finding types at runtime easier, so I explored implementing a kind of runtime "using" statement. First I create a global dictionary of types, GlobalTypeMap, that allowed simple names for built-in types and system types and requires full types names for all others.

            Console.WriteLine();
            var globalTM = new GlobalTypeMap();
            Type t0 = globalTM.FindType("int");
            Console.WriteLine(t0.FullName);
            Type t1 = globalTM.FindType("DateTime");
            Console.WriteLine(t1.FullName);
            Type t2 = globalTM.FindType("TypeSupportExamples.ClassA");
            Console.WriteLine(t2.FullName);
Enter fullscreen mode Exit fullscreen mode
System.Int32
System.DateTime
TypeSupportExamples.ClassA
Enter fullscreen mode Exit fullscreen mode

Then create a child type map, ScopedTypeMap, where I apply using statements.


            var scopedTM1 = new ScopedTypeMap(globalTM);
            scopedTM1.UsingNamespace("TypeSupportExamples");
            Type t3 = scopedTM1.FindType("ClassA");
            IClassAInterface d0 = Activator.CreateInstance(t3) as IClassAInterface;
            Console.WriteLine();
            Console.WriteLine(d0.Hello()); // In main program
Enter fullscreen mode Exit fullscreen mode
In main program
Enter fullscreen mode Exit fullscreen mode

If new assemblies are loaded, the global type map is updated and the change is reflected in the scoped type map.

            var scopedTM2 = new ScopedTypeMap(globalTM);
            string apath0 = Path.Combine(Directory.GetCurrentDirectory(), "AssemblyA.dll");
            Assembly a0 = AssemblyLoadContext.Default.LoadFromAssemblyPath(apath0);
            scopedTM2.UsingNamespace("NamespaceA1");
            Type t4 = scopedTM2.FindType("ClassA"); // This is NamespaceA1.ClassA in AssemblyA
            IClassAInterface d1 = Activator.CreateInstance(t4) as IClassAInterface;
            Console.WriteLine(d1.Hello());
Enter fullscreen mode Exit fullscreen mode
This is NamespaceA1.ClassA in AssemblyA
Enter fullscreen mode Exit fullscreen mode

The global type map also works across multiple load contexts…

            var scopedTM3 = new ScopedTypeMap(globalTM);
            string apath1 = Path.Combine(Directory.GetCurrentDirectory(),@"AssemblyB.dll");
            AssemblyLoadContext alc0 = new AssemblyLoadContext("alc0");
            Assembly a1 = alc0.LoadFromAssemblyPath(apath1);
            scopedTM3.UsingNamespace("NamespaceB1");
            Type t5 = scopedTM3.FindType("ClassA"); // This is NamespaceB1.ClassA in AssemblyB
            IClassAInterface d2 = Activator.CreateInstance(t5) as IClassAInterface;
            Console.WriteLine(d2.Hello());
Enter fullscreen mode Exit fullscreen mode
This is NamespaceB1.ClassA in AssemblyB
Enter fullscreen mode Exit fullscreen mode

Finally we might apply namespace scope to types that have identical simple names. This is also supported.

            var scopedTM4 = new ScopedTypeMap(globalTM);
            scopedTM4.UsingNamespace("TypeSupportExamples");
            scopedTM4.UsingNamespace("NamespaceA1");
            scopedTM4.UsingNamespace("NamespaceA2");
            scopedTM4.UsingNamespace("NamespaceB1");
            Type[] tt = scopedTM4.FindTypes("ClassA");
            Console.WriteLine();
            foreach (var t in tt)
                Console.WriteLine(t.FullName);
Enter fullscreen mode Exit fullscreen mode
NamespaceA1.ClassA
NamespaceA2.ClassA
NamespaceB1.ClassA
TypeSupportExamples.ClassA

Enter fullscreen mode Exit fullscreen mode

Summary and Discussion

What I have demonstrated is a runtime type facility to allow types to be more easily found. All the code for this facility including the examples above can be found at https://github.com/bobrundle/TypeSupport

The reason I built this facility is that I want to use it for serializing types in a very lightweight way. This type serialization mechanism will be the subject of a future post.

This runtime type facility is definitely not lightweight. A Hello World program contains over 2000 types. For certain applications, however, I think it will be useful.

I did not support all of the capabilities of the .NET type system. For example load contexts can be unloaded and this properly should remove all the relevant types from the global type map. I will add that later if I need it.

I did not address generics but the runtime type facility will support them. You simply need to understand how the grammar of the type name system works. This is documented in https://docs.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/specifying-fully-qualified-type-names.

I did struggle with the issue of ambiguous types. At compile time if you try to use a simple type name that is scoped in multiple namespaces, you get an ambiguous type error. For the scoped type map, I considered throwing an exception if you tried to find a single type by name and there were more than one defined. In the end I decided not to throw an exception and to simply return the first type in a sorted list. The sort for the type list moves types in the default load context to the front of the list. Perhaps I will change my mind on this later.

I hope this post is useful. .NET types should be thoroughly understood and I was surprised how much I learned about various aspects of the type system that I already thought I thoroughly understood.

Discussion (0)