16

This code snippet is a simplified extract of my class-generation code, which creates two classes that reference each other as arguments in a generic type:

namespace Sandbox
{
    using System;
    using System.Reflection;
    using System.Reflection.Emit;

    internal class Program
    {
        private static void Main(string[] args)
        {
            var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("Test"), AssemblyBuilderAccess.Run);
            var module = assembly.DefineDynamicModule("Test");

            var typeOne = module.DefineType("TypeOne", TypeAttributes.Public);
            var typeTwo = module.DefineType("TypeTwo", TypeAttributes.Public);

            typeOne.DefineField("Two", typeof(TestGeneric<>).MakeGenericType(typeTwo), FieldAttributes.Public);
            typeTwo.DefineField("One", typeof(TestGeneric<>).MakeGenericType(typeOne), FieldAttributes.Public);

            typeOne.CreateType();
            typeTwo.CreateType();

            Console.WriteLine("Done");
            Console.ReadLine();
        }
    }

    public struct TestGeneric<T>
    {
    }
}

Which should produce MSIL equivalent to the following:

public class TypeOne
{
    public Program.TestGeneric<TypeTwo> Two;
}

public class TypeTwo
{
    public Program.TestGeneric<TypeOne> One;
}

But instead throws this exception on the line typeOne.CreateType():

System.TypeLoadException was unhandled
  Message=Could not load type 'TypeTwo' from assembly 'Test, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'.
  Source=mscorlib
  TypeName=TypeTwo
  StackTrace:
       at System.Reflection.Emit.TypeBuilder.TermCreateClass(RuntimeModule module, Int32 tk, ObjectHandleOnStack type)
       at System.Reflection.Emit.TypeBuilder.CreateTypeNoLock()
       at System.Reflection.Emit.TypeBuilder.CreateType()
       at Sandbox.Program.Main(String[] args) in C:\Users\aca1\Code\Sandbox\Program.cs:line 20

Interesting things to note:

  • The circular reference isn't required to cause the exception; if I don't define field One on TypeTwo, creating TypeOne before TypeTwo still fails, but creating TypeTwo before TypeOne succeeds. Therefore, the exception is specifically caused by using a type that has not yet been created as an argument in a generic field type; however, because I need to use a circular reference, I cannot avoid this situation by creating the types in a specific order.
  • Yes, I do need to use a circular reference.
  • Removing the wrapper TestGeneric<> type and declaring the fields as TypeOne & TypeTwo directly does not produce this error; thus I can use dynamic types that have been defined but not created.
  • Changing TestGeneric<> from a struct to a class does not produce this error; so this pattern does work with most generics, just not generic value types.
  • I can't change the declaration of TestGeneric<> in my case as it is declared in another assembly - specifically, System.Data.Linq.EntityRef<> declared in System.Data.Linq.dll.
  • My circular reference is caused by representing two tables with foreign key references to each other; hence the need for that specific generic type and this specific pattern.
  • Changing the circular reference to a self-reference edit succeeds. This failed originally because I had TestGeneric<> as a nested type in Program, so it inherited the internal visibility. I've fixed this now in the code sample above, and it does in fact work.
  • Compiling the generated code manually (as C# code) also works, so it's not an obscure compiler issue.

Any ideas on a) why this occuring, b) how I can fix this and/or c) how I can work around it?

Thanks.

FacticiusVir
  • 2,037
  • 15
  • 28

1 Answers1

10

I do not know exactly why this is occurring. I have a good guess.

As you have observed, creating a generic class is treated differently than creating a generic struct. When you create the type 'TypeOne' the emitter needs to create the generic type 'TestGeneric' and for some reason the proper Type is needed rather than the TypeBuilder. Perhaps this occurs when trying to determine the size of the new generic struct? I'm not sure. Maybe the TypeBuilder can't figure out its size so the created 'TypeTwo' Type is needed.

When TypeTwo cannot be found (because it only exists as a TypeBuilder) the AppDomain's TypeResolve event will be triggered. This gives you a chance to fix the problem. While handling the TypeResolve event you can create the type 'TypeTwo' and solve the problem.

Here is a rough implementation:

namespace Sandbox
{
    using System;
    using System.Collections.Generic;
    using System.Reflection;
    using System.Reflection.Emit;

    internal class Program
    {
        private static void Main(string[] args)
        {
            var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("Test"), AssemblyBuilderAccess.Run);
            var module = assembly.DefineDynamicModule("Test");

            var typeOne = module.DefineType("TypeOne", TypeAttributes.Public);
            var typeTwo = module.DefineType("TypeTwo", TypeAttributes.Public);

            typeOne.DefineField("Two", typeof(TestGeneric<>).MakeGenericType(typeTwo), FieldAttributes.Public);
            typeTwo.DefineField("One", typeof(TestGeneric<>).MakeGenericType(typeOne), FieldAttributes.Public);

            TypeConflictResolver resolver = new TypeConflictResolver();
            resolver.AddTypeBuilder(typeTwo);
            resolver.Bind(AppDomain.CurrentDomain);

            typeOne.CreateType();
            typeTwo.CreateType();

            resolver.Release();

            Console.WriteLine("Done");
            Console.ReadLine();
        }
    }

    public struct TestGeneric<T>
    {
    }

    internal class TypeConflictResolver
    {
        private AppDomain _domain;
        private Dictionary<string, TypeBuilder> _builders = new Dictionary<string, TypeBuilder>();

        public void Bind(AppDomain domain)
        {
            domain.TypeResolve += Domain_TypeResolve;
        }

        public void Release()
        {
            if (_domain != null)
            {
                _domain.TypeResolve -= Domain_TypeResolve;
                _domain = null;
            }
        }

        public void AddTypeBuilder(TypeBuilder builder)
        {
            _builders.Add(builder.Name, builder);
        }

        Assembly Domain_TypeResolve(object sender, ResolveEventArgs args)
        {
            if (_builders.ContainsKey(args.Name))
            {
                return _builders[args.Name].CreateType().Assembly;
            }
            else
            {
                return null;
            }
        }
    }
}
Scott
  • 1,876
  • 17
  • 13
  • Interesting; you're actually creating typeTwo inside the TypeResolver, which causes it to be created halfway through typeOne being created, but late enough that runtime handles to typeOne exist. A gotcha to this code is that both types must be fully defined before either can be created; I happen to be doing that in this example, but must be sure to enforce that in my production code. In any case, this solves my problem, so thanks! – FacticiusVir Jul 18 '11 at 23:42
  • Glad to help. This was a particularly fun/interesting problem to look at. Indeed, the types which are involved in the circular relationship must be ready for creation at the same point in time. Thank you for pointing that out. – Scott Jul 20 '11 at 21:26