< 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@5f1d2b981c9d7292c11fd448428c6ab6c811c5de
Line coverage
88%
Covered lines: 239
Uncovered lines: 30
Coverable lines: 269
Total lines: 682
Line coverage: 88.8%
Branch coverage
85%
Covered branches: 135
Total branches: 158
Branch coverage: 85.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 11/19/2025 - 17:40:50 Line coverage: 89.2% (200/224) Branch coverage: 84.3% (113/134) Total lines: 536 Tag: Kestrun/Kestrun@fcf33342333cef0516fe0d0912a86709874fd02603/28/2026 - 19:32:00 Line coverage: 88.8% (239/269) Branch coverage: 85.4% (135/158) Total lines: 682 Tag: Kestrun/Kestrun@84d0d2071c053497504fc6f8a83b92eb3b0e4e21 11/19/2025 - 17:40:50 Line coverage: 89.2% (200/224) Branch coverage: 84.3% (113/134) Total lines: 536 Tag: Kestrun/Kestrun@fcf33342333cef0516fe0d0912a86709874fd02603/28/2026 - 19:32:00 Line coverage: 88.8% (239/269) Branch coverage: 85.4% (135/158) Total lines: 682 Tag: Kestrun/Kestrun@84d0d2071c053497504fc6f8a83b92eb3b0e4e21

Coverage delta

Coverage delta 2 -2

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Build(...)93.75%1616100%
GetVbReturnType(...)100%44100%
Compile(...)89.29%282897.56%
GetStartLineOrThrow(...)75%4485.71%
BuildMetadataReferences(...)100%11100%
IsSatelliteAssembly(...)75%14854.55%
CollectDynamicMetadata(...)66.67%121287.5%
AddTypeMetadata(...)70%121072.73%
AddImportAssemblyLocations(...)83.33%6688.89%
AddAssemblyLocations(...)100%44100%
AddMetadataReferenceLocations(...)100%66100%
AddLoadedAssemblyLocation(...)100%22100%
AddAssemblyLocation(...)50%2257.14%
IsReferenceableAssembly(...)100%44100%
NamespaceExistsInAssembly(...)100%8881.82%
SafeHasLocation(...)100%2260%
LogWarnings(...)90%1010100%
ThrowIfErrors(...)16.67%23621.43%
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
 15internal static class VBNetDelegateBuilder
 16{
 17    /// <summary>
 18    /// The marker that indicates where user code starts in the VB.NET script.
 19    /// This is used to ensure that the user code is correctly placed within the generated module.
 20    /// </summary>
 21    private const string StartMarker = "' ---- User code starts here ----";
 22
 23    /// <summary>
 24    /// Builds a VB.NET delegate for Kestrun routes.
 25    /// </summary>
 26    /// <remarks>
 27    /// This method uses the Roslyn compiler to compile the provided VB.NET code into a delegate.
 28    /// </remarks>
 29    /// <param name="host">The Kestrun host instance.</param>
 30    /// <param name="code">The VB.NET code to compile.</param>
 31    /// <param name="args">The arguments to pass to the script.</param>
 32    /// <param name="extraImports">Optional additional namespaces to import in the script.</param>
 33    /// <param name="extraRefs">Optional additional assemblies to reference in the script.</param>
 34    /// <param name="languageVersion">The VB.NET language version to use for compilation.</param>
 35    /// <returns>A delegate that takes CsGlobals and returns a Task.</returns>
 36    /// <exception cref="CompilationErrorException">Thrown if the compilation fails with errors.</exception>
 37    /// <remarks>
 38    /// This method uses the Roslyn compiler to compile the provided VB.NET code into a delegate.
 39    /// </remarks>
 40    internal static RequestDelegate Build(KestrunHost host,
 41        string code, Dictionary<string, object?>? args, string[]? extraImports,
 42        Assembly[]? extraRefs, LanguageVersion languageVersion = LanguageVersion.VisualBasic16_9)
 43    {
 544        var log = host.Logger;
 545        if (log.IsEnabled(LogEventLevel.Debug))
 46        {
 247            log.Debug("Building VB.NET delegate, script length={Length}, imports={ImportsCount}, refs={RefsCount}, lang=
 248               code.Length, extraImports?.Length ?? 0, extraRefs?.Length ?? 0, languageVersion);
 49        }
 50
 51        // Validate inputs
 552        if (string.IsNullOrWhiteSpace(code))
 53        {
 154            throw new ArgumentNullException(nameof(code), "VB.NET code cannot be null or whitespace.");
 55        }
 56        // 1. Compile the VB.NET code into a script
 57        //    - Use VisualBasicScript.Create() to create a script with the provided code
 58        //    - Use ScriptOptions to specify imports, references, and language version
 59        //    - Inject the provided arguments into the globals
 460        var script = Compile<bool>(host: host, code: code, extraImports: extraImports, extraRefs: extraRefs, null, langu
 61
 62        // 2. Build the per-request delegate
 63        //    - This delegate will be executed for each request
 64        //    - It will create a KestrunContext and CsGlobals, then execute the script with these globals
 65        //    - The script can access the request context and shared state store
 466        if (log.IsEnabled(LogEventLevel.Debug))
 67        {
 268            log.Debug("C# delegate built successfully, script length={Length}, imports={ImportsCount}, refs={RefsCount},
 269                code?.Length, extraImports?.Length ?? 0, extraRefs?.Length ?? 0, languageVersion);
 70        }
 71
 472        return async ctx =>
 473        {
 474            try
 475            {
 376                if (log.IsEnabled(LogEventLevel.Debug))
 477                {
 178                    log.Debug("Preparing execution for C# script at {Path}", ctx.Request.Path);
 479                }
 480
 381                var (Globals, Response, Context) = await DelegateBuilder.PrepareExecutionAsync(host, ctx, args).Configur
 482
 483                // Execute the script with the current context and shared state
 384                if (log.IsEnabled(LogEventLevel.Debug))
 485                {
 186                    log.DebugSanitized("Executing VB.NET script for {Path}", ctx.Request.Path);
 487                }
 488
 389                _ = await script(Globals).ConfigureAwait(false);
 390                if (log.IsEnabled(LogEventLevel.Debug))
 491                {
 192                    log.DebugSanitized("VB.NET script executed successfully for {Path}", ctx.Request.Path);
 493                }
 494
 495                // Apply the response to the Kestrun context
 396                await DelegateBuilder.ApplyResponseAsync(ctx, Response, log).ConfigureAwait(false);
 397            }
 498            finally
 499            {
 4100                // Do not complete the response here; allow downstream middleware (e.g., StatusCodePages)
 4101                // to produce a body for status-only responses when needed.
 4102            }
 7103        };
 104    }
 105
 106    /// <summary>
 107    /// Decide the VB return type string that matches TResult
 108    /// </summary>
 109    /// <param name="t">The type to get the VB return type for.</param>
 110    /// <returns> The VB.NET return type as a string.</returns>
 111    private static string GetVbReturnType(Type t)
 112    {
 16113        if (t == typeof(bool))
 114        {
 10115            return "Boolean";
 116        }
 117
 6118        if (t == typeof(IEnumerable<Claim>))
 119        {
 3120            return "System.Collections.Generic.IEnumerable(Of System.Security.Claims.Claim)";
 121        }
 122
 123        // Fallback so it still compiles even for object / string / etc.
 3124        return "Object";
 125    }
 126
 127    /// <summary>
 128    /// Compiles the provided VB.NET code into a delegate that can be executed with CsGlobals.
 129    /// </summary>
 130    /// <typeparam name="TResult">The type of the result returned by the delegate.</typeparam>
 131    /// <param name="host">The Kestrun host instance.</param>
 132    /// <param name="code">The VB.NET code to compile.</param>
 133    /// <param name="extraImports">Optional additional namespaces to import in the script.</param>
 134    /// <param name="extraRefs">Optional additional assemblies to reference in the script.</param>
 135    /// <param name="locals">Optional local variables to provide to the script.</param>
 136    /// <param name="languageVersion">The VB.NET language version to use for compilation.</param>
 137    /// <returns>A delegate that takes CsGlobals and returns a Task.</returns>
 138    /// <exception cref="CompilationErrorException">Thrown if the compilation fails with errors.</exception>
 139    /// <remarks>
 140    /// This method uses the Roslyn compiler to compile the provided VB.NET code into a delegate.
 141    /// </remarks>
 142    internal static Func<CsGlobals, Task<TResult>> Compile<TResult>(
 143        KestrunHost host,
 144            string? code, string[]? extraImports,
 145            Assembly[]? extraRefs, IReadOnlyDictionary<string, object?>? locals, LanguageVersion languageVersion
 146        )
 147    {
 16148        var log = host.Logger;
 16149        if (log.IsEnabled(LogEventLevel.Debug))
 150        {
 12151            log.Debug("Building VB.NET delegate, script length={Length}, imports={ImportsCount}, refs={RefsCount}, lang=
 12152               code?.Length, extraImports?.Length ?? 0, extraRefs?.Length ?? 0, languageVersion);
 153        }
 154
 155        // Validate inputs
 16156        if (string.IsNullOrWhiteSpace(code))
 157        {
 0158            throw new ArgumentNullException(nameof(code), "VB.NET code cannot be null or whitespace.");
 159        }
 160
 16161        extraImports ??= [];
 16162        extraImports = [.. extraImports, "System.Collections.Generic", "System.Linq", "System.Security.Claims"];
 163
 16164        var (dynamicImports, dynamicRefs) = CollectDynamicMetadata(host, locals);
 16165        if (dynamicImports.Count > 0)
 166        {
 8167            var mergedImports = extraImports.Concat(dynamicImports)
 8168                .Distinct(StringComparer.OrdinalIgnoreCase)
 8169                .ToArray();
 8170            if (log.IsEnabled(LogEventLevel.Debug) && mergedImports.Length != extraImports.Length)
 171            {
 5172                log.Debug("Added {Count} dynamic VB imports from globals/locals.", mergedImports.Length - extraImports.L
 173            }
 8174            extraImports = mergedImports;
 175        }
 176
 177        // 🔧 1.  Build a real VB file around the user snippet
 16178        var source = BuildWrappedSource(code, extraImports, vbReturnType: GetVbReturnType(typeof(TResult)),
 16179            locals: locals);
 180
 181        // Prepares the source code for compilation.
 16182        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
 16186        var tree = VisualBasicSyntaxTree.ParseText(
 16187                   source,
 16188                   new VisualBasicParseOptions(languageVersion));
 189
 16190        var refs = BuildMetadataReferences(extraRefs, extraImports, dynamicRefs);
 191        // 🔧 3.  Normal DLL compilation
 16192        var compilation = VisualBasicCompilation.Create(
 16193                 assemblyName: $"RouteScript_{Guid.NewGuid():N}",
 16194                 syntaxTrees: [tree],
 16195                 references: refs,
 16196                 options: new VisualBasicCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
 197
 16198        using var ms = new MemoryStream();
 16199        var emitResult = compilation.Emit(ms) ?? throw new InvalidOperationException("Failed to compile VB.NET script.")
 200        // 🔧 4.  Log the compilation result
 16201        if (log.IsEnabled(LogEventLevel.Debug))
 202        {
 12203            log.Debug("VB.NET script compilation completed, assembly size={Size} bytes", ms.Length);
 204        }
 205
 206        // 🔧 5. Handle diagnostics
 16207        ThrowIfErrors(emitResult.Diagnostics, startLine, log);
 208        // Log any warnings from the compilation process
 16209        LogWarnings(emitResult.Diagnostics, startLine, log);
 210
 211        // If there are no errors, log a debug message
 16212        if (emitResult.Success && log.IsEnabled(LogEventLevel.Debug))
 213        {
 12214            log.Debug("VB.NET script compiled successfully with no errors.");
 215        }
 216
 217        // If there are no errors, proceed to load the assembly and create the delegate
 16218        if (log.IsEnabled(LogEventLevel.Debug))
 219        {
 12220            log.Debug("VB.NET script compiled successfully, loading assembly...");
 221        }
 222
 16223        ms.Position = 0;
 16224        return LoadDelegateFromAssembly<TResult>(ms.ToArray());
 16225    }
 226
 227    /// <summary>
 228    /// Prepares the source code for compilation.
 229    /// </summary>
 230    /// <param name="source">The source code to prepare.</param>
 231    /// <param name="log">The logger instance.</param>
 232    /// <returns>The prepared source code.</returns>
 233    /// <exception cref="ArgumentException">Thrown when the source code is invalid.</exception>
 234    private static int GetStartLineOrThrow(string source, Serilog.ILogger log)
 235    {
 16236        var startIndex = source.IndexOf(StartMarker, StringComparison.Ordinal);
 16237        if (startIndex < 0)
 238        {
 0239            throw new ArgumentException($"VB.NET code must contain the marker '{StartMarker}' to indicate where user cod
 240        }
 241
 16242        var startLine = CcUtilities.GetLineNumber(source, startIndex);
 16243        if (log.IsEnabled(LogEventLevel.Debug))
 244        {
 12245            log.Debug("VB.NET script starts at line {LineNumber}", startLine);
 246        }
 247
 16248        return startLine;
 249    }
 250
 251    /// <summary>
 252    /// Prepares the metadata references for the VB.NET script.
 253    /// </summary>
 254    /// <param name="extraRefs">The extra references to include.</param>
 255    /// <param name="imports">The effective imports used by the generated source.</param>
 256    /// <param name="dynamicRefs">Assemblies inferred from host globals and compile-time locals.</param>
 257    /// <returns>An enumerable of metadata references.</returns>
 258    private static IEnumerable<MetadataReference> BuildMetadataReferences(
 259        Assembly[]? extraRefs,
 260        IEnumerable<string> imports,
 261        IEnumerable<Assembly> dynamicRefs)
 262    {
 16263        var locations = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 16264        AddMetadataReferenceLocations(locations, DelegateBuilder.BuildBaselineReferences());
 16265        AddAssemblyLocation(locations, typeof(Microsoft.VisualBasic.Constants).Assembly);
 16266        AddAssemblyLocation(locations, typeof(CsGlobals).Assembly);
 16267        AddLoadedAssemblyLocation(locations, "System.Runtime");
 16268        AddLoadedAssemblyLocation(locations, "netstandard");
 16269        AddImportAssemblyLocations(locations, imports);
 16270        AddAssemblyLocations(locations, dynamicRefs);
 16271        AddAssemblyLocations(locations, extraRefs);
 272
 3492273        return locations.Select(static location => MetadataReference.CreateFromFile(location));
 274    }
 275
 276    private static bool IsSatelliteAssembly(Assembly a)
 277    {
 278        try
 279        {
 6125280            var name = a.GetName();
 6125281            if (name.Name != null && name.Name.EndsWith(".resources", StringComparison.OrdinalIgnoreCase))
 282            {
 0283                return true;
 284            }
 6125285            var loc = a.Location;
 6125286            if (!string.IsNullOrEmpty(loc) && loc.EndsWith(".resources.dll", StringComparison.OrdinalIgnoreCase))
 287            {
 0288                return true;
 289            }
 6125290        }
 0291        catch
 292        {
 293            // If we can't inspect it, be conservative and treat as non-satellite
 0294        }
 6125295        return false;
 0296    }
 297
 298    /// <summary>
 299    /// Collects dynamic imports and assembly references from the types of the provided locals and shared globals.
 300    /// </summary>
 301    /// <param name="host">The Kestrun host instance.</param>
 302    /// <param name="locals">The local variables to inspect.</param>
 303    /// <returns>A set of unique namespace strings and the corresponding assembly references.</returns>
 304    private static (HashSet<string> Imports, HashSet<Assembly> References) CollectDynamicMetadata(
 305        KestrunHost host,
 306        IReadOnlyDictionary<string, object?>? locals)
 307    {
 16308        var imports = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 16309        var references = new HashSet<Assembly>();
 32310        foreach (var g in host.SharedState.Snapshot())
 311        {
 0312            AddTypeMetadata(g.Value?.GetType(), imports, references);
 313        }
 314
 16315        if (locals is { Count: > 0 })
 316        {
 38317            foreach (var l in locals)
 318            {
 11319                AddTypeMetadata(l.Value?.GetType(), imports, references);
 320            }
 321        }
 322
 16323        return (imports, references);
 324    }
 325
 326    /// <summary>
 327    /// Adds the namespace and assembly metadata for the supplied type, including generic arguments and array elements.
 328    /// </summary>
 329    /// <param name="type">The type to inspect.</param>
 330    /// <param name="imports">The namespace set to update.</param>
 331    /// <param name="references">The assembly set to update.</param>
 332    private static void AddTypeMetadata(Type? type, HashSet<string> imports, HashSet<Assembly> references)
 333    {
 13334        if (type == null)
 335        {
 0336            return;
 337        }
 338
 13339        if (!string.IsNullOrEmpty(type.Namespace))
 340        {
 13341            _ = imports.Add(type.Namespace);
 342        }
 343
 13344        _ = references.Add(type.Assembly);
 345
 13346        if (type.IsGenericType)
 347        {
 0348            foreach (var genericArgument in type.GetGenericArguments())
 349            {
 0350                AddTypeMetadata(genericArgument, imports, references);
 351            }
 352        }
 353
 13354        if (type.IsArray)
 355        {
 2356            AddTypeMetadata(type.GetElementType(), imports, references);
 357        }
 13358    }
 359
 360    /// <summary>
 361    /// Adds assembly locations needed for the supplied import namespaces.
 362    /// </summary>
 363    /// <param name="locations">The reference location set to update.</param>
 364    /// <param name="imports">The imports used by the generated source.</param>
 365    private static void AddImportAssemblyLocations(HashSet<string> locations, IEnumerable<string> imports)
 366    {
 16367        var importSet = imports
 58368            .Where(importName => !string.IsNullOrWhiteSpace(importName))
 16369            .ToHashSet(StringComparer.OrdinalIgnoreCase);
 16370        if (importSet.Count == 0)
 371        {
 0372            return;
 373        }
 374
 9494375        foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().Where(IsReferenceableAssembly))
 376        {
 21079377            if (importSet.Any(importName => NamespaceExistsInAssembly(assembly, importName)))
 378            {
 1322379                AddAssemblyLocation(locations, assembly);
 380            }
 381        }
 16382    }
 383
 384    /// <summary>
 385    /// Adds all safe assembly locations from the provided collection.
 386    /// </summary>
 387    /// <param name="locations">The reference location set to update.</param>
 388    /// <param name="assemblies">The assemblies to inspect.</param>
 389    private static void AddAssemblyLocations(HashSet<string> locations, IEnumerable<Assembly>? assemblies)
 390    {
 32391        if (assemblies == null)
 392        {
 15393            return;
 394        }
 395
 50396        foreach (var assembly in assemblies)
 397        {
 8398            AddAssemblyLocation(locations, assembly);
 399        }
 17400    }
 401
 402    /// <summary>
 403    /// Adds file paths from existing metadata references to the location set.
 404    /// </summary>
 405    /// <param name="locations">The reference location set to update.</param>
 406    /// <param name="references">The metadata references to inspect.</param>
 407    private static void AddMetadataReferenceLocations(HashSet<string> locations, IEnumerable<MetadataReference> referenc
 408    {
 6916409        foreach (var reference in references.OfType<PortableExecutableReference>())
 410        {
 3442411            var filePath = reference.FilePath;
 3442412            if (!string.IsNullOrWhiteSpace(filePath) && File.Exists(filePath))
 413            {
 3442414                _ = locations.Add(filePath);
 415            }
 416        }
 16417    }
 418
 419    /// <summary>
 420    /// Adds a loaded assembly by simple name when it is available and safe to reference.
 421    /// </summary>
 422    /// <param name="locations">The reference location set to update.</param>
 423    /// <param name="assemblyName">The simple assembly name to resolve.</param>
 424    private static void AddLoadedAssemblyLocation(HashSet<string> locations, string assemblyName)
 425    {
 32426        var assembly = AppDomain.CurrentDomain.GetAssemblies()
 336427            .FirstOrDefault(candidate => string.Equals(candidate.GetName().Name, assemblyName, StringComparison.OrdinalI
 32428        if (assembly != null)
 429        {
 32430            AddAssemblyLocation(locations, assembly);
 431        }
 32432    }
 433
 434    /// <summary>
 435    /// Adds an assembly location when the assembly can be safely referenced by Roslyn.
 436    /// </summary>
 437    /// <param name="locations">The reference location set to update.</param>
 438    /// <param name="assembly">The assembly to inspect.</param>
 439    private static void AddAssemblyLocation(HashSet<string> locations, Assembly assembly)
 440    {
 1394441        if (!IsReferenceableAssembly(assembly))
 442        {
 0443            return;
 444        }
 445
 446        try
 447        {
 1394448            _ = locations.Add(assembly.Location);
 1394449        }
 0450        catch
 451        {
 0452        }
 1394453    }
 454
 455    /// <summary>
 456    /// Determines whether the assembly can be safely used as a metadata reference.
 457    /// </summary>
 458    /// <param name="assembly">The assembly to inspect.</param>
 459    /// <returns><see langword="true"/> when the assembly has a stable file location and is not a satellite assembly.</r
 460    private static bool IsReferenceableAssembly(Assembly assembly)
 6850461        => !assembly.IsDynamic && SafeHasLocation(assembly) && !IsSatelliteAssembly(assembly);
 462
 463    /// <summary>
 464    /// Determines whether the supplied namespace exists in the assembly.
 465    /// </summary>
 466    /// <param name="assembly">The assembly to inspect.</param>
 467    /// <param name="namespaceName">The namespace to search for.</param>
 468    /// <returns><see langword="true"/> when the namespace or one of its children is present.</returns>
 469    private static bool NamespaceExistsInAssembly(Assembly assembly, string namespaceName)
 470    {
 471        try
 472        {
 4461298473            foreach (var type in assembly.DefinedTypes)
 474            {
 2214962475                var currentNamespace = type.Namespace;
 2214962476                if (currentNamespace == null)
 477                {
 478                    continue;
 479                }
 480
 2132968481                if (currentNamespace.Equals(namespaceName, StringComparison.OrdinalIgnoreCase)
 2132968482                    || currentNamespace.StartsWith(namespaceName + ".", StringComparison.OrdinalIgnoreCase))
 483                {
 1322484                    return true;
 485                }
 486            }
 15026487        }
 0488        catch (ReflectionTypeLoadException)
 489        {
 490            // If we can't load all types, be conservative and assume the namespace is not present
 0491        }
 492
 15026493        return false;
 1322494    }
 495    private static bool SafeHasLocation(Assembly a)
 496    {
 497        try
 498        {
 6775499            var loc = a.Location; // may throw for some dynamic contexts
 6775500            return !string.IsNullOrEmpty(loc) && File.Exists(loc);
 501        }
 0502        catch
 503        {
 0504            return false;
 505        }
 6775506    }
 507    /// <summary>
 508    /// Logs any warnings from the compilation process.
 509    /// </summary>
 510    /// <param name="diagnostics">The diagnostics to check.</param>
 511    /// <param name="startLine">The starting line number.</param>
 512    /// <param name="log">The logger instance.</param>
 513    private static void LogWarnings(ImmutableArray<Diagnostic> diagnostics, int startLine, Serilog.ILogger log)
 514    {
 134515        var warnings = diagnostics.Where(d => d.Severity == DiagnosticSeverity.Warning).ToArray();
 516        // If there are no warnings, log a debug message
 16517        if (warnings.Length == 0)
 518        {
 11519            if (log.IsEnabled(LogEventLevel.Debug))
 520            {
 10521                log.Debug("VB.NET script compiled successfully with no warnings.");
 522            }
 523
 11524            return;
 525        }
 526
 5527        log.Warning($"VBNet script compilation completed with {warnings.Length} warning(s):");
 20528        foreach (var warning in warnings)
 529        {
 5530            var location = warning.Location.IsInSource
 5531                ? $" at line {warning.Location.GetLineSpan().StartLinePosition.Line - startLine + 1}"
 5532                : "";
 5533            log.Warning($"  Warning [{warning.Id}]: {warning.GetMessage()}{location}");
 534        }
 5535        if (log.IsEnabled(LogEventLevel.Debug))
 536        {
 2537            log.Debug("VB.NET script compiled with warnings: {Count}", warnings.Length);
 538        }
 5539    }
 540
 541    /// <summary>
 542    /// Throws an exception if there are compilation errors.
 543    /// </summary>
 544    /// <param name="diagnostics">The diagnostics to check.</param>
 545    /// <param name="startLine">The starting line number.</param>
 546    /// <param name="log">The logger instance.</param>
 547    private static void ThrowIfErrors(ImmutableArray<Diagnostic> diagnostics, int startLine, Serilog.ILogger log)
 548    {
 134549        var errors = diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToArray();
 16550        if (errors.Length == 0)
 551        {
 16552            return;
 553        }
 554
 0555        log.Error($"VBNet script compilation completed with {errors.Length} error(s):");
 0556        var sb = new StringBuilder();
 0557        _ = sb.AppendLine("VBNet route code compilation failed:");
 0558        foreach (var error in errors)
 559        {
 0560            var location = error.Location.IsInSource
 0561                ? $" at line {error.Location.GetLineSpan().StartLinePosition.Line - startLine + 1}"
 0562                : "";
 0563            var msg = $"  Error [{error.Id}]: {error.GetMessage()}{location}";
 0564            log.Error(msg);
 0565            _ = sb.AppendLine(msg);
 566        }
 0567        throw new CompilationErrorException(sb.ToString().TrimEnd(), diagnostics);
 568    }
 569
 570    /// <summary>
 571    /// Loads a delegate from the provided assembly bytes.
 572    /// </summary>
 573    /// <typeparam name="TResult">The type of the result.</typeparam>
 574    /// <param name="asmBytes">The assembly bytes.</param>
 575    /// <returns>A delegate that can be invoked with the specified globals.</returns>
 576    private static Func<CsGlobals, Task<TResult>> LoadDelegateFromAssembly<TResult>(byte[] asmBytes)
 577    {
 16578        var asm = Assembly.Load(asmBytes);
 16579        var runMethod = asm.GetType("RouteScript")!
 16580                           .GetMethod("Run", BindingFlags.Public | BindingFlags.Static)!;
 581
 16582        var delegateType = typeof(Func<,>).MakeGenericType(
 16583            typeof(CsGlobals),
 16584            typeof(Task<>).MakeGenericType(typeof(TResult)));
 585
 16586        return (Func<CsGlobals, Task<TResult>>)runMethod.CreateDelegate(delegateType);
 587    }
 588
 589    /// <summary>
 590    /// Builds the wrapped source code for the VB.NET script.
 591    /// </summary>
 592    /// <param name="code">The user-provided code to wrap.</param>
 593    /// <param name="extraImports">Additional imports to include.</param>
 594    /// <param name="vbReturnType">The return type of the VB.NET function.</param>
 595    /// <param name="locals">Local variables to bind to the script.</param>
 596    /// <returns>The wrapped source code.</returns>
 597    private static string BuildWrappedSource(string? code, IEnumerable<string>? extraImports,
 598    string vbReturnType, IReadOnlyDictionary<string, object?>? locals = null
 599       )
 600    {
 16601        var sb = new StringBuilder();
 602
 603        // common + caller-supplied Imports
 16604        var builtIns = new[] {
 16605        "System", "System.Threading.Tasks",
 16606        "Kestrun", "Kestrun.Models",
 16607          "Microsoft.VisualBasic",
 16608          "Kestrun.Languages"
 16609        };
 610
 326611        foreach (var ns in builtIns.Concat(extraImports ?? [])
 16612                                   .Distinct(StringComparer.OrdinalIgnoreCase))
 613        {
 147614            _ = sb.AppendLine($"Imports {ns}");
 615        }
 616
 16617        _ = sb.AppendLine($"""
 16618                Public Module RouteScript
 16619                    Public Async Function Run(g As CsGlobals) As Task(Of {vbReturnType})
 16620                        Await Task.Yield() ' placeholder await
 16621                        Dim Request  = g.Context?.Request
 16622                        Dim Response = g.Context?.Response
 16623                        Dim Context  = g.Context
 16624        """);
 625
 626        // only emit these _when_ you called Compile with locals:
 16627        if (locals?.ContainsKey("username") ?? false)
 628        {
 2629            _ = sb.AppendLine("""
 2630        ' only bind creds if someone passed them in
 2631                        Dim username As String = CStr(g.Locals("username"))
 2632        """);
 633        }
 634
 16635        if (locals?.ContainsKey("password") ?? false)
 636        {
 1637            _ = sb.AppendLine("""
 1638                        Dim password As String = CStr(g.Locals("password"))
 1639        """);
 640        }
 641
 16642        if (locals?.ContainsKey("providedKey") == true)
 643        {
 2644            _ = sb.AppendLine("""
 2645        ' only bind keys if someone passed them in
 2646                        Dim providedKey As String = CStr(g.Locals("providedKey"))
 2647        """);
 648        }
 649
 16650        if (locals?.ContainsKey("providedKeyBytes") == true)
 651        {
 2652            _ = sb.AppendLine("""
 2653                        Dim providedKeyBytes As Byte() = CType(g.Locals("providedKeyBytes"), Byte())
 2654        """);
 655        }
 656
 16657        if (locals?.ContainsKey("identity") == true)
 658        {
 3659            _ = sb.AppendLine("""
 3660                        Dim identity As String = CStr(g.Locals("identity"))
 3661        """);
 662        }
 663
 664        // add the Marker for user code
 16665        _ = sb.AppendLine(StartMarker);
 666        // ---- User code starts here ----
 667
 16668        if (!string.IsNullOrEmpty(code))
 669        {
 670            // indent the user snippet so VB is happy
 16671            _ = sb.AppendLine(string.Join(
 16672                Environment.NewLine,
 37673                code.Split('\n').Select(l => "        " + l.TrimEnd('\r'))));
 674        }
 16675        _ = sb.AppendLine("""
 16676
 16677                End Function
 16678            End Module
 16679    """);
 16680        return sb.ToString();
 681    }
 682}

Methods/Properties

Build(Kestrun.Hosting.KestrunHost,System.String,System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.String[],System.Reflection.Assembly[],Microsoft.CodeAnalysis.VisualBasic.LanguageVersion)
GetVbReturnType(System.Type)
Compile(Kestrun.Hosting.KestrunHost,System.String,System.String[],System.Reflection.Assembly[],System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Object>,Microsoft.CodeAnalysis.VisualBasic.LanguageVersion)
GetStartLineOrThrow(System.String,Serilog.ILogger)
BuildMetadataReferences(System.Reflection.Assembly[],System.Collections.Generic.IEnumerable`1<System.String>,System.Collections.Generic.IEnumerable`1<System.Reflection.Assembly>)
IsSatelliteAssembly(System.Reflection.Assembly)
CollectDynamicMetadata(Kestrun.Hosting.KestrunHost,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Object>)
AddTypeMetadata(System.Type,System.Collections.Generic.HashSet`1<System.String>,System.Collections.Generic.HashSet`1<System.Reflection.Assembly>)
AddImportAssemblyLocations(System.Collections.Generic.HashSet`1<System.String>,System.Collections.Generic.IEnumerable`1<System.String>)
AddAssemblyLocations(System.Collections.Generic.HashSet`1<System.String>,System.Collections.Generic.IEnumerable`1<System.Reflection.Assembly>)
AddMetadataReferenceLocations(System.Collections.Generic.HashSet`1<System.String>,System.Collections.Generic.IEnumerable`1<Microsoft.CodeAnalysis.MetadataReference>)
AddLoadedAssemblyLocation(System.Collections.Generic.HashSet`1<System.String>,System.String)
AddAssemblyLocation(System.Collections.Generic.HashSet`1<System.String>,System.Reflection.Assembly)
IsReferenceableAssembly(System.Reflection.Assembly)
NamespaceExistsInAssembly(System.Reflection.Assembly,System.String)
SafeHasLocation(System.Reflection.Assembly)
LogWarnings(System.Collections.Immutable.ImmutableArray`1<Microsoft.CodeAnalysis.Diagnostic>,System.Int32,Serilog.ILogger)
ThrowIfErrors(System.Collections.Immutable.ImmutableArray`1<Microsoft.CodeAnalysis.Diagnostic>,System.Int32,Serilog.ILogger)
LoadDelegateFromAssembly(System.Byte[])
BuildWrappedSource(System.String,System.Collections.Generic.IEnumerable`1<System.String>,System.String,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Object>)