< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Languages.VBNetDelegateBuilder
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Languages/VBNetDelegateBuilder.cs
Tag: Kestrun/Kestrun@0d738bf294e6281b936d031e1979d928007495ff
Line coverage
89%
Covered lines: 200
Uncovered lines: 24
Coverable lines: 224
Total lines: 536
Line coverage: 89.2%
Branch coverage
84%
Covered branches: 113
Total branches: 134
Branch coverage: 84.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 08/26/2025 - 14:53:17 Line coverage: 89.9% (170/189) Branch coverage: 71.8% (69/96) Total lines: 450 Tag: Kestrun/Kestrun@78d1e497d8ba989d121b57aa39aa3c6b22de743109/04/2025 - 17:02:01 Line coverage: 90.8% (189/208) Branch coverage: 77.4% (96/124) Total lines: 505 Tag: Kestrun/Kestrun@f3880b25ea131298aa2f8b1e0d0a8d55eb160bc009/06/2025 - 18:30:33 Line coverage: 94.7% (197/208) Branch coverage: 83.8% (104/124) Total lines: 505 Tag: Kestrun/Kestrun@aeddbedb8a96e9137aac94c2d5edd011b57ac87110/13/2025 - 16:52:37 Line coverage: 94.7% (199/210) Branch coverage: 89.5% (111/124) Total lines: 508 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e10/15/2025 - 21:27:26 Line coverage: 94.7% (197/208) Branch coverage: 89.5% (111/124) Total lines: 507 Tag: Kestrun/Kestrun@c33ec02a85e4f8d6061aeaab5a5e8c3a8b66559411/14/2025 - 12:29:34 Line coverage: 89.2% (200/224) Branch coverage: 84.3% (113/134) Total lines: 536 Tag: Kestrun/Kestrun@5e12b09a6838e68e704cd3dc975331b9e680a626 08/26/2025 - 14:53:17 Line coverage: 89.9% (170/189) Branch coverage: 71.8% (69/96) Total lines: 450 Tag: Kestrun/Kestrun@78d1e497d8ba989d121b57aa39aa3c6b22de743109/04/2025 - 17:02:01 Line coverage: 90.8% (189/208) Branch coverage: 77.4% (96/124) Total lines: 505 Tag: Kestrun/Kestrun@f3880b25ea131298aa2f8b1e0d0a8d55eb160bc009/06/2025 - 18:30:33 Line coverage: 94.7% (197/208) Branch coverage: 83.8% (104/124) Total lines: 505 Tag: Kestrun/Kestrun@aeddbedb8a96e9137aac94c2d5edd011b57ac87110/13/2025 - 16:52:37 Line coverage: 94.7% (199/210) Branch coverage: 89.5% (111/124) Total lines: 508 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e10/15/2025 - 21:27:26 Line coverage: 94.7% (197/208) Branch coverage: 89.5% (111/124) Total lines: 507 Tag: Kestrun/Kestrun@c33ec02a85e4f8d6061aeaab5a5e8c3a8b66559411/14/2025 - 12:29:34 Line coverage: 89.2% (200/224) Branch coverage: 84.3% (113/134) Total lines: 536 Tag: Kestrun/Kestrun@5e12b09a6838e68e704cd3dc975331b9e680a626

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Build(...)93.75%1616100%
GetVbReturnType(...)100%44100%
Compile(...)89.28%282897.56%
GetStartLineOrThrow(...)75%4485.71%
BuildMetadataReferences(...)100%88100%
IsSatelliteAssembly(...)75%14854.54%
CollectDynamicImports(...)66.66%121287.5%
AddTypeNamespaces(...)70%131070%
SafeHasLocation(...)100%2260%
LogWarnings(...)90%1010100%
ThrowIfErrors(...)16.66%23621.42%
LoadDelegateFromAssembly(...)100%11100%
BuildWrappedSource(...)96.15%2626100%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Languages/VBNetDelegateBuilder.cs

#LineLine coverage
 1using System.Collections.Immutable;
 2using System.Reflection;
 3using System.Text;
 4using Microsoft.CodeAnalysis.Scripting;
 5using Microsoft.CodeAnalysis.VisualBasic;
 6using Serilog.Events;
 7using Microsoft.CodeAnalysis;
 8using Kestrun.Utilities;
 9using System.Security.Claims;
 10using Kestrun.Logging;
 11using Kestrun.Hosting;
 12
 13namespace Kestrun.Languages;
 14
 15
 16internal static class VBNetDelegateBuilder
 17{
 18    /// <summary>
 19    /// The marker that indicates where user code starts in the VB.NET script.
 20    /// This is used to ensure that the user code is correctly placed within the generated module.
 21    /// </summary>
 22    private const string StartMarker = "' ---- User code starts here ----";
 23
 24    /// <summary>
 25    /// Builds a VB.NET delegate for Kestrun routes.
 26    /// </summary>
 27    /// <remarks>
 28    /// This method uses the Roslyn compiler to compile the provided VB.NET code into a delegate.
 29    /// </remarks>
 30    /// <param name="host">The Kestrun host instance.</param>
 31    /// <param name="code">The VB.NET code to compile.</param>
 32    /// <param name="args">The arguments to pass to the script.</param>
 33    /// <param name="extraImports">Optional additional namespaces to import in the script.</param>
 34    /// <param name="extraRefs">Optional additional assemblies to reference in the script.</param>
 35    /// <param name="languageVersion">The VB.NET language version to use for compilation.</param>
 36    /// <returns>A delegate that takes CsGlobals and returns a Task.</returns>
 37    /// <exception cref="CompilationErrorException">Thrown if the compilation fails with errors.</exception>
 38    /// <remarks>
 39    /// This method uses the Roslyn compiler to compile the provided VB.NET code into a delegate.
 40    /// </remarks>
 41    internal static RequestDelegate Build(KestrunHost host,
 42        string code, Dictionary<string, object?>? args, string[]? extraImports,
 43        Assembly[]? extraRefs, LanguageVersion languageVersion = LanguageVersion.VisualBasic16_9)
 44    {
 545        var log = host.Logger;
 546        if (log.IsEnabled(LogEventLevel.Debug))
 47        {
 448            log.Debug("Building VB.NET delegate, script length={Length}, imports={ImportsCount}, refs={RefsCount}, lang=
 449               code.Length, extraImports?.Length ?? 0, extraRefs?.Length ?? 0, languageVersion);
 50        }
 51
 52        // Validate inputs
 553        if (string.IsNullOrWhiteSpace(code))
 54        {
 155            throw new ArgumentNullException(nameof(code), "VB.NET code cannot be null or whitespace.");
 56        }
 57        // 1. Compile the VB.NET code into a script
 58        //    - Use VisualBasicScript.Create() to create a script with the provided code
 59        //    - Use ScriptOptions to specify imports, references, and language version
 60        //    - Inject the provided arguments into the globals
 461        var script = Compile<bool>(host: host, code: code, extraImports: extraImports, extraRefs: extraRefs, null, langu
 62
 63        // 2. Build the per-request delegate
 64        //    - This delegate will be executed for each request
 65        //    - It will create a KestrunContext and CsGlobals, then execute the script with these globals
 66        //    - The script can access the request context and shared state store
 467        if (log.IsEnabled(LogEventLevel.Debug))
 68        {
 369            log.Debug("C# delegate built successfully, script length={Length}, imports={ImportsCount}, refs={RefsCount},
 370                code?.Length, extraImports?.Length ?? 0, extraRefs?.Length ?? 0, languageVersion);
 71        }
 72
 473        return async ctx =>
 474        {
 475            try
 476            {
 377                if (log.IsEnabled(LogEventLevel.Debug))
 478                {
 279                    log.Debug("Preparing execution for C# script at {Path}", ctx.Request.Path);
 480                }
 481
 382                var (Globals, Response, Context) = await DelegateBuilder.PrepareExecutionAsync(host, ctx, args).Configur
 483
 484                // Execute the script with the current context and shared state
 385                if (log.IsEnabled(LogEventLevel.Debug))
 486                {
 287                    log.DebugSanitized("Executing VB.NET script for {Path}", ctx.Request.Path);
 488                }
 489
 390                _ = await script(Globals).ConfigureAwait(false);
 391                if (log.IsEnabled(LogEventLevel.Debug))
 492                {
 293                    log.DebugSanitized("VB.NET script executed successfully for {Path}", ctx.Request.Path);
 494                }
 495
 496                // Apply the response to the Kestrun context
 397                await DelegateBuilder.ApplyResponseAsync(ctx, Response, log).ConfigureAwait(false);
 398            }
 499            finally
 4100            {
 4101                // Do not complete the response here; allow downstream middleware (e.g., StatusCodePages)
 4102                // to produce a body for status-only responses when needed.
 4103            }
 7104        };
 105    }
 106
 107    /// <summary>
 108    /// Decide the VB return type string that matches TResult
 109    /// </summary>
 110    /// <param name="t">The type to get the VB return type for.</param>
 111    /// <returns> The VB.NET return type as a string.</returns>
 112    private static string GetVbReturnType(Type t)
 113    {
 13114        if (t == typeof(bool))
 115        {
 8116            return "Boolean";
 117        }
 118
 5119        if (t == typeof(IEnumerable<Claim>))
 120        {
 3121            return "System.Collections.Generic.IEnumerable(Of System.Security.Claims.Claim)";
 122        }
 123
 124        // Fallback so it still compiles even for object / string / etc.
 2125        return "Object";
 126    }
 127
 128    /// <summary>
 129    /// Compiles the provided VB.NET code into a delegate that can be executed with CsGlobals.
 130    /// </summary>
 131    /// <typeparam name="TResult">The type of the result returned by the delegate.</typeparam>
 132    /// <param name="host">The Kestrun host instance.</param>
 133    /// <param name="code">The VB.NET code to compile.</param>
 134    /// <param name="extraImports">Optional additional namespaces to import in the script.</param>
 135    /// <param name="extraRefs">Optional additional assemblies to reference in the script.</param>
 136    /// <param name="locals">Optional local variables to provide to the script.</param>
 137    /// <param name="languageVersion">The VB.NET language version to use for compilation.</param>
 138    /// <returns>A delegate that takes CsGlobals and returns a Task.</returns>
 139    /// <exception cref="CompilationErrorException">Thrown if the compilation fails with errors.</exception>
 140    /// <remarks>
 141    /// This method uses the Roslyn compiler to compile the provided VB.NET code into a delegate.
 142    /// </remarks>
 143    internal static Func<CsGlobals, Task<TResult>> Compile<TResult>(
 144        KestrunHost host,
 145            string? code, string[]? extraImports,
 146            Assembly[]? extraRefs, IReadOnlyDictionary<string, object?>? locals, LanguageVersion languageVersion
 147        )
 148    {
 13149        var log = host.Logger;
 13150        if (log.IsEnabled(LogEventLevel.Debug))
 151        {
 10152            log.Debug("Building VB.NET delegate, script length={Length}, imports={ImportsCount}, refs={RefsCount}, lang=
 10153               code?.Length, extraImports?.Length ?? 0, extraRefs?.Length ?? 0, languageVersion);
 154        }
 155
 156        // Validate inputs
 13157        if (string.IsNullOrWhiteSpace(code))
 158        {
 0159            throw new ArgumentNullException(nameof(code), "VB.NET code cannot be null or whitespace.");
 160        }
 161
 13162        extraImports ??= [];
 13163        extraImports = [.. extraImports, "System.Collections.Generic", "System.Linq", "System.Security.Claims"];
 164
 165        // Discover dynamic namespaces from globals + locals similar to C# path
 13166        var dynamicImports = CollectDynamicImports(host, locals);
 13167        if (dynamicImports.Count > 0)
 168        {
 6169            var mergedImports = extraImports.Concat(dynamicImports)
 6170                .Distinct(StringComparer.Ordinal)
 6171                .ToArray();
 6172            if (log.IsEnabled(LogEventLevel.Debug) && mergedImports.Length != extraImports.Length)
 173            {
 4174                log.Debug("Added {Count} dynamic VB imports from globals/locals.", mergedImports.Length - extraImports.L
 175            }
 6176            extraImports = mergedImports;
 177        }
 178
 179        // 🔧 1.  Build a real VB file around the user snippet
 13180        var source = BuildWrappedSource(code, extraImports, vbReturnType: GetVbReturnType(typeof(TResult)),
 13181            locals: locals);
 182
 183        // Prepares the source code for compilation.
 13184        var startLine = GetStartLineOrThrow(source, log);
 185
 186        // Parse the source code into a syntax tree
 187        // This will allow us to analyze and compile the code
 13188        var tree = VisualBasicSyntaxTree.ParseText(
 13189                   source,
 13190                   new VisualBasicParseOptions(languageVersion));
 191
 192        // 🔧 2.  References = everything already loaded  +  extras
 13193        var refs = BuildMetadataReferences(extraRefs);
 194        // 🔧 3.  Normal DLL compilation
 13195        var compilation = VisualBasicCompilation.Create(
 13196                 assemblyName: $"RouteScript_{Guid.NewGuid():N}",
 13197                 syntaxTrees: [tree],
 13198                 references: refs,
 13199                 options: new VisualBasicCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
 200
 13201        using var ms = new MemoryStream();
 13202        var emitResult = compilation.Emit(ms) ?? throw new InvalidOperationException("Failed to compile VB.NET script.")
 203        // 🔧 4.  Log the compilation result
 13204        if (log.IsEnabled(LogEventLevel.Debug))
 205        {
 10206            log.Debug("VB.NET script compilation completed, assembly size={Size} bytes", ms.Length);
 207        }
 208
 209        // 🔧 5. Handle diagnostics
 13210        ThrowIfErrors(emitResult.Diagnostics, startLine, log);
 211        // Log any warnings from the compilation process
 13212        LogWarnings(emitResult.Diagnostics, startLine, log);
 213
 214        // If there are no errors, log a debug message
 13215        if (emitResult.Success && log.IsEnabled(LogEventLevel.Debug))
 216        {
 10217            log.Debug("VB.NET script compiled successfully with no errors.");
 218        }
 219
 220        // If there are no errors, proceed to load the assembly and create the delegate
 13221        if (log.IsEnabled(LogEventLevel.Debug))
 222        {
 10223            log.Debug("VB.NET script compiled successfully, loading assembly...");
 224        }
 225
 13226        ms.Position = 0;
 13227        return LoadDelegateFromAssembly<TResult>(ms.ToArray());
 13228    }
 229
 230    /// <summary>
 231    /// Prepares the source code for compilation.
 232    /// </summary>
 233    /// <param name="source">The source code to prepare.</param>
 234    /// <param name="log">The logger instance.</param>
 235    /// <returns>The prepared source code.</returns>
 236    /// <exception cref="ArgumentException">Thrown when the source code is invalid.</exception>
 237    private static int GetStartLineOrThrow(string source, Serilog.ILogger log)
 238    {
 13239        var startIndex = source.IndexOf(StartMarker, StringComparison.Ordinal);
 13240        if (startIndex < 0)
 241        {
 0242            throw new ArgumentException($"VB.NET code must contain the marker '{StartMarker}' to indicate where user cod
 243        }
 244
 13245        var startLine = CcUtilities.GetLineNumber(source, startIndex);
 13246        if (log.IsEnabled(LogEventLevel.Debug))
 247        {
 10248            log.Debug("VB.NET script starts at line {LineNumber}", startLine);
 249        }
 250
 13251        return startLine;
 252    }
 253
 254    /// <summary>
 255    /// Prepares the metadata references for the VB.NET script.
 256    /// </summary>
 257    /// <param name="extraRefs">The extra references to include.</param>
 258    /// <returns>An enumerable of metadata references.</returns>
 259    private static IEnumerable<MetadataReference> BuildMetadataReferences(Assembly[]? extraRefs)
 260    {
 261        // NOTE: Some tests create throwaway assemblies in temp folders and then delete the folder.
 262        // On Windows the delete often fails (file still locked) so Assembly.Location continues to exist.
 263        // On Linux the delete succeeds; the Assembly remains loaded but its Location now points to a
 264        // non-existent path.  Roslyn's MetadataReference.CreateFromFile will throw FileNotFoundException
 265        // in that scenario.  We therefore skip any loaded assemblies whose Location no longer exists.
 13266        var baseRefs = AppDomain.CurrentDomain.GetAssemblies()
 3900267            .Where(a => !a.IsDynamic && SafeHasLocation(a) && !IsSatelliteAssembly(a))
 3271268            .Select(a => MetadataReference.CreateFromFile(a.Location));
 269
 13270        var extras = extraRefs?.Select(r => MetadataReference.CreateFromFile(r.Location))
 13271                     ?? Enumerable.Empty<MetadataReference>();
 272
 273        // Always include the VB runtime explicitly then add our common baseline references.
 13274        return baseRefs
 13275            .Concat(extras)
 13276            .Append(MetadataReference.CreateFromFile(typeof(Microsoft.VisualBasic.Constants).Assembly.Location))
 13277            .Concat(DelegateBuilder.BuildBaselineReferences());
 278    }
 279
 280    private static bool IsSatelliteAssembly(Assembly a)
 281    {
 282        try
 283        {
 3258284            var name = a.GetName();
 3258285            if (name.Name != null && name.Name.EndsWith(".resources", StringComparison.OrdinalIgnoreCase))
 286            {
 0287                return true;
 288            }
 3258289            var loc = a.Location;
 3258290            if (!string.IsNullOrEmpty(loc) && loc.EndsWith(".resources.dll", StringComparison.OrdinalIgnoreCase))
 291            {
 0292                return true;
 293            }
 3258294        }
 0295        catch
 296        {
 297            // If we can't inspect it, be conservative and treat as non-satellite
 0298        }
 3258299        return false;
 0300    }
 301
 302    /// <summary>
 303    /// Collects dynamic imports from the types of the provided locals and shared globals.
 304    /// </summary>
 305    /// <param name="host">The Kestrun host instance.</param>
 306    /// <param name="locals">The local variables to inspect.</param>
 307    /// <returns>A set of unique namespace strings.</returns>
 308    private static HashSet<string> CollectDynamicImports(KestrunHost host, IReadOnlyDictionary<string, object?>? locals)
 309    {
 13310        var imports = new HashSet<string>(StringComparer.Ordinal);
 311        // Merge globals + locals (locals override) just for namespace harvesting
 13312        var globals = host.SharedState.Snapshot();
 26313        foreach (var g in globals)
 314        {
 0315            AddTypeNamespaces(g.Value?.GetType(), imports);
 316        }
 13317        if (locals is { Count: > 0 })
 318        {
 30319            foreach (var l in locals)
 320            {
 9321                AddTypeNamespaces(l.Value?.GetType(), imports);
 322            }
 323        }
 13324        return imports;
 325    }
 326
 327    private static void AddTypeNamespaces(Type? t, HashSet<string> set)
 328    {
 11329        if (t == null)
 330        {
 0331            return;
 332        }
 11333        if (!string.IsNullOrEmpty(t.Namespace))
 334        {
 11335            _ = set.Add(t.Namespace);
 336        }
 11337        if (t.IsGenericType)
 338        {
 0339            foreach (var ga in t.GetGenericArguments())
 340            {
 0341                AddTypeNamespaces(ga, set);
 342            }
 343        }
 11344        if (t.IsArray)
 345        {
 2346            AddTypeNamespaces(t.GetElementType(), set);
 347        }
 11348    }
 349    private static bool SafeHasLocation(Assembly a)
 350    {
 351        try
 352        {
 3851353            var loc = a.Location; // may throw for some dynamic contexts
 3851354            return !string.IsNullOrEmpty(loc) && File.Exists(loc);
 355        }
 0356        catch
 357        {
 0358            return false;
 359        }
 3851360    }
 361    /// <summary>
 362    /// Logs any warnings from the compilation process.
 363    /// </summary>
 364    /// <param name="diagnostics">The diagnostics to check.</param>
 365    /// <param name="startLine">The starting line number.</param>
 366    /// <param name="log">The logger instance.</param>
 367    private static void LogWarnings(ImmutableArray<Diagnostic> diagnostics, int startLine, Serilog.ILogger log)
 368    {
 108369        var warnings = diagnostics.Where(d => d.Severity == DiagnosticSeverity.Warning).ToArray();
 370        // If there are no warnings, log a debug message
 13371        if (warnings.Length == 0)
 372        {
 9373            if (log.IsEnabled(LogEventLevel.Debug))
 374            {
 8375                log.Debug("VB.NET script compiled successfully with no warnings.");
 376            }
 377
 9378            return;
 379        }
 380
 4381        log.Warning($"VBNet script compilation completed with {warnings.Length} warning(s):");
 16382        foreach (var warning in warnings)
 383        {
 4384            var location = warning.Location.IsInSource
 4385                ? $" at line {warning.Location.GetLineSpan().StartLinePosition.Line - startLine + 1}"
 4386                : "";
 4387            log.Warning($"  Warning [{warning.Id}]: {warning.GetMessage()}{location}");
 388        }
 4389        if (log.IsEnabled(LogEventLevel.Debug))
 390        {
 2391            log.Debug("VB.NET script compiled with warnings: {Count}", warnings.Length);
 392        }
 4393    }
 394
 395    /// <summary>
 396    /// Throws an exception if there are compilation errors.
 397    /// </summary>
 398    /// <param name="diagnostics">The diagnostics to check.</param>
 399    /// <param name="startLine">The starting line number.</param>
 400    /// <param name="log">The logger instance.</param>
 401    private static void ThrowIfErrors(ImmutableArray<Diagnostic> diagnostics, int startLine, Serilog.ILogger log)
 402    {
 108403        var errors = diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToArray();
 13404        if (errors.Length == 0)
 405        {
 13406            return;
 407        }
 408
 0409        log.Error($"VBNet script compilation completed with {errors.Length} error(s):");
 0410        var sb = new StringBuilder();
 0411        _ = sb.AppendLine("VBNet route code compilation failed:");
 0412        foreach (var error in errors)
 413        {
 0414            var location = error.Location.IsInSource
 0415                ? $" at line {error.Location.GetLineSpan().StartLinePosition.Line - startLine + 1}"
 0416                : "";
 0417            var msg = $"  Error [{error.Id}]: {error.GetMessage()}{location}";
 0418            log.Error(msg);
 0419            _ = sb.AppendLine(msg);
 420        }
 0421        throw new CompilationErrorException(sb.ToString().TrimEnd(), diagnostics);
 422    }
 423
 424    /// <summary>
 425    /// Loads a delegate from the provided assembly bytes.
 426    /// </summary>
 427    /// <typeparam name="TResult">The type of the result.</typeparam>
 428    /// <param name="asmBytes">The assembly bytes.</param>
 429    /// <returns>A delegate that can be invoked with the specified globals.</returns>
 430    private static Func<CsGlobals, Task<TResult>> LoadDelegateFromAssembly<TResult>(byte[] asmBytes)
 431    {
 13432        var asm = Assembly.Load(asmBytes);
 13433        var runMethod = asm.GetType("RouteScript")!
 13434                           .GetMethod("Run", BindingFlags.Public | BindingFlags.Static)!;
 435
 13436        var delegateType = typeof(Func<,>).MakeGenericType(
 13437            typeof(CsGlobals),
 13438            typeof(Task<>).MakeGenericType(typeof(TResult)));
 439
 13440        return (Func<CsGlobals, Task<TResult>>)runMethod.CreateDelegate(delegateType);
 441    }
 442
 443    /// <summary>
 444    /// Builds the wrapped source code for the VB.NET script.
 445    /// </summary>
 446    /// <param name="code">The user-provided code to wrap.</param>
 447    /// <param name="extraImports">Additional imports to include.</param>
 448    /// <param name="vbReturnType">The return type of the VB.NET function.</param>
 449    /// <param name="locals">Local variables to bind to the script.</param>
 450    /// <returns>The wrapped source code.</returns>
 451    private static string BuildWrappedSource(string? code, IEnumerable<string>? extraImports,
 452    string vbReturnType, IReadOnlyDictionary<string, object?>? locals = null
 453       )
 454    {
 13455        var sb = new StringBuilder();
 456
 457        // common + caller-supplied Imports
 13458        var builtIns = new[] {
 13459        "System", "System.Threading.Tasks",
 13460        "Kestrun", "Kestrun.Models",
 13461          "Microsoft.VisualBasic",
 13462          "Kestrun.Languages"
 13463        };
 464
 260465        foreach (var ns in builtIns.Concat(extraImports ?? [])
 13466                                   .Distinct(StringComparer.Ordinal))
 467        {
 117468            _ = sb.AppendLine($"Imports {ns}");
 469        }
 470
 13471        _ = sb.AppendLine($"""
 13472                Public Module RouteScript
 13473                    Public Async Function Run(g As CsGlobals) As Task(Of {vbReturnType})
 13474                        Await Task.Yield() ' placeholder await
 13475                        Dim Request  = g.Context?.Request
 13476                        Dim Response = g.Context?.Response
 13477                        Dim Context  = g.Context
 13478        """);
 479
 480        // only emit these _when_ you called Compile with locals:
 13481        if (locals?.ContainsKey("username") ?? false)
 482        {
 1483            _ = sb.AppendLine("""
 1484        ' only bind creds if someone passed them in
 1485                        Dim username As String = CStr(g.Locals("username"))
 1486        """);
 487        }
 488
 13489        if (locals?.ContainsKey("password") ?? false)
 490        {
 1491            _ = sb.AppendLine("""
 1492                        Dim password As String = CStr(g.Locals("password"))
 1493        """);
 494        }
 495
 13496        if (locals?.ContainsKey("providedKey") == true)
 497        {
 2498            _ = sb.AppendLine("""
 2499        ' only bind keys if someone passed them in
 2500                        Dim providedKey As String = CStr(g.Locals("providedKey"))
 2501        """);
 502        }
 503
 13504        if (locals?.ContainsKey("providedKeyBytes") == true)
 505        {
 2506            _ = sb.AppendLine("""
 2507                        Dim providedKeyBytes As Byte() = CType(g.Locals("providedKeyBytes"), Byte())
 2508        """);
 509        }
 510
 13511        if (locals?.ContainsKey("identity") == true)
 512        {
 3513            _ = sb.AppendLine("""
 3514                        Dim identity As String = CStr(g.Locals("identity"))
 3515        """);
 516        }
 517
 518        // add the Marker for user code
 13519        _ = sb.AppendLine(StartMarker);
 520        // ---- User code starts here ----
 521
 13522        if (!string.IsNullOrEmpty(code))
 523        {
 524            // indent the user snippet so VB is happy
 13525            _ = sb.AppendLine(string.Join(
 13526                Environment.NewLine,
 28527                code.Split('\n').Select(l => "        " + l.TrimEnd('\r'))));
 528        }
 13529        _ = sb.AppendLine("""
 13530
 13531                End Function
 13532            End Module
 13533    """);
 13534        return sb.ToString();
 535    }
 536}