Finding Types at Runtime in .NET Core
Bob Rundle
Posted on August 29, 2021
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.
Posted on August 29, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024
November 27, 2024