RickStrahl/Westwind.Scripting

Compiling a class with another reference class

naweed opened this issue · 4 comments

Hi,

I am trying to dynamically compile classes. For most part, it is working fine, except when it comes to classes which have reference to another class which was previously compiled. Plz see the example below. Any idea how this can be done.

FdynzMMXoAEkgNr

see the comment of the method CompileClassToType

        // Summary:
        //     This method compiles a class and hands back a dynamic reference to that class
        //     that you can call members on.
        //
        // Parameters:
        //   code:
        //     Fully self-contained C# class
        //
        // Returns:
        //     Instance of that class or null

It should be fully self-contained.
Use CompileAssembly instead

Took a look at this today finally. I think this depends on the platform you're running on - on full framework this should work, but on .NET Core it'll fail as written.

The reason for .NET Core failing is that the assembly assignment isn't working based on the dynamically generated type.

script.AddAssembly(deptClassType)

isn't working because the Assembly.Location for this type is empty. The workaround for this is that you have to generate an assembly on disk by specifying a GenerateOutputAssembly for the compilation. You also have to specify a GeneratedClassName (which is used for the modulename).

The following works:

        [TestMethod]
        public void TwoDynamicClassesTest()
        {
            var class1Code = @"
using System;

namespace Test1 {
    public class Person
    {
        public string Name {get; set; } = ""Rick"";
        public string Description {get; set; } = ""Testing"";
    } 
}
";

            var class2Code = @"
using System;
using Test1;

namespace Test
{

    public class Customer
    {
        public Test1.Person CustomerInfo {get; set; } = new Test1.Person();
        public string CustomerNumber  { get; set; }         
    } 
}
";
            var script = new CSharpScriptExecution();
            script.AddLoadedReferences();

            // THESE TWO ARE IMPORTANT! Unique names and physical DLL so it can be referenced
            script.GeneratedClassName = "__person";   
            script.OutputAssembly = @"c:\temp\person.dll"; 

            var personType = script.CompileClassToType(class1Code);
            var person = Activator.CreateInstance(personType);


            Assert.IsNotNull(person, "Person should not be null. " + script.ErrorMessage + "\n" + script.GeneratedClassCodeWithLineNumbers);
            Console.WriteLine("Location: " + personType.Assembly.Location);
            
            //script = new CSharpScriptExecution();
            //script.AddDefaultReferencesAndNamespaces(); //AddLoadedReferences();
            //script.AddAssembly(script.OutputAssembly);
            

            // THESE TWO ARE IMPORTANT!!!!!
            script.GeneratedClassName = "__customer";
            script.OutputAssembly = null;

            script.AddAssembly(personType);
            var customerType = script.CompileClassToType(class2Code);

            Assert.IsNotNull(customerType, "Customer should not be null. " + script.ErrorMessage + "\n" + script.GeneratedClassCodeWithLineNumbers);
            Console.WriteLine(customerType);
            Console.WriteLine(customerType.Assembly.Location);

            dynamic customer = Activator.CreateInstance(customerType);

            
            Assert.IsNotNull(customer.CustomerInfo.Name, "Customer should not be null");
            Console.WriteLine(customer.CustomerInfo.Name);
        }
    }

Note the GeneratedClassName is used for the module name in this case, since we are providing a class name as part of the code. GeneratedClassName is also used when using 'snippets' that don't include the class name like ExecuteCode() or EvaluateExpression() which generate class wrappers.

I bumped this up to the Roslyn repo to see if there might be a solution to get a MetaDataReference from an in-memory compiled assembly/type.

dotnet/roslyn#65627

So figured out how to get the code to compile by fixing up the meta data reference. That fixes the compilation issue.

Unfortunately though, it still doesn't work because the top level type can't be intstantiated because the dependent type can't be resolved at runtime. Even though we have a type instance that's already active - apparently the reference is not identical to what exists in memory already from the compilation result.

We still end up with this error:

Could not load file or assembly '__person.cs, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'. The system cannot find the file specified.

I think the bottom line to all of this is that if you want to re-use dynamically compiled assemblies in another compiled context, you have to use an on-disk image. The only other option I see is to use a custom assembly loader and that's even more overhead than having the assembly on disk and managing it.