How to Apply XmlIncludeAttribute to TypeBuilder?
Asked Answered
G

3

12

I am developing a library in C# that generates runtime types using System.Reflection.Emit.TypeBuilder class and i want to generate the following class hierarchy:

[XmlInclude(typeof(Derived))]
public class Base
{
}

public class Derived : Base
{
}

I use the TypeBuilder class in the following way:

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

        var moduleBuilder = assembly.DefineDynamicModule("Test");

        var baseTypeBuilder = moduleBuilder.DefineType("Base", TypeAttributes.Public, typeof(Object));

        var derivedTypeBuilder = moduleBuilder.DefineType("Derived", TypeAttributes.Public);

        derivedTypeBuilder.SetParent(baseTypeBuilder);

        baseTypeBuilder.SetCustomAttribute(new CustomAttributeBuilder(typeof(XmlIncludeAttribute).GetConstructor(new[] { typeof(Type) }), new[] { derivedTypeBuilder }));

        var baseType = baseTypeBuilder.CreateType();

        var derivedType = derivedTypeBuilder.CreateType();

        var attribute = baseType.GetCustomAttribute<XmlIncludeAttribute>();
    }
}

The call:

var attribute = baseType.GetCustomAttribute<XmlIncludeAttribute>();

I receive the following error:

Could not load file or assembly 'Test, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.

Any ideas are well-appreciated: how can i apply a custom attribute on a TypeBuilder for base class that refers to a TypeBuilder for a derived class?

P.S: I'm using Visual Studio 2017 (v15.7.5) and a C# Class Library (.NET Framework project template) NOT .NET Core or .NET Standard

Growing answered 2/8, 2018 at 8:9 Comment(6)
Could you expand the code you posted to a compileable example?Tasteful
Should be fixed now.Growing
I can not reproduce your issue: dotnetfiddle.net/Mk9j2BTasteful
I'm using VS 2017 and .NET Framework (NOT.NET Core or .NET Standard). Can you please verify there?Growing
Sorry I do not have a full IDE at hand at the moment. Did you test it by pasting your provided code into a new project? Since it works on dotnetfiddle there should be no problem with the normal environment.Tasteful
Yes. I'm testing on a VS 2017 v15.7.5 on a .NET Framework console application.Growing
C
7

Cannot provide much reason but a solution that will work without additional load/unload or whatever from and to disk:

Adding a separate field containing the actual Assembly, one can just subscribe to AppDomain.CurrentDomain.AssemblyResolve and check if the dynamic assembly was requested.

If it was, you just need to return your field and it works perfectly fine.

Example:

class Program
{
    static Assembly ass;
    public static void Main(string[] args)
    {
        AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
        var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("Test"), AssemblyBuilderAccess.Run);

        var moduleBuilder = assembly.DefineDynamicModule("Test");

        var baseTypeBuilder = moduleBuilder.DefineType("Base", TypeAttributes.Public, typeof(Object));

        var derivedTypeBuilder = moduleBuilder.DefineType("Derived", TypeAttributes.Public);

        derivedTypeBuilder.SetParent(baseTypeBuilder);

        baseTypeBuilder.SetCustomAttribute(new CustomAttributeBuilder(typeof(XmlIncludeAttribute).GetConstructor(new[] { typeof(Type) }), new[] { derivedTypeBuilder }));

        var baseType = baseTypeBuilder.CreateType();

        var derivedType = derivedTypeBuilder.CreateType();
        ass = baseType.Assembly;

        var attribute = baseType.GetCustomAttribute<XmlIncludeAttribute>();

        Console.WriteLine(attribute.Type.FullName);
    }

    private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
    {
        return ass;
    }
}
Carmella answered 20/9, 2018 at 14:19 Comment(7)
Thank you. We have found this solution before, but we were reluctant to use it because we did not understand why it works. I hope that someone can explain why there is a problem in the first place and why this solution works.Dimitris
Sadly i cannot answer why the assembly does not gets added to the appdomain proper ... but i do can tell you that this works because you provide the assembly via hand :) and the baseType.Assembly is required because you need some actual assembly reference there and not the assembly builderCarmella
@YacoubMassad - this has nothing to do with XmlIncludeAttribute specifically, and nothing to do with the fact that there are two classes. It's just that if you add a TypeBuilder instance to a custom attribute parameter definition, the reflection resolver will need to get the Type (not the TypeBuilder) to give it back to you. To do this, it only knows the assembly that contains it ("Test"), and it just doesn't know where to find that assembly.Voltameter
@SimonMourier - This is not true, using a fully constructed, non-related extra type (moduleBuilder.DefineType("Other", TypeAttributes.Public);) you still will get the same result as the assembly still cannot get resolved.Carmella
@x39 - ? I don't see how that really relates to what I said. I just said if you use typeof(string) for example in SetCustomAttributes, you won't have any problem because the resolver knows how to load the System.String type assembly.Voltameter
To me, it reads like the reasoning is due to passing the type builder instead of passing an actual type. If that Was not the Intention, everything is fine and I just missunderstoodCarmella
@SimonMourier, so is this expected behavior? Is it documented somewhere?Dimitris
P
4

I've reproduced your exception. It's looks like the .NET Framework want to read a module from disk.

So simplest workaround would be to save your assembly to the disk:

var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(
  new AssemblyName("Test"), 
  AssemblyBuilderAccess.RunAndSave); // allow run & save

var moduleBuilder = assembly.DefineDynamicModule("Test",
  "Test.dll"); // specify a file name where module will be stored

...

var baseType = baseTypeBuilder.CreateType();
var derivedType = derivedTypeBuilder.CreateType();

assembly.Save("Test.dll");

Now I was able to get attribute without exception:

var attribute = baseType.GetCustomAttribute<XmlIncludeAttribute>();
Preengage answered 20/9, 2018 at 14:11 Comment(1)
Thank you. We have found this solution before, but would like a solution that does not require writing to disk.Dimitris
B
1

Well, I can tell you why the fix @X39 posted works. Stepping through the forest of framework code (perhaps fallbacks for different cultures) for the baseType.GetCustomAttribute(); call, when finally reaching appdomain.cs I see:

enter image description here

At this point, _AssemblyResolve is null (which is the private backing field for AppDomain.CurrentDomain.AssemblyResolve) and after stepping through to return null;, the code crashes with the error you posted.

More info: https://learn.microsoft.com/en-us/dotnet/api/system.appdomain.assemblyresolve?view=netframework-4.7.2

@X39 feel free to merge my answer and let me know so that I can remove mine, I did not want to edit your answer directly.

Bander answered 21/9, 2018 at 10:29 Comment(3)
Thanks for adding an answer. I am not sure though how this explains why the answer provided @Carmella works. Why is the original code posted not supposed to work? Should it work based on the documentation? Are we expected to use AssemblyResolve here?Dimitris
From the docs page: Remarks It is the responsibility of the ResolveEventHandler for this event to return the assembly that is specified by the ResolveEventArgs.Name property, or to return null if the assembly is not recognized. The assembly must be loaded into an execution context; if it is loaded into the reflection-only context, the load that caused this event to be raised fails.Lh
I do not know if there is another way, can’t find anything in the docs recommending or mentioning another way if an assembly resolve/load is involved. The load is called by the line OP mentionedLh

© 2022 - 2024 — McMap. All rights reserved.