2

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?

Triynko
  • 18,766
  • 21
  • 107
  • 173
  • 1
    Maybe I am dense, but are you trying to create an instance of a particular type or just read information about that type? This seems like an interesting question but I am having a difficult time understanding the goal here. – drew_w Dec 11 '13 at 17:36
  • It's a very straightforward question. Given a C# type name string, how can I get a Type object for it. Given a name like `System.Collections.Generic.List` or `ConsoleApplication8.Program.InnerClass`, i.e. real C# class names as they would be referenced in code, how can I get a runtime Type reference. It's for a type binding solution. – Triynko Dec 11 '13 at 17:48
  • I've built a solution that allows me to seamlessly transfer ActionScript 3 objects to .NET via JSON. Fully supports every existing AS3 type, including generic vectors like `Vector.>` and performs type mapping and friendly name translation so that `Vector.` is sent in the JSON $type member instead of `__AS3__.vec.Vector.`. Anyway, when it arrives at .NET, it's parsed by JSON.NET, transformed by a binder into a .NET type name, and there's the problem. It's fine for simple types like System.Int64, because they are static, but it needs to translate both ways. – Triynko Dec 11 '13 at 17:54
  • Correction, it was `__AS3__.vec::Vector.`. The web.config has a simple mapping of client types to server types, but such a static map only works well for simple types. To support generic types, I'm going to have take an incoming value like `Vector.` and transform it to something like ``System.Collections.Generic.List`1[[System.Int32]]``. I'm just trying to find the best way to do that. – Triynko Dec 11 '13 at 17:56
  • I just thought that maybe there was a simple way to reverse what the CodeDom does, for someone more familiar with its operations. – Triynko Dec 11 '13 at 17:59
  • Can't you just call "System.Type.GetType(name)" with a fully qualified type/assembly name? (See: http://msdn.microsoft.com/en-us/library/system.type.assemblyqualifiedname(v=vs.110).aspx) – drew_w Dec 11 '13 at 18:01
  • No, because Type.GetType requires a specific format that is not equivalent to the format for C# source code. Type.GetType would require `ConsoleApplication8.Program+InnerClass` not `ConsoleApplication8.Program.InnerClass` (i.e. it must use the plus sign for inner classes), and it would require ``System.Collections.Generic.List`1[[System.Int32]]`` not `System.Collections.Generic.List`. – Triynko Dec 11 '13 at 18:04
  • It looks like you are already using the CodeDOM - could you do something like this example: http://stackoverflow.com/questions/826398/is-it-possible-to-dynamically-compile-and-execute-c-sharp-code-fragments – drew_w Dec 11 '13 at 18:06
  • To be clear, I'm not a novice at this and I do understand the type name formats and what's required, I'm just unfamiliar with the API. It's not that I **can't** use type names like `__AS3__.vec::Vector.` and ``System.Collections.Generic.Dictionary`2[[System.Type],[System.String]]``, it's just I'd prefer to work with more friendly names (i.e. as you'd see in source code) like `Vector.` and `Dictionary`, especially within the web.config settings and the serialized JSON strings. – Triynko Dec 11 '13 at 18:07
  • If I was to compile the code, I'm wondering what it would have to look like. Perhaps some like a Program class with a static method that contains the line `return typeof(System.Collections.Generic.List)`. I would then have to load the compiled assembly and call that static method to get a type object, but then I'm wondering if the type reference would be valid in the application domain. Maybe I'd be better off just building the type names manually and pass them to Type.GetType. – Triynko Dec 11 '13 at 18:11
  • 1
    Writing an example as your thinking about it : ) – drew_w Dec 11 '13 at 18:30
  • Doesn't Microsoft uses + instead of . because otherwise how would you be able to distinguish between a namespace and nesting class? For example, what happens when you have a class named "People" in the namespace "Company.Division" and you also have a class named "People" nested in another class called "Division" which resides in the namespace "Company"? Your friendly C# name will be identical in both scenarios, and therefore impossible to convert back to the proper "People" class. – Scott Dec 11 '13 at 20:42

2 Answers2

6

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
Triynko
  • 18,766
  • 21
  • 107
  • 173
  • As of right now, I tried dropping this into the test program I wrote and the code doesn't compile. You aren't returning a type from the "GetEmbeddedType" and you aren't exporting that type from the assembly from what I can tell. **Correction:** The dynamic code doesn't compile. – drew_w Dec 11 '13 at 18:49
  • I accidentally pasted the wrong code at first. I corrected it. I have two windows open, one with my main project, and another that I was using to test and debug the code. I copied the original code at first instead of the one I debugged. Damn, I forgot a step. Just a second... – Triynko Dec 11 '13 at 18:54
  • Fixed. Instead of returning type[0], I had to return the result of calling the static GetEmbeddedType method on type[0]. I like the simplicity of calling GetExportedTypes, becaus there's only one type exported. – Triynko Dec 11 '13 at 18:57
  • With a proper cache, this method along with the one I posted in the question will allow for wicked-fast, seamless mapping from friendly type names to runtime Type references for any publically defined types in any currently loaded modules. – Triynko Dec 11 '13 at 20:04
  • Ok, not so flawless. I need a better way of including currently loaded assemblies. When they are included by name, some of them seem to be temporary dll files that cannot be accessed. – Triynko Dec 11 '13 at 20:43
  • Turns out that .NET sucks and the "GenerateInMemory" option is a "parlor trick" because it actually doesn't, so every time the compiler runs for a type, it generates a new temporary assembly in the current AppDomain, and as we all know, there is no way to unload an assembly once it's loaded! As a workaround, what I'll do is set up a separate AppDomain for the type lookup which can be torn down (this will occur at most once for each type thanks to the cache), and it will marshall the AssemblyQualifiedName back to the original AppDomain for passing to Type.GetType. – Triynko Dec 11 '13 at 21:31
  • WARNING: If you do not see AppDomain and ConcurrentDictionaries in the code above, do not use it, because I have not updated it yet. Although the functions work, using the GetType method without caching values will result in a new assembly loaded permanently into the AppDomain on each call. To work around this, I've added caches to ensure the translation occurs at most once for each type, and I also will have it perform the translation a thread-safe manor in a child AppDomain, which will be recycled/unloaded periodically so that there are no memory issues with the dynamic assemblies. – Triynko Dec 12 '13 at 00:36
  • I added `.Where(n => !n.Contains("<")).` to the module names query to get around the temporary assemblies. @Triynko I'm not seeing the AppDomain code. Did you ever finish this? – Shawn South Jul 17 '15 at 17:34
  • On .net6 this code crashes with: System.PlatformNotSupportedException: Operation is not supported on this platform. at Microsoft.CSharp.CSharpCodeGenerator.FromFileBatch(...) – Dudar Dec 20 '22 at 15:04
2

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!

drew_w
  • 10,320
  • 4
  • 28
  • 49
  • Thanks! I wrote one too. I'll post it. I'm going to merge the methods into a simple two way conversion with a bi-directional thread-safe cache. – Triynko Dec 11 '13 at 18:43
  • Also note that as far as pulling types from the currently executing assembly, they would have to be publically accessible and you'd have to make the currently executing module available to the compiler. I've updated my own code to do this. – Triynko Dec 11 '13 at 19:48