RickStrahl/Westwind.Scripting

Any way to compile method? can only find compile class or assembly methods?

grofit opened this issue · 3 comments

Hi Rick,

I was looking for a quick way to allow users to provide snippets of code where I can pass in a context style variable which contains high level objects they should have access to, i.e some variables, an event system etc.

So based off these requirements it looked like ExecuteMethodAsync was the best bet as I could then do something like:

var scriptExecutor = new CSharpScriptExecution { AllowReferencesInCode = true, ThrowExceptions = true };
scriptExecutor.AddDefaultReferencesAndNamespaces();
scriptExecutor.AddAssembly(typeof(ExecutionContext));

var context = new ExecutionContext(Logger, AppState.UserVariables, AppState.TransientVariables, flowVars, EventBus);
await scriptExecutor.ExecuteMethodAsync(data.CSharpCode, "Execute", context);
return ExecutionResult.Success();

However it seemed reasonable that before the user could run the execution phase (above) they would probably want to validate their code by compiling it, which is where I stumbled into this issue, here is the code I am currently using:

private void CompileCode()
{
    var scriptExecutor = new CSharpScriptExecution { AllowReferencesInCode = true };
    scriptExecutor.AddDefaultReferencesAndNamespaces();
    scriptExecutor.AddAssembly(typeof(ExecutionContext));
    scriptExecutor.CompileClass(Data.CSharpCode);

    IsErrored = scriptExecutor.Error;
    CompilationResult = scriptExecutor.ErrorMessage;
    // The UI handles this elsewhere
}

However it blows up here

(1,1): error CS0106: The modifier 'public' is not valid for this item
(1,1): error CS8805: Program using top-level statements must be an executable.
(1,19): error CS0161: 'Execute(ExecutionContext)': not all code paths return a value
(1,14): error CS0246: The type or namespace name 'Task' could not be found (are you missing a using directive or an assembly reference?)
(1,27): error CS0246: The type or namespace name 'ExecutionContext' could not be found (are you missing a using directive or an assembly reference?)
(1,19): warning CS1998: This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
(1,19): warning CS8321: The local function 'Execute' is declared but never used

So its blowing up for good reason I assume because its expecting a whole class not just a method, but I was unsure how to verify the syntax in this scenario, here is the example code that the user starts with which I am verifying for now:

public async Task Execute(ExecutionContext context)
{
    // Your code goes here    
};

Worst case I can probably work around by using a class based approach, but given the execution allows methods I just wanted to see if this use case was supported or any guidance on how I should solve the problem.

From my perspective my only requirement is that I NEED to provide the user access to runtime variables that already exist within their code, the less code I have to provide in the "harness" for them the better (i.e method seemed least boilerplate for them with ability to pass in var).

Ok revisiting...

I'm not quite sure what you're asking. Are you saying ExecuteMethod() doesn't work when you pass a method as you show? That should work just fine assuming all your references and namespaces are valid.

Or are you asking how you can precompile your code to check for errors?

I can't help with the former since that is likely a problem of missing references or namepaces that you need to add.

For the latter, my suggestion the answer is that you can't get compilation errors without running the code if you use ExecuteMethod(). That's because ExecuteMethod() is an aggregate wrapper that combines compilation and execution into one step.

It's possible to seperate that but then you're into the lower level features and creating a class that can be compiled and executed separately.

You can check out the unit tests to see how to do this or you can look at this blog post about this class that goes into more detail.

Another way you can do this in a simpler way is to:

  • Add a second do nothing method to the one you actually need to call
  • Execute the do-nothing method that you know always succeeds

The 'method' you pass can look like this:

public async string HelloWorld(MyContext context) {
   ....
}

public bool DoNothing()
{
    return true;
}

(yeah it's actually a class body that you're passing - the library fixes that up into a class)

You then execute DoNothing() - if there are no compiler errors you get a valid result. Otherwise you can check the compiler errors. Then you execute HelloWorld() if there are no errors.

If there are errors when you run DoNothing() you know there are compiler errors. Once you've compiled and run the class generated is cached so if you keep the reference to the execution engine you can quickly re-run the actual user method (once or multiple times).

Hi Rick,

Sorry for any misunderstanding it was more the compile stage I was interested in. I could run the code fine but noticed there was compile methods but I think I originally misunderstood their purpose thinking they were there to purely compile and provide errors, but they are there to compile the class into a dynamic object for processing.

Anyway it's a moot point as I've just gone with a class approach now, it's probably a bit inefficient as I'm using the compile to generate a class type and check for errors whenever the user wants to verify their syntax, however in reality that's gonna be making lots of instances behind the scenes I imaging, but happy to flag that as tech debt for now and look into the innards of the roslyn bit later to purely compile without generating anything.

Thanks again for your great work on this library.

@grofit,

The smallest unit of compilation with .NET and Roslyn (or any .NET compiler) is a class. You can't compile just a method. You can use Reflection.Emit and ExpressionTrees which are a dynamic compilation feature but that doesn't work of source code - you have to create the parse trees which is complicated.

While Roslyn has scripting libraries that allow you to 'just run' a snippet of code, that mechanism still compiles a class and assembly in the background and does something very similar that this library does, but with less flexibilty since you can't control the class that gets generated.

See my post that accompanies this library:

https://weblog.west-wind.com/posts/2022/Jun/07/Runtime-CSharp-Code-Compilation-Revisited-for-Roslyn#roslyn-scripting-apis