Roslyn, how can I instantiate a class in a script during runtime and invoke methods of that class?
Asked Answered
C

1

11

I understand how I can execute entire scripts using Roslyn in C# but what I now want to accomplish is to compile a class inside the script, instantiate it, parse it to an interface and then invoke methods that the compiled and instantiated class implements.

Does Roslyn expose such functionality? Can you someone please point me to such approach?

Thanks

Capapie answered 10/11, 2017 at 9:0 Comment(0)
C
16

I think you can do what you want for example like this:

namespace ConsoleApp2 {
    class Program {
        static void Main(string[] args) {
            // create class and return its type from script
            // reference current assembly to use interface defined below
            var script = CSharpScript.Create(@"
        public class Test : ConsoleApp2.IRunnable {
            public void Run() {
                System.Console.WriteLine(""test"");
            }
        }
        return typeof(Test);
        ", ScriptOptions.Default.WithReferences(Assembly.GetExecutingAssembly()));
            script.Compile();
            // run and you get Type object for your fresh type
            var testType = (Type) script.RunAsync().Result.ReturnValue;
            // create and cast to interface
            var runnable = (IRunnable)Activator.CreateInstance(testType);
            // use
            runnable.Run();
            Console.ReadKey();
        }
    }

    public interface IRunnable {
        void Run();
    }
}

Instead of returning type you created from script you can also use globals and return it that way:

namespace ConsoleApp2 {
    class Program {
        static void Main(string[] args) {

            var script = CSharpScript.Create(@"
        public class Test : ConsoleApp2.IRunnable {
            public void Run() {
                System.Console.WriteLine(""test"");
            }
        }
        MyTypes.Add(typeof(Test).Name, typeof(Test));
        ", ScriptOptions.Default.WithReferences(Assembly.GetExecutingAssembly()), globalsType: typeof(ScriptGlobals));
            script.Compile();
            var globals = new ScriptGlobals();
            script.RunAsync(globals).Wait();            
            var runnable = (IRunnable)Activator.CreateInstance(globals.MyTypes["Test"]);
            runnable.Run();
            Console.ReadKey();
        }
    }

    public class ScriptGlobals {
        public Dictionary<string, Type> MyTypes { get; } = new Dictionary<string, Type>();
    }

    public interface IRunnable {
        void Run();
    }
}

Edit to answer your comment.

what if I know the name and type of the class in the script? My understanding is that script.Compile() adds the compiled assembly to gac? Am I incorrect? If I then simply use Activator.CreateInstance(typeofClass) would this not solve my problem without even having to run the script

Compiled assembly is not added to gac - it is compiled and stored in memory, similar to how you can load assembly with Assembly.Load(someByteArray). Anyway, after you call Compile that assembly is loaded in current app domain so you can access your types without RunAsunc(). Problem is this assembly has cryptic name, for example: ℛ*fde34898-86d2-42e9-a786-e3c1e1befa78#1-0. To find it you can for example do this:

script.Compile();
var asmAfterCompile = AppDomain.CurrentDomain.GetAssemblies().Single(c =>
     String.IsNullOrWhiteSpace(c.Location) && c.CodeBase.EndsWith("Microsoft.CodeAnalysis.Scripting.dll"));

But note this is not stable, because if you compile multiple scripts in your app domain (or even same script multiple times) - multiple such assemblies are generated, so it is hard to distinguish between them. If that is not a problem for you - you can use this way (but ensure that you properly test all this).

After you found generated assembly - problems are not over. All your script contents are compiled under wrapping class. I see its named "Submission#0" but I cannot guarantee it's always named like that. So suppose you have class Test in your script. It will be child class of that wrapper, so real type name will be "Submission#0+Test". So to get your type from generated assembly it's better to do this:

var testType = asmAfterCompile.GetTypes().Single(c => c.Name == "Test");

I consider this approach somewhat more fragile compared to previous, but if previous are not applicable for you - try this one.

Another alternative suggested in comments:

script.Compile();
var stream = new MemoryStream();
var emitResult = script.GetCompilation().Emit(stream);
if (emitResult.Success) {
    var asm = Assembly.Load(stream.ToArray());
}

That way you create assembly yourself and so do not need to search it in current app domain.

Calvano answered 10/11, 2017 at 9:23 Comment(11)
Awesome, exactly what I was looking for, thanks a lot.Capapie
@MattWolf ScriptGlobals is class defined in answer code itself (does not belong to any roslyn assembly).Calvano
Sorry, just saw it, is that in your opinion, the best way to create the instance without returning any value/type in the script?Capapie
@MattWolf Well at least I didn't find another way to do that (well you can also use local variables in script, but that's quite similar) so I think yes. If you don't want to add types manually to MyTypes - maybe you can do Assembly.GetExecutingAssembly() inside script and get all types from there (and then still assign result to some global variable).Calvano
what if I know the name and type of the class in the script? My understanding is that script.Compile() adds the compiled assembly to gac? Am I incorrect? If I then simply use Activator.CreateInstance(typeofClass) would this not solve my problem without even having to run the script? My understanding is that by explicitly compiling I would not need to RunAsync(...)?Capapie
I am asking because I cannot return any results nor can I utilize shared objects inside the scripts. The script is a simple class declaration which I want Roslyn to compile and then instantiate.Capapie
@MattWolf I updated answer, because that's too long for a comment.Calvano
thanks this helps a lot, wished I could upvote multiple times.Capapie
I just the newest version of each compiled script. But I guess I can figure that out quite easily from the current app domain.Capapie
by the way, an easier way to get the assembly in question might be via CSharpScript.Create(...).GetCompilation() and to then Emit into a memory stream and in that way loading the assembly. Just saw the GetCompilation method.Capapie
@MattWolf yes that's also an option indeed, better than searching current app domain for that assembly.Calvano

© 2022 - 2024 — McMap. All rights reserved.