feat: wire Inbound API Route.To().Call() to site instance scripts and add Roslyn compilation

Completes the Inbound API → site script call chain by adding RouteToCallRequest
handlers in SiteCommunicationActor and DeploymentManagerActor. Also replaces the
placeholder dispatch table in InboundScriptExecutor with Roslyn compilation of
API method scripts at startup, enabling user-defined inbound API methods to call
instance scripts across the cluster.
This commit is contained in:
Joseph Doherty
2026-03-18 08:43:13 -04:00
parent eb8ead58d2
commit 78fbb13df7
5 changed files with 125 additions and 6 deletions

View File

@@ -1,4 +1,6 @@
using System.Text.Json;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.Extensions.Logging;
using ScadaLink.Commons.Entities.InboundApi;
@@ -6,11 +8,7 @@ namespace ScadaLink.InboundAPI;
/// <summary>
/// WP-3: Executes the C# script associated with an inbound API method.
/// The script receives input parameters and a route helper, and returns a result
/// that is serialized as the JSON response.
///
/// In a full implementation this would use Roslyn scripting. For now, scripts
/// are a simple dispatch table so the rest of the pipeline can be tested end-to-end.
/// Compiles method scripts via Roslyn and caches compiled delegates.
/// </summary>
public class InboundScriptExecutor
{
@@ -24,13 +22,73 @@ public class InboundScriptExecutor
/// <summary>
/// Registers a compiled script handler for a method name.
/// In production, this would be called after Roslyn compilation of the method's Script property.
/// </summary>
public void RegisterHandler(string methodName, Func<InboundScriptContext, Task<object?>> handler)
{
_scriptHandlers[methodName] = handler;
}
/// <summary>
/// Compiles and registers a single API method script.
/// </summary>
public bool CompileAndRegister(ApiMethod method)
{
if (string.IsNullOrWhiteSpace(method.Script))
{
_logger.LogWarning("API method {Method} has no script code", method.Name);
return false;
}
try
{
var scriptOptions = ScriptOptions.Default
.WithReferences(
typeof(object).Assembly,
typeof(Enumerable).Assembly,
typeof(Dictionary<,>).Assembly,
typeof(RouteHelper).Assembly,
typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo).Assembly)
.WithImports(
"System",
"System.Collections.Generic",
"System.Linq",
"System.Threading.Tasks");
var compiled = CSharpScript.Create<object?>(
method.Script,
scriptOptions,
globalsType: typeof(InboundScriptContext));
var diagnostics = compiled.Compile();
var errors = diagnostics
.Where(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error)
.Select(d => d.GetMessage())
.ToList();
if (errors.Count > 0)
{
_logger.LogWarning(
"API method {Method} script compilation failed: {Errors}",
method.Name, string.Join("; ", errors));
return false;
}
_scriptHandlers[method.Name] = async ctx =>
{
var state = await compiled.RunAsync(ctx, ctx.CancellationToken);
return state.ReturnValue;
};
_logger.LogInformation("API method {Method} script compiled and registered", method.Name);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to compile API method {Method} script", method.Name);
return false;
}
}
/// <summary>
/// Executes the script for the given method with the provided context.
/// </summary>

View File

@@ -20,4 +20,8 @@
<InternalsVisibleTo Include="ScadaLink.InboundAPI.Tests" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="5.0.0" />
</ItemGroup>
</Project>