< 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@2d87023b37eb91155071c91dd3d6a2eeb3004705
Line coverage
94%
Covered lines: 197
Uncovered lines: 11
Coverable lines: 208
Total lines: 507
Line coverage: 94.7%
Branch coverage
89%
Covered branches: 111
Total branches: 124
Branch coverage: 89.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 08/26/2025 - 01:25:22 Line coverage: 89.9% (170/189) Branch coverage: 71.8% (69/96) Total lines: 450 Tag: Kestrun/Kestrun@07f821172e5dc3657f1be7e6818f18d6721cf38a09/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@c33ec02a85e4f8d6061aeaab5a5e8c3a8b665594 08/26/2025 - 01:25:22 Line coverage: 89.9% (170/189) Branch coverage: 71.8% (69/96) Total lines: 450 Tag: Kestrun/Kestrun@07f821172e5dc3657f1be7e6818f18d6721cf38a09/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@c33ec02a85e4f8d6061aeaab5a5e8c3a8b665594

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Build(...)93.75%1616100%
GetVbReturnType(...)100%44100%
Compile(...)89.28%282897.5%
GetStartLineOrThrow(...)75%4485.71%
BuildMetadataReferences(...)100%66100%
CollectDynamicImports(...)91.66%1212100%
AddTypeNamespaces(...)100%1010100%
SafeHasLocation(...)100%2260%
LogWarnings(...)90%1010100%
ThrowIfErrors(...)16.66%18630%
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.SharedState;
 12using Kestrun.Hosting;
 13
 14namespace Kestrun.Languages;
 15
 16
 17internal static class VBNetDelegateBuilder
 18{
 19    /// <summary>
 20    /// The marker that indicates where user code starts in the VB.NET script.
 21    /// This is used to ensure that the user code is correctly placed within the generated module.
 22    /// </summary>
 23    private const string StartMarker = "' ---- User code starts here ----";
 24
 25    /// <summary>
 26    /// Builds a VB.NET delegate for Kestrun routes.
 27    /// </summary>
 28    /// <remarks>
 29    /// This method uses the Roslyn compiler to compile the provided VB.NET code into a delegate.
 30    /// </remarks>
 31    /// <param name="host">The Kestrun host instance.</param>
 32    /// <param name="code">The VB.NET code to compile.</param>
 33    /// <param name="args">The arguments to pass to the script.</param>
 34    /// <param name="extraImports">Optional additional namespaces to import in the script.</param>
 35    /// <param name="extraRefs">Optional additional assemblies to reference in the script.</param>
 36    /// <param name="languageVersion">The VB.NET language version to use for compilation.</param>
 37    /// <returns>A delegate that takes CsGlobals and returns a Task.</returns>
 38    /// <exception cref="CompilationErrorException">Thrown if the compilation fails with errors.</exception>
 39    /// <remarks>
 40    /// This method uses the Roslyn compiler to compile the provided VB.NET code into a delegate.
 41    /// </remarks>
 42    internal static RequestDelegate Build(KestrunHost host,
 43        string code, Dictionary<string, object?>? args, string[]? extraImports,
 44        Assembly[]? extraRefs, LanguageVersion languageVersion = LanguageVersion.VisualBasic16_9)
 45    {
 546        var log = host.Logger;
 547        if (log.IsEnabled(LogEventLevel.Debug))
 48        {
 449            log.Debug("Building VB.NET delegate, script length={Length}, imports={ImportsCount}, refs={RefsCount}, lang=
 450               code.Length, extraImports?.Length ?? 0, extraRefs?.Length ?? 0, languageVersion);
 51        }
 52
 53        // Validate inputs
 554        if (string.IsNullOrWhiteSpace(code))
 55        {
 156            throw new ArgumentNullException(nameof(code), "VB.NET code cannot be null or whitespace.");
 57        }
 58        // 1. Compile the VB.NET code into a script
 59        //    - Use VisualBasicScript.Create() to create a script with the provided code
 60        //    - Use ScriptOptions to specify imports, references, and language version
 61        //    - Inject the provided arguments into the globals
 462        var script = Compile<bool>(code, log, extraImports, extraRefs, null, languageVersion);
 63
 64        // 2. Build the per-request delegate
 65        //    - This delegate will be executed for each request
 66        //    - It will create a KestrunContext and CsGlobals, then execute the script with these globals
 67        //    - The script can access the request context and shared state store
 468        if (log.IsEnabled(LogEventLevel.Debug))
 69        {
 370            log.Debug("C# delegate built successfully, script length={Length}, imports={ImportsCount}, refs={RefsCount},
 371                code?.Length, extraImports?.Length ?? 0, extraRefs?.Length ?? 0, languageVersion);
 72        }
 73
 474        return async ctx =>
 475        {
 476            try
 477            {
 378                if (log.IsEnabled(LogEventLevel.Debug))
 479                {
 280                    log.Debug("Preparing execution for C# script at {Path}", ctx.Request.Path);
 481                }
 482
 383                var (Globals, Response, Context) = await DelegateBuilder.PrepareExecutionAsync(host, ctx, args).Configur
 484
 485                // Execute the script with the current context and shared state
 386                if (log.IsEnabled(LogEventLevel.Debug))
 487                {
 288                    log.DebugSanitized("Executing VB.NET script for {Path}", ctx.Request.Path);
 489                }
 490
 391                _ = await script(Globals).ConfigureAwait(false);
 392                if (log.IsEnabled(LogEventLevel.Debug))
 493                {
 294                    log.DebugSanitized("VB.NET script executed successfully for {Path}", ctx.Request.Path);
 495                }
 496
 497                // Apply the response to the Kestrun context
 398                await DelegateBuilder.ApplyResponseAsync(ctx, Response, log).ConfigureAwait(false);
 399            }
 4100            finally
 4101            {
 4102                // Do not complete the response here; allow downstream middleware (e.g., StatusCodePages)
 4103                // to produce a body for status-only responses when needed.
 4104            }
 7105        };
 106    }
 107
 108    /// <summary>
 109    /// Decide the VB return type string that matches TResult
 110    /// </summary>
 111    /// <param name="t">The type to get the VB return type for.</param>
 112    /// <returns> The VB.NET return type as a string.</returns>
 113    private static string GetVbReturnType(Type t)
 114    {
 13115        if (t == typeof(bool))
 116        {
 8117            return "Boolean";
 118        }
 119
 5120        if (t == typeof(IEnumerable<Claim>))
 121        {
 3122            return "System.Collections.Generic.IEnumerable(Of System.Security.Claims.Claim)";
 123        }
 124
 125        // Fallback so it still compiles even for object / string / etc.
 2126        return "Object";
 127    }
 128
 129    /// <summary>
 130    /// Compiles the provided VB.NET code into a delegate that can be executed with CsGlobals.
 131    /// </summary>
 132    /// <param name="code">The VB.NET code to compile.</param>
 133    /// <param name="log">The logger to use for logging compilation errors and warnings.</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            string? code, Serilog.ILogger log, string[]? extraImports,
 145            Assembly[]? extraRefs, IReadOnlyDictionary<string, object?>? locals, LanguageVersion languageVersion
 146        )
 147    {
 13148        if (log.IsEnabled(LogEventLevel.Debug))
 149        {
 10150            log.Debug("Building VB.NET delegate, script length={Length}, imports={ImportsCount}, refs={RefsCount}, lang=
 10151               code?.Length, extraImports?.Length ?? 0, extraRefs?.Length ?? 0, languageVersion);
 152        }
 153
 154        // Validate inputs
 13155        if (string.IsNullOrWhiteSpace(code))
 156        {
 0157            throw new ArgumentNullException(nameof(code), "VB.NET code cannot be null or whitespace.");
 158        }
 159
 13160        extraImports ??= [];
 13161        extraImports = [.. extraImports, "System.Collections.Generic", "System.Linq", "System.Security.Claims"];
 162
 163        // Discover dynamic namespaces from globals + locals similar to C# path
 13164        var dynamicImports = CollectDynamicImports(locals);
 13165        if (dynamicImports.Count > 0)
 166        {
 11167            var mergedImports = extraImports.Concat(dynamicImports)
 11168                .Distinct(StringComparer.Ordinal)
 11169                .ToArray();
 11170            if (log.IsEnabled(LogEventLevel.Debug) && mergedImports.Length != extraImports.Length)
 171            {
 8172                log.Debug("Added {Count} dynamic VB imports from globals/locals.", mergedImports.Length - extraImports.L
 173            }
 11174            extraImports = mergedImports;
 175        }
 176
 177        // 🔧 1.  Build a real VB file around the user snippet
 13178        var source = BuildWrappedSource(code, extraImports, vbReturnType: GetVbReturnType(typeof(TResult)),
 13179            locals: locals);
 180
 181        // Prepares the source code for compilation.
 13182        var startLine = GetStartLineOrThrow(source, log);
 183
 184        // Parse the source code into a syntax tree
 185        // This will allow us to analyze and compile the code
 13186        var tree = VisualBasicSyntaxTree.ParseText(
 13187                       source,
 13188                       new VisualBasicParseOptions(LanguageVersion.VisualBasic16));
 189
 190        // 🔧 2.  References = everything already loaded  +  extras
 13191        var refs = BuildMetadataReferences(extraRefs);
 192        // 🔧 3.  Normal DLL compilation
 13193        var compilation = VisualBasicCompilation.Create(
 13194                 assemblyName: $"RouteScript_{Guid.NewGuid():N}",
 13195                 syntaxTrees: [tree],
 13196                 references: refs,
 13197                 options: new VisualBasicCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
 198
 13199        using var ms = new MemoryStream();
 13200        var emitResult = compilation.Emit(ms) ?? throw new InvalidOperationException("Failed to compile VB.NET script.")
 201        // 🔧 4.  Log the compilation result
 13202        if (log.IsEnabled(LogEventLevel.Debug))
 203        {
 10204            log.Debug("VB.NET script compilation completed, assembly size={Size} bytes", ms.Length);
 205        }
 206
 207        // 🔧 5. Handle diagnostics
 13208        ThrowIfErrors(emitResult.Diagnostics, startLine, log);
 209        // Log any warnings from the compilation process
 13210        LogWarnings(emitResult.Diagnostics, startLine, log);
 211
 212        // If there are no errors, log a debug message
 13213        if (emitResult.Success && log.IsEnabled(LogEventLevel.Debug))
 214        {
 10215            log.Debug("VB.NET script compiled successfully with no errors.");
 216        }
 217
 218        // If there are no errors, proceed to load the assembly and create the delegate
 13219        if (log.IsEnabled(LogEventLevel.Debug))
 220        {
 10221            log.Debug("VB.NET script compiled successfully, loading assembly...");
 222        }
 223
 13224        ms.Position = 0;
 13225        return LoadDelegateFromAssembly<TResult>(ms.ToArray());
 13226    }
 227
 228    /// <summary>
 229    /// Prepares the source code for compilation.
 230    /// </summary>
 231    /// <param name="source">The source code to prepare.</param>
 232    /// <param name="log">The logger instance.</param>
 233    /// <returns>The prepared source code.</returns>
 234    /// <exception cref="ArgumentException">Thrown when the source code is invalid.</exception>
 235    private static int GetStartLineOrThrow(string source, Serilog.ILogger log)
 236    {
 13237        var startIndex = source.IndexOf(StartMarker, StringComparison.Ordinal);
 13238        if (startIndex < 0)
 239        {
 0240            throw new ArgumentException($"VB.NET code must contain the marker '{StartMarker}' to indicate where user cod
 241        }
 242
 13243        var startLine = CcUtilities.GetLineNumber(source, startIndex);
 13244        if (log.IsEnabled(LogEventLevel.Debug))
 245        {
 10246            log.Debug("VB.NET script starts at line {LineNumber}", startLine);
 247        }
 248
 13249        return startLine;
 250    }
 251
 252    /// <summary>
 253    /// Prepares the metadata references for the VB.NET script.
 254    /// </summary>
 255    /// <param name="extraRefs">The extra references to include.</param>
 256    /// <returns>An enumerable of metadata references.</returns>
 257    private static IEnumerable<MetadataReference> BuildMetadataReferences(Assembly[]? extraRefs)
 258    {
 259        // NOTE: Some tests create throwaway assemblies in temp folders and then delete the folder.
 260        // On Windows the delete often fails (file still locked) so Assembly.Location continues to exist.
 261        // On Linux the delete succeeds; the Assembly remains loaded but its Location now points to a
 262        // non-existent path.  Roslyn's MetadataReference.CreateFromFile will throw FileNotFoundException
 263        // in that scenario.  We therefore skip any loaded assemblies whose Location no longer exists.
 13264        var baseRefs = AppDomain.CurrentDomain.GetAssemblies()
 3959265            .Where(a => !a.IsDynamic && SafeHasLocation(a))
 3379266            .Select(a => MetadataReference.CreateFromFile(a.Location));
 267
 13268        var extras = extraRefs?.Select(r => MetadataReference.CreateFromFile(r.Location))
 13269                     ?? Enumerable.Empty<MetadataReference>();
 270
 271        // Always include the VB runtime explicitly then add our common baseline references.
 13272        return baseRefs
 13273            .Concat(extras)
 13274            .Append(MetadataReference.CreateFromFile(typeof(Microsoft.VisualBasic.Constants).Assembly.Location))
 13275            .Concat(DelegateBuilder.BuildBaselineReferences());
 276    }
 277
 278    /// <summary>
 279    /// Collects dynamic imports from the types of the provided locals and shared globals.
 280    /// </summary>
 281    /// <param name="locals">The local variables to inspect.</param>
 282    /// <returns>A set of unique namespace strings.</returns>
 283    private static HashSet<string> CollectDynamicImports(IReadOnlyDictionary<string, object?>? locals)
 284    {
 13285        var imports = new HashSet<string>(StringComparer.Ordinal);
 286        // Merge globals + locals (locals override) just for namespace harvesting
 13287        var globals = SharedStateStore.Snapshot();
 150288        foreach (var g in globals)
 289        {
 62290            AddTypeNamespaces(g.Value?.GetType(), imports);
 291        }
 13292        if (locals is { Count: > 0 })
 293        {
 30294            foreach (var l in locals)
 295            {
 9296                AddTypeNamespaces(l.Value?.GetType(), imports);
 297            }
 298        }
 13299        return imports;
 300    }
 301
 302    private static void AddTypeNamespaces(Type? t, HashSet<string> set)
 303    {
 89304        if (t == null)
 305        {
 35306            return;
 307        }
 54308        if (!string.IsNullOrEmpty(t.Namespace))
 309        {
 54310            _ = set.Add(t.Namespace!);
 311        }
 54312        if (t.IsGenericType)
 313        {
 54314            foreach (var ga in t.GetGenericArguments())
 315            {
 16316                AddTypeNamespaces(ga, set);
 317            }
 318        }
 54319        if (t.IsArray)
 320        {
 2321            AddTypeNamespaces(t.GetElementType(), set);
 322        }
 54323    }
 324    private static bool SafeHasLocation(Assembly a)
 325    {
 326        try
 327        {
 3927328            var loc = a.Location; // may throw for some dynamic contexts
 3927329            return !string.IsNullOrEmpty(loc) && File.Exists(loc);
 330        }
 0331        catch
 332        {
 0333            return false;
 334        }
 3927335    }
 336    /// <summary>
 337    /// Logs any warnings from the compilation process.
 338    /// </summary>
 339    /// <param name="diagnostics">The diagnostics to check.</param>
 340    /// <param name="startLine">The starting line number.</param>
 341    /// <param name="log">The logger instance.</param>
 342    private static void LogWarnings(ImmutableArray<Diagnostic> diagnostics, int startLine, Serilog.ILogger log)
 343    {
 108344        var warnings = diagnostics.Where(d => d.Severity == DiagnosticSeverity.Warning).ToArray();
 345        // If there are no warnings, log a debug message
 13346        if (warnings.Length == 0)
 347        {
 9348            if (log.IsEnabled(LogEventLevel.Debug))
 349            {
 8350                log.Debug("VB.NET script compiled successfully with no warnings.");
 351            }
 352
 9353            return;
 354        }
 355
 4356        log.Warning($"VBNet script compilation completed with {warnings.Length} warning(s):");
 16357        foreach (var warning in warnings)
 358        {
 4359            var location = warning.Location.IsInSource
 4360                ? $" at line {warning.Location.GetLineSpan().StartLinePosition.Line - startLine + 1}"
 4361                : "";
 4362            log.Warning($"  Warning [{warning.Id}]: {warning.GetMessage()}{location}");
 363        }
 4364        if (log.IsEnabled(LogEventLevel.Debug))
 365        {
 2366            log.Debug("VB.NET script compiled with warnings: {Count}", warnings.Length);
 367        }
 4368    }
 369
 370    /// <summary>
 371    /// Throws an exception if there are compilation errors.
 372    /// </summary>
 373    /// <param name="diagnostics">The diagnostics to check.</param>
 374    /// <param name="startLine">The starting line number.</param>
 375    /// <param name="log">The logger instance.</param>
 376    private static void ThrowIfErrors(ImmutableArray<Diagnostic> diagnostics, int startLine, Serilog.ILogger log)
 377    {
 108378        var errors = diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToArray();
 13379        if (errors.Length == 0)
 380        {
 13381            return;
 382        }
 383
 0384        log.Error($"VBNet script compilation completed with {errors.Length} error(s):");
 0385        foreach (var error in errors)
 386        {
 0387            var location = error.Location.IsInSource
 0388                ? $" at line {error.Location.GetLineSpan().StartLinePosition.Line - startLine + 1}"
 0389                : "";
 0390            log.Error($"  Error [{error.Id}]: {error.GetMessage()}{location}");
 391        }
 0392        throw new CompilationErrorException("VBNet route code compilation failed", diagnostics);
 393    }
 394
 395    /// <summary>
 396    /// Loads a delegate from the provided assembly bytes.
 397    /// </summary>
 398    /// <typeparam name="TResult">The type of the result.</typeparam>
 399    /// <param name="asmBytes">The assembly bytes.</param>
 400    /// <returns>A delegate that can be invoked with the specified globals.</returns>
 401    private static Func<CsGlobals, Task<TResult>> LoadDelegateFromAssembly<TResult>(byte[] asmBytes)
 402    {
 13403        var asm = Assembly.Load(asmBytes);
 13404        var runMethod = asm.GetType("RouteScript")!
 13405                           .GetMethod("Run", BindingFlags.Public | BindingFlags.Static)!;
 406
 13407        var delegateType = typeof(Func<,>).MakeGenericType(
 13408            typeof(CsGlobals),
 13409            typeof(Task<>).MakeGenericType(typeof(TResult)));
 410
 13411        return (Func<CsGlobals, Task<TResult>>)runMethod.CreateDelegate(delegateType);
 412    }
 413
 414    /// <summary>
 415    /// Builds the wrapped source code for the VB.NET script.
 416    /// </summary>
 417    /// <param name="code">The user-provided code to wrap.</param>
 418    /// <param name="extraImports">Additional imports to include.</param>
 419    /// <param name="vbReturnType">The return type of the VB.NET function.</param>
 420    /// <param name="locals">Local variables to bind to the script.</param>
 421    /// <returns>The wrapped source code.</returns>
 422    private static string BuildWrappedSource(string? code, IEnumerable<string>? extraImports,
 423    string vbReturnType, IReadOnlyDictionary<string, object?>? locals = null
 424       )
 425    {
 13426        var sb = new StringBuilder();
 427
 428        // common + caller-supplied Imports
 13429        var builtIns = new[] {
 13430        "System", "System.Threading.Tasks",
 13431        "Kestrun", "Kestrun.Models",
 13432          "Microsoft.VisualBasic",
 13433          "Kestrun.Languages"
 13434        };
 435
 260436        foreach (var ns in builtIns.Concat(extraImports ?? [])
 13437                                   .Distinct(StringComparer.Ordinal))
 438        {
 117439            _ = sb.AppendLine($"Imports {ns}");
 440        }
 441
 13442        _ = sb.AppendLine($"""
 13443                Public Module RouteScript
 13444                    Public Async Function Run(g As CsGlobals) As Task(Of {vbReturnType})
 13445                        Await Task.Yield() ' placeholder await
 13446                        Dim Request  = g.Context?.Request
 13447                        Dim Response = g.Context?.Response
 13448                        Dim Context  = g.Context
 13449        """);
 450
 451        // only emit these _when_ you called Compile with locals:
 13452        if (locals?.ContainsKey("username") ?? false)
 453        {
 1454            _ = sb.AppendLine("""
 1455        ' only bind creds if someone passed them in
 1456                        Dim username As String = CStr(g.Locals("username"))
 1457        """);
 458        }
 459
 13460        if (locals?.ContainsKey("password") ?? false)
 461        {
 1462            _ = sb.AppendLine("""
 1463                        Dim password As String = CStr(g.Locals("password"))
 1464        """);
 465        }
 466
 13467        if (locals?.ContainsKey("providedKey") == true)
 468        {
 2469            _ = sb.AppendLine("""
 2470        ' only bind keys if someone passed them in
 2471                        Dim providedKey As String = CStr(g.Locals("providedKey"))
 2472        """);
 473        }
 474
 13475        if (locals?.ContainsKey("providedKeyBytes") == true)
 476        {
 2477            _ = sb.AppendLine("""
 2478                        Dim providedKeyBytes As Byte() = CType(g.Locals("providedKeyBytes"), Byte())
 2479        """);
 480        }
 481
 13482        if (locals?.ContainsKey("identity") == true)
 483        {
 3484            _ = sb.AppendLine("""
 3485                        Dim identity As String = CStr(g.Locals("identity"))
 3486        """);
 487        }
 488
 489        // add the Marker for user code
 13490        _ = sb.AppendLine(StartMarker);
 491        // ---- User code starts here ----
 492
 13493        if (!string.IsNullOrEmpty(code))
 494        {
 495            // indent the user snippet so VB is happy
 13496            _ = sb.AppendLine(string.Join(
 13497                Environment.NewLine,
 28498                code.Split('\n').Select(l => "        " + l.TrimEnd('\r'))));
 499        }
 13500        _ = sb.AppendLine("""
 13501
 13502                End Function
 13503            End Module
 13504    """);
 13505        return sb.ToString();
 506    }
 507}