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…


This content originally appeared on DEV Community and was authored by Bob Rundle

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);

But at runtime this doesn't work…

            Type t01 = Type.GetType("int");  // null

You need to do this…

            Type t02 = Type.GetType("System.Int32");

Similarly for other system types such as DateTime…

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

Let's say you created your own local type…

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

Which references this interface in a separate assembly…

namespace ClassAInterface
{
    public interface IClassAInterface
    {
        public string Hello();
    }
}

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");

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");

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;

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);
ClassA
TypeSupportExamples.ClassA
TypeSupportExamples.ClassA, TypeSupportExamples, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

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);
System.Int32
System.DateTime
TypeSupportExamples.ClassA

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
In main program

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());
This is NamespaceA1.ClassA in AssemblyA

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());
This is NamespaceB1.ClassA in AssemblyB

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);
NamespaceA1.ClassA
NamespaceA2.ClassA
NamespaceB1.ClassA
TypeSupportExamples.ClassA

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.


This content originally appeared on DEV Community and was authored by Bob Rundle


Print Share Comment Cite Upload Translate Updates
APA

Bob Rundle | Sciencx (2021-08-29T14:34:46+00:00) Finding Types at Runtime in .NET Core. Retrieved from https://www.scien.cx/2021/08/29/finding-types-at-runtime-in-net-core/

MLA
" » Finding Types at Runtime in .NET Core." Bob Rundle | Sciencx - Sunday August 29, 2021, https://www.scien.cx/2021/08/29/finding-types-at-runtime-in-net-core/
HARVARD
Bob Rundle | Sciencx Sunday August 29, 2021 » Finding Types at Runtime in .NET Core., viewed ,<https://www.scien.cx/2021/08/29/finding-types-at-runtime-in-net-core/>
VANCOUVER
Bob Rundle | Sciencx - » Finding Types at Runtime in .NET Core. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2021/08/29/finding-types-at-runtime-in-net-core/
CHICAGO
" » Finding Types at Runtime in .NET Core." Bob Rundle | Sciencx - Accessed . https://www.scien.cx/2021/08/29/finding-types-at-runtime-in-net-core/
IEEE
" » Finding Types at Runtime in .NET Core." Bob Rundle | Sciencx [Online]. Available: https://www.scien.cx/2021/08/29/finding-types-at-runtime-in-net-core/. [Accessed: ]
rf:citation
» Finding Types at Runtime in .NET Core | Bob Rundle | Sciencx | https://www.scien.cx/2021/08/29/finding-types-at-runtime-in-net-core/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.