How to get a Type from a C# type name string?

10.9k views Asked by At

I've been looking at return values for Type.Namespace, Type.Name, Type.FullName, and Type.AssemblyQualifiedName. There are inconsistencies.

For an inner class like ConsoleApplication8.Program+InnerClass, Namespace returns ConsoleApplication8 and Name returns InnerClass, omitting Program, so concatenating Type.NameSpace and Type.Name would be an incomplete representation of the class name (just as an example).

Even the FullName property is inconsistent. Although it omits assembly name and returns ConsoleApplication8.Program+InnerClass for such an inner class, FullName includes the assembly name in the generic arguments for a type such as List<long> (even though it's omitted for the outer generic type itself, so I guess there is some level of consistency there).

I am currently using this code with a cached type name lookup that uses the CodeDom to produce real C# code names. Basically, I'm trying to reverse the process to get the type, given a real class name.

static System.Collections.Concurrent.ConcurrentDictionary<Type, string> typeNameCache = new System.Collections.Concurrent.ConcurrentDictionary<Type, string>();

static string GetTypeName(Type type)
{
    string name;
    if (!typeNameCache.TryGetValue( type, out name ))
    {
        var codeDomProvider = CodeDomProvider.CreateProvider("C#");
        var typeReferenceExpression = new CodeTypeReferenceExpression(new CodeTypeReference(type));
        using (var writer = new StringWriter())
        {
            codeDomProvider.GenerateCodeFromExpression(typeReferenceExpression, writer, new CodeGeneratorOptions());
            name = writer.GetStringBuilder().ToString();
        }
        typeNameCache.TryAdd( type, name );
    }
    return name;
}

The above function produces friendly C# name like System.Collections.Generic.List<long>. But it also produces names like ConsoleApplication8.Program.InnerClass (i.e. it uses a dot instead of a plus sign between Program and InnerClass). The problem is that calling Type.GetType(name) won't work, because it would require the plus sign to be there, and in addition it sometimes requires the assembly name.

So how can I get a references to a Type object, given a friendly C# class name as it would be referenced in code?

2

There are 2 answers

2
drew_w On

So, basically per our conversation in the comments the goal here is to get a type object (for whatever purpose) from a name of the type. Calling "Type.GetType()" works for simple types but for generics and other types it doesn't work because the name needs to be qualified. The key, then is to use the code compiler to actually have the C# code engine find and get the type. The following is a working program that does just that:

using System;
using System.Reflection;
using Microsoft.CSharp;
using System.CodeDom.Compiler;

namespace SimpleCompileTest
{
    class Program
    {
        public static void Main(string[] args)
        {
            string typeName = "System.Collections.Generic.List<int>";
            Type theType = GetTypeFromName(typeName);
        }

        private static Type GetTypeFromName(string typeName)
        {
            // double open and close are for escape purposes
            const string typeProgram = @"using System; using System.Collections.Generic; using System.IO;
                namespace SimpleTest
                {{
                    public class Program
                    {{
                        public static Type GetItemType()
                        {{
                            {0} typeTest = new {0}();
                            if (typeTest == null) return null;
                            return typeTest.GetType();
                        }}
                    }}
                }}";

            var formattedCode = String.Format(typeProgram, typeName);

            var CompilerParams = new CompilerParameters
                {
                    GenerateInMemory = true,
                    TreatWarningsAsErrors = false,
                    GenerateExecutable = false,
                    CompilerOptions = "/optimize"
                };

            string[] references = { "System.dll" };
            CompilerParams.ReferencedAssemblies.AddRange(references);

            var provider = new CSharpCodeProvider();
            CompilerResults compile = provider.CompileAssemblyFromSource(CompilerParams, formattedCode);
            if (compile.Errors.HasErrors) return null;


            Module module = compile.CompiledAssembly.GetModules()[0];
            Type mt = null; MethodInfo methInfo = null;

            if (module != null) mt = module.GetType("SimpleTest.Program");
            if (mt != null) methInfo = mt.GetMethod("GetItemType");
            if (methInfo != null) return (Type)methInfo.Invoke(null, null);

            return null;
        }
    }
}

One really important thing to note - you need to add the list of assemblies to the compiler that you hope to pull types from. That means if you have a custom type that you want to reference you need to provide that to the compiler, but otherwise, this works! Enjoy!

9
Triynko On

I've managed to achieve this now with a single line of code in each direction translating to and from friendly type names and runtime Type instances. Incredible. And some said it was not possible at all, lol. Flawless.

static Type GetType( string friendlyName )
{
    return (Type)(new CSharpCodeProvider().CompileAssemblyFromSource( new CompilerParameters( AppDomain.CurrentDomain.GetAssemblies().SelectMany<Assembly,string>( a => a.GetModules().Select<Module,string>( m => m.FullyQualifiedName )).ToArray(), null, false) {GenerateExecutable = false, GenerateInMemory = true, TreatWarningsAsErrors = false, CompilerOptions = "/optimize"}, "public static class C{public static System.Type M(){return typeof(" + friendlyName + ");}}").CompiledAssembly.GetExportedTypes()[0].GetMethod("M").Invoke( null, System.Reflection.BindingFlags.Static, null, null, null ));
}

static string GetFriendlyName( Type type )
{
    return new CSharpCodeProvider().GetTypeOutput(new CodeTypeReference(type));
}

The above code (first method only), when expanded to multiple lines looks like the following. You can just make a call like GetType("System.Collections.Generic.List<int>"); and it will return a type reference.

static Type GetType( string friendlyName )
{
    var currentlyLoadedModuleNames = AppDomain.CurrentDomain.GetAssemblies().SelectMany<Assembly,string>( a => a.GetModules().Select<Module,string>( m => m.FullyQualifiedName )).ToArray();
    var csc = new CSharpCodeProvider();
    CompilerResults results = csc.CompileAssemblyFromSource(
        new CompilerParameters( currentlyLoadedModuleNames, "temp.dll", false) {
            GenerateExecutable = false, GenerateInMemory = true, TreatWarningsAsErrors = false, CompilerOptions = "/optimize"
        },
        @"public static class TypeInfo {
            public static System.Type GetEmbeddedType() {
            return typeof(" + friendlyName + @");
            }
        }");
    if (results.Errors.Count > 0)
        throw new Exception( "Error compiling type name." );
    Type[] type = results.CompiledAssembly.GetExportedTypes();
    return (Type)type[0].GetMethod("GetEmbeddedType").Invoke( null, System.Reflection.BindingFlags.Static, null, null, null );
}

Update: I added a line to make all the modules loaded in the current app domain available to the compiler. That should guarantee that this is able to acquire any type by name as if you had referenced it directly in your code, with the exception that the desired types have to be public, since they are essentially referenced from an external assembly from within the compiler rather than directly in the currently executing assembly.

Just as as test, the returned type should work in a cache, since:

Type t = GetType( "System.Collections.Generic.List<int>" );
Console.WriteLine( typeof(System.Collections.Generic.List<int>) == t );
//RETURNS TRUE