< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Languages.CSharpDelegateBuilder
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Languages/CSharpDelegateBuilder.cs
Tag: Kestrun/Kestrun@9d3a582b2d63930269564a7591aa77ef297cadeb
Line coverage
82%
Covered lines: 190
Uncovered lines: 41
Coverable lines: 231
Total lines: 586
Line coverage: 82.2%
Branch coverage
69%
Covered branches: 126
Total branches: 182
Branch coverage: 69.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Build(...)66.66%1818100%
Compile(...)64.28%342880.48%
CollectLoadedAssemblyReferences(...)80%161061.11%
CollectKestrunNamespaces(...)100%22100%
CreateScriptOptions(...)50%22100%
AddExtraImports(...)83.33%66100%
AddExtraReferences(...)16.66%911218.18%
AddLoadedAssemblyReferences(...)58.33%121287.5%
BuildGlobalsAndLocalsPreamble(...)91.66%3636100%
FormatTypeName(...)75%242078.94%
CompileAndGetDiagnostics(...)100%1140%
ThrowIfDiagnosticsNull(...)50%2266.66%
ThrowOnErrors(...)62.5%161693.33%
LogWarnings(...)50%401033.33%
LogSuccessIfNoWarnings(...)87.5%88100%

File(s)

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

#LineLine coverage
 1using System.Collections.Immutable;
 2using System.Reflection;
 3using System.Text;
 4using Kestrun.SharedState;
 5using Microsoft.CodeAnalysis;
 6using Microsoft.CodeAnalysis.CSharp;
 7using Microsoft.CodeAnalysis.CSharp.Scripting;
 8using Microsoft.CodeAnalysis.Scripting;
 9using Serilog.Events;
 10using Kestrun.Logging;
 11
 12namespace Kestrun.Languages;
 13
 14
 15internal static class CSharpDelegateBuilder
 16{
 17    /// <summary>
 18    /// Builds a C# delegate for handling HTTP requests.
 19    /// </summary>
 20    /// <param name="code">The C# code to execute.</param>
 21    /// <param name="log">The logger instance.</param>
 22    /// <param name="args">Arguments to inject as variables into the script.</param>
 23    /// <param name="extraImports">Additional namespaces to import.</param>
 24    /// <param name="extraRefs">Additional assemblies to reference.</param>
 25    /// <param name="languageVersion">The C# language version to use.</param>
 26    /// <returns>A delegate that handles HTTP requests.</returns>
 27    /// <exception cref="ArgumentNullException">Thrown if the code is null or whitespace.</exception>
 28    /// <exception cref="CompilationErrorException">Thrown if the C# code compilation fails.</exception>
 29    /// <remarks>
 30    /// This method compiles the provided C# code into a script and returns a delegate that can be used to handle HTTP r
 31    /// It supports additional imports and references, and can inject global variables into the script.
 32    /// The delegate will execute the provided C# code within the context of an HTTP request, allowing access to the req
 33    /// </remarks>
 34    internal static RequestDelegate Build(
 35            string code, Serilog.ILogger log, Dictionary<string, object?>? args, string[]? extraImports,
 36            Assembly[]? extraRefs, LanguageVersion languageVersion = LanguageVersion.CSharp12)
 37    {
 1338        if (log.IsEnabled(LogEventLevel.Debug))
 39        {
 1340            log.Debug("Building C# delegate, script length={Length}, imports={ImportsCount}, refs={RefsCount}, lang={Lan
 1341                code?.Length, extraImports?.Length ?? 0, extraRefs?.Length ?? 0, languageVersion);
 42        }
 43
 44        // Validate inputs
 1345        if (string.IsNullOrWhiteSpace(code))
 46        {
 147            throw new ArgumentNullException(nameof(code), "C# code cannot be null or whitespace.");
 48        }
 49        // 1. Compile the C# code into a script
 50        //    - Use CSharpScript.Create() to create a script with the provided code
 51        //    - Use ScriptOptions to specify imports, references, and language version
 52        //    - Inject the provided arguments into the globals
 1253        var script = Compile(code, log, extraImports, extraRefs, null, languageVersion);
 54
 55        // 2. Return a delegate that executes the script
 56        //    - The delegate takes an HttpContext and returns a Task
 57        //    - It creates a KestrunContext and KestrunResponse from the HttpContext
 58        //    - It executes the script with the provided globals and locals
 59        //    - It applies the response to the HttpContext
 1260        if (log.IsEnabled(LogEventLevel.Debug))
 61        {
 1262            log.Debug("C# delegate built successfully, script length={Length}, imports={ImportsCount}, refs={RefsCount},
 1263                code?.Length, extraImports?.Length ?? 0, extraRefs?.Length ?? 0, languageVersion);
 64        }
 65
 1266        return async ctx =>
 1267        {
 1268            try
 1269            {
 270                if (log.IsEnabled(LogEventLevel.Debug))
 1271                {
 272                    log.DebugSanitized("Preparing execution for C# script at {Path}", ctx.Request.Path);
 1273                }
 1274
 275                var (Globals, Response, Context) = await DelegateBuilder.PrepareExecutionAsync(ctx, log, args).Configure
 1276
 1277                // Execute the script with the current context and shared state
 278                if (log.IsEnabled(LogEventLevel.Debug))
 1279                {
 280                    log.DebugSanitized("Executing C# script for {Path}", ctx.Request.Path);
 1281                }
 1282
 283                _ = await script.RunAsync(Globals).ConfigureAwait(false);
 284                if (log.IsEnabled(LogEventLevel.Debug))
 1285                {
 286                    log.DebugSanitized("C# script executed successfully for {Path}", ctx.Request.Path);
 1287                }
 1288
 1289                // Apply the response to the Kestrun context
 290                await DelegateBuilder.ApplyResponseAsync(ctx, Response, log).ConfigureAwait(false);
 291            }
 1292            finally
 1293            {
 294                await ctx.Response.CompleteAsync().ConfigureAwait(false);
 1295            }
 1496        };
 97    }
 98
 99    /// <summary>
 100    /// Compiles the provided C# code into a script.
 101    /// This method supports additional imports and references, and can inject global variables into the script.
 102    /// It returns a compiled script that can be executed later.
 103    /// </summary>
 104    /// <param name="code">The C# code to compile.</param>
 105    /// <param name="log">The logger instance.</param>
 106    /// <param name="extraImports">Additional namespaces to import.</param>
 107    /// <param name="extraRefs">Additional assembly references.</param>
 108    /// <param name="locals">Local variables to inject into the script.</param>
 109    /// <param name="languageVersion">The C# language version to use.</param>
 110    /// <returns>A compiled script that can be executed later.</returns>
 111    /// <exception cref="ArgumentNullException">Thrown when the code is null or whitespace.</exception>
 112    /// <exception cref="CompilationErrorException">Thrown when there are compilation errors.</exception>
 113    /// <remarks>
 114    /// This method compiles the provided C# code into a script using Roslyn.
 115    /// It supports additional imports and references, and can inject global variables into the script.
 116    /// The script can be executed later with the provided globals and locals.
 117    /// It is useful for scenarios where dynamic C# code execution is required, such as in web applications or scripting
 118    /// </remarks>
 119    internal static Script<object> Compile(
 120            string? code, Serilog.ILogger log, string[]? extraImports,
 121            Assembly[]? extraRefs, IReadOnlyDictionary<string, object?>? locals, LanguageVersion languageVersion = Langu
 122            )
 123    {
 23124        if (log.IsEnabled(LogEventLevel.Debug))
 125        {
 23126            log.Debug("Compiling C# script, length={Length}, imports={ImportsCount}, refs={RefsCount}, lang={Lang}",
 23127                code?.Length, extraImports?.Length ?? 0, extraRefs?.Length ?? 0, languageVersion);
 128        }
 129
 130        // Validate inputs
 23131        if (string.IsNullOrWhiteSpace(code))
 132        {
 0133            throw new ArgumentNullException(nameof(code), "C# code cannot be null or whitespace.");
 134        }
 135
 136        // References and imports
 23137        var coreRefs = DelegateBuilder.BuildBaselineReferences();
 138        // Core references + Kestrun + extras
 139        // Note: Order matters, Kestrun must come after core to avoid conflicts
 23140        var kestrunAssembly = typeof(Hosting.KestrunHost).Assembly; // Kestrun.dll
 23141        var kestrunRef = MetadataReference.CreateFromFile(kestrunAssembly.Location);
 23142        var kestrunNamespaces = CollectKestrunNamespaces(kestrunAssembly);
 143        // Create script options
 23144        var opts = CreateScriptOptions(DelegateBuilder.PlatformImports, kestrunNamespaces, coreRefs, kestrunRef);
 23145        opts = AddExtraImports(opts, extraImports);
 23146        opts = AddExtraReferences(opts, extraRefs, log);
 147
 148        // Include currently loaded assemblies (deduplicated) to minimize missing reference issues.
 23149        opts = AddLoadedAssemblyReferences(opts, log);
 150
 151        // Globals/locals injection plus dynamic discovery of namespaces & assemblies needed
 23152        var (CodeWithPreamble, DynamicImports, DynamicReferences) = BuildGlobalsAndLocalsPreamble(code, locals, log);
 23153        code = CodeWithPreamble;
 154
 23155        if (DynamicImports.Count > 0)
 156        {
 9157            var newImports = DynamicImports.Except(opts.Imports, StringComparer.Ordinal).ToArray();
 9158            if (newImports.Length > 0)
 159            {
 0160                opts = opts.WithImports(opts.Imports.Concat(newImports));
 0161                if (log.IsEnabled(LogEventLevel.Debug))
 162                {
 0163                    log.Debug("Added {ImportCount} dynamic imports derived from globals/locals: {Imports}", newImports.L
 164                }
 165            }
 166        }
 167
 23168        if (DynamicReferences.Count > 0)
 169        {
 170            // Avoid duplicates by location
 9171            var existingRefPaths = new HashSet<string>(opts.MetadataReferences
 9172                .OfType<PortableExecutableReference>()
 2092173                .Select(r => r.FilePath ?? string.Empty)
 2101174                .Where(p => !string.IsNullOrEmpty(p)), StringComparer.OrdinalIgnoreCase);
 175
 9176            var newRefs = DynamicReferences
 9177                .Where(r => !string.IsNullOrEmpty(r.Location) && File.Exists(r.Location) && !existingRefPaths.Contains(r
 0178                .Select(r => MetadataReference.CreateFromFile(r.Location))
 9179                .ToArray();
 180
 9181            if (newRefs.Length > 0)
 182            {
 0183                opts = opts.WithReferences(opts.MetadataReferences.Concat(newRefs));
 0184                if (log.IsEnabled(LogEventLevel.Debug))
 185                {
 0186                    log.Debug("Added {RefCount} dynamic assembly reference(s) derived from globals/locals.", newRefs.Len
 187                }
 188            }
 189        }
 190
 191        // Compile
 23192        var script = CSharpScript.Create(code, opts, typeof(CsGlobals));
 23193        var diagnostics = CompileAndGetDiagnostics(script, log);
 23194        ThrowIfDiagnosticsNull(diagnostics);
 23195        ThrowOnErrors(diagnostics, log);
 22196        LogWarnings(diagnostics, log);
 22197        LogSuccessIfNoWarnings(diagnostics, log);
 198
 22199        return script;
 200    }
 201
 202    /// <summary>Collects metadata references for all non-dynamic loaded assemblies with a physical location.</summary>
 203    /// <param name="log">Logger.</param>
 204    /// <returns>Tuple of references and total count considered.</returns>
 205    private static (IEnumerable<MetadataReference> Refs, int Total) CollectLoadedAssemblyReferences(Serilog.ILogger log)
 206    {
 207        try
 208        {
 23209            var loaded = AppDomain.CurrentDomain.GetAssemblies();
 23210            var refs = new List<MetadataReference>(loaded.Length);
 23211            var considered = 0;
 12262212            foreach (var a in loaded)
 213            {
 6108214                considered++;
 6108215                if (a.IsDynamic)
 216                {
 217                    continue;
 218                }
 6046219                if (string.IsNullOrEmpty(a.Location) || !File.Exists(a.Location))
 220                {
 221                    continue;
 222                }
 223                try
 224                {
 5443225                    refs.Add(MetadataReference.CreateFromFile(a.Location));
 5443226                }
 0227                catch (Exception ex)
 228                {
 0229                    if (log.IsEnabled(LogEventLevel.Debug))
 230                    {
 0231                        log.Debug(ex, "Failed to add loaded assembly reference: {Assembly}", a.FullName);
 232                    }
 0233                }
 234            }
 23235            return (refs, considered);
 236        }
 0237        catch (Exception ex)
 238        {
 0239            log.Warning(ex, "Failed to enumerate loaded assemblies for dynamic references.");
 0240            return (Array.Empty<MetadataReference>(), 0);
 241        }
 23242    }
 243
 244    /// <summary>
 245    /// Builds the core assembly references for the script.
 246    /// </summary>
 247    /// <returns>The core assembly references.</returns>
 248
 249    /// <summary>
 250    /// Collects the namespaces from the Kestrun assembly.
 251    /// </summary>
 252    /// <param name="kestrunAssembly">The Kestrun assembly.</param>
 253    /// <returns>The collected namespaces.</returns>
 254    private static string[] CollectKestrunNamespaces(Assembly kestrunAssembly)
 255    {
 23256        return [.. kestrunAssembly
 23257            .GetExportedTypes()
 2300258            .Select(t => t.Namespace)
 2300259            .Where(ns => !string.IsNullOrEmpty(ns) && ns!.StartsWith("Kestrun", StringComparison.Ordinal))
 2254260            .Select(ns => ns!)
 23261            .Distinct()];
 262    }
 263
 264    /// <summary>
 265    /// Creates script options for the VB.NET script.
 266    /// </summary>
 267    /// <param name="platformImports">The platform-specific namespaces to import.</param>
 268    /// <param name="kestrunNamespaces">The Kestrun-specific namespaces to import.</param>
 269    /// <param name="coreRefs">The core assembly references to include.</param>
 270    /// <param name="kestrunRef">The Kestrun assembly reference to include.</param>
 271    /// <returns>The created script options.</returns>
 272    private static ScriptOptions CreateScriptOptions(
 273        IEnumerable<string> platformImports,
 274        IEnumerable<string> kestrunNamespaces,
 275        IEnumerable<MetadataReference> coreRefs,
 276        MetadataReference kestrunRef)
 277    {
 23278        var allImports = platformImports.Concat(kestrunNamespaces) ?? [];
 279        // Keep default references then add our core + Kestrun to avoid losing essential BCL assemblies
 23280        var opts = ScriptOptions.Default
 23281            .WithImports(allImports)
 23282            .AddReferences(coreRefs)
 23283            .AddReferences(kestrunRef);
 23284        return opts;
 285    }
 286
 287    /// <summary>
 288    /// Adds extra using directives to the script options.
 289    /// </summary>
 290    /// <param name="opts">The script options to modify.</param>
 291    /// <param name="extraImports">The extra using directives to add.</param>
 292    /// <returns>The modified script options.</returns>
 293    private static ScriptOptions AddExtraImports(ScriptOptions opts, string[]? extraImports)
 294    {
 23295        extraImports ??= ["Kestrun"];
 23296        if (!extraImports.Contains("Kestrun"))
 297        {
 1298            var importsList = extraImports.ToList();
 1299            importsList.Add("Kestrun");
 1300            extraImports = [.. importsList];
 301        }
 23302        return extraImports.Length > 0
 23303            ? opts.WithImports(opts.Imports.Concat(extraImports))
 23304            : opts;
 305    }
 306
 307    /// <summary>
 308    /// Adds extra assembly references to the script options.
 309    /// </summary>
 310    /// <param name="opts">The script options to modify.</param>
 311    /// <param name="extraRefs">The extra assembly references to add.</param>
 312    /// <param name="log">The logger to use for logging.</param>
 313    /// <returns>The modified script options.</returns>
 314    private static ScriptOptions AddExtraReferences(ScriptOptions opts, Assembly[]? extraRefs, Serilog.ILogger log)
 315    {
 23316        if (extraRefs is not { Length: > 0 })
 317        {
 23318            return opts;
 319        }
 320
 0321        foreach (var r in extraRefs)
 322        {
 0323            if (string.IsNullOrEmpty(r.Location))
 324            {
 0325                log.Warning("Skipping dynamic assembly with no location: {Assembly}", r.FullName);
 326            }
 0327            else if (!File.Exists(r.Location))
 328            {
 0329                log.Warning("Skipping missing assembly file: {Location}", r.Location);
 330            }
 331        }
 332
 0333        var safeRefs = extraRefs
 0334            .Where(r => !string.IsNullOrEmpty(r.Location) && File.Exists(r.Location))
 0335            .Select(r => MetadataReference.CreateFromFile(r.Location));
 336
 0337        return opts.WithReferences(opts.MetadataReferences.Concat(safeRefs));
 338    }
 339
 340    /// <summary>
 341    /// Adds references for all currently loaded (non-duplicate) assemblies to the script options.
 342    /// </summary>
 343    /// <param name="opts">Current script options.</param>
 344    /// <param name="log">Logger.</param>
 345    /// <returns>Updated script options.</returns>
 346    private static ScriptOptions AddLoadedAssemblyReferences(ScriptOptions opts, Serilog.ILogger log)
 347    {
 348        // Optionally include all currently loaded assemblies to reduce missing reference issues.
 349        // Roslyn will de-duplicate by file path internally but we still filter to avoid redundant work.
 23350        var (loadedRefs, loadedCount) = CollectLoadedAssemblyReferences(log);
 23351        if (loadedCount <= 0)
 352        {
 0353            return opts;
 354        }
 355
 23356        var existingPaths = new HashSet<string>(opts.MetadataReferences
 23357            .OfType<PortableExecutableReference>()
 3952358            .Select(r => r.FilePath ?? string.Empty)
 3975359            .Where(p => !string.IsNullOrEmpty(p)), StringComparer.OrdinalIgnoreCase);
 360
 23361        var newLoadedRefs = loadedRefs
 5443362            .Where(r => r is PortableExecutableReference pe && !string.IsNullOrEmpty(pe.FilePath) && !existingPaths.Cont
 23363            .ToArray();
 364
 23365        if (newLoadedRefs.Length == 0)
 366        {
 0367            return opts;
 368        }
 369
 23370        var updated = opts.WithReferences(opts.MetadataReferences.Concat(newLoadedRefs));
 23371        if (log.IsEnabled(LogEventLevel.Debug))
 372        {
 23373            log.Debug("Added {RefCount} loaded assembly reference(s) (of {TotalLoaded}) for dynamic script compilation."
 374        }
 23375        return updated;
 376    }
 377
 378    /// <summary>
 379    /// Prepends global and local variable declarations to the provided code.
 380    /// </summary>
 381    /// <param name="code">The original code to modify.</param>
 382    /// <param name="locals">The local variables to include.</param>
 383    /// <returns>The modified code with global and local variable declarations.</returns>
 384    /// <summary>Builds the preamble variable declarations for globals &amp; locals and discovers required namespaces an
 385    /// <param name="log">Logger instance.</param>
 386    /// <returns>Tuple containing code with preamble, dynamic imports, dynamic references.</returns>
 387    private static (string CodeWithPreamble, List<string> DynamicImports, List<Assembly> DynamicReferences) BuildGlobals
 388        string? code,
 389        IReadOnlyDictionary<string, object?>? locals,
 390        Serilog.ILogger log)
 391    {
 23392        var preambleBuilder = new StringBuilder();
 23393        var allGlobals = SharedStateStore.Snapshot();
 23394        var merged = new Dictionary<string, (string Dict, object? Value)>(StringComparer.OrdinalIgnoreCase);
 226395        foreach (var g in allGlobals)
 396        {
 90397            merged[g.Key] = ("Globals", g.Value);
 398        }
 23399        if (locals is { Count: > 0 })
 400        {
 28401            foreach (var l in locals)
 402            {
 8403                merged[l.Key] = ("Locals", l.Value);
 404            }
 405        }
 406
 23407        var dynamicImports = new HashSet<string>(StringComparer.Ordinal);
 23408        var dynamicRefs = new HashSet<Assembly>();
 409
 242410        foreach (var kvp in merged)
 411        {
 98412            var valueType = kvp.Value.Value?.GetType();
 98413            var typeName = FormatTypeName(valueType);
 98414            _ = preambleBuilder.AppendLine($"var {kvp.Key} = ({typeName}){kvp.Value.Dict}[\"{kvp.Key}\"]; ");
 415
 98416            if (valueType != null)
 417            {
 13418                if (!string.IsNullOrEmpty(valueType.Namespace))
 419                {
 13420                    _ = dynamicImports.Add(valueType.Namespace!); // capture added namespace
 421                }
 422                // Include generic argument namespaces as well
 13423                if (valueType.IsGenericType)
 424                {
 24425                    foreach (var ga in valueType.GetGenericArguments())
 426                    {
 8427                        if (!string.IsNullOrEmpty(ga.Namespace))
 428                        {
 8429                            _ = dynamicImports.Add(ga.Namespace!); // capture generic arg namespace
 430                        }
 8431                        _ = dynamicRefs.Add(ga.Assembly); // capture generic arg assembly
 432                    }
 433                }
 13434                _ = dynamicRefs.Add(valueType.Assembly); // capture value type assembly
 435            }
 436        }
 437
 23438        if (log.IsEnabled(LogEventLevel.Debug) && (dynamicImports.Count > 0 || dynamicRefs.Count > 0))
 439        {
 9440            log.Debug("Discovered {ImportCount} dynamic import(s) and {RefCount} reference(s) from globals/locals.", dyn
 441        }
 442
 23443        return (
 23444            preambleBuilder.Length > 0 ? preambleBuilder + (code ?? string.Empty) : code ?? string.Empty,
 23445            dynamicImports.ToList(),
 9446            dynamicRefs.Where(r => !string.IsNullOrEmpty(r.Location)).ToList()
 23447        );
 448    }
 449
 450    // Produces a C# friendly type name for reflection types (handles generics, arrays, nullable, and fallbacks).
 451    private static string FormatTypeName(Type? t)
 452    {
 107453        if (t == null)
 454        {
 85455            return "object";
 456        }
 22457        if (t.IsGenericParameter)
 458        {
 0459            return "object";
 460        }
 22461        if (t.IsArray)
 462        {
 1463            return FormatTypeName(t.GetElementType()) + "[]";
 464        }
 465        // Nullable<T>
 21466        if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>))
 467        {
 0468            return FormatTypeName(t.GetGenericArguments()[0]) + "?";
 469        }
 21470        if (t.IsGenericType)
 471        {
 472            try
 473            {
 4474                var genericDefName = t.Name;
 4475                var tickIndex = genericDefName.IndexOf('`');
 4476                if (tickIndex > 0)
 477                {
 4478                    genericDefName = genericDefName[..tickIndex];
 479                }
 4480                var args = t.GetGenericArguments().Select(FormatTypeName);
 4481                return (t.Namespace != null ? t.Namespace + "." : string.Empty) + genericDefName + "<" + string.Join(","
 482            }
 0483            catch
 484            {
 0485                return "object";
 486            }
 487        }
 488        // Non generic
 17489        return t.FullName ?? t.Name ?? "object";
 4490    }
 491
 492    /// <summary>
 493    /// Compiles the provided VB.NET script and returns any diagnostics.
 494    /// </summary>
 495    /// <param name="script">The VB.NET script to compile.</param>
 496    /// <param name="log">The logger to use for logging.</param>
 497    /// <returns>A collection of diagnostics produced during compilation, or null if compilation failed.</returns>
 498    private static ImmutableArray<Diagnostic>? CompileAndGetDiagnostics(Script<object> script, Serilog.ILogger log)
 499    {
 500        try
 501        {
 23502            return script.Compile();
 503        }
 0504        catch (CompilationErrorException ex)
 505        {
 0506            log.Error(ex, "C# script compilation failed with errors.");
 0507            return null;
 508        }
 23509    }
 510
 511    private static void ThrowIfDiagnosticsNull(ImmutableArray<Diagnostic>? diagnostics)
 512    {
 23513        if (diagnostics == null)
 514        {
 0515            throw new CompilationErrorException("C# script compilation failed with no diagnostics.", []);
 516        }
 23517    }
 518
 519    /// <summary>
 520    /// Throws a CompilationErrorException if the diagnostics are null.
 521    /// </summary>
 522    /// <param name="diagnostics">The compilation diagnostics.</param>
 523    /// <param name="log">The logger to use for logging.</param>
 524    /// <exception cref="CompilationErrorException"></exception>
 525    private static void ThrowOnErrors(ImmutableArray<Diagnostic>? diagnostics, Serilog.ILogger log)
 526    {
 24527        if (diagnostics?.Any(d => d.Severity == DiagnosticSeverity.Error) != true)
 528        {
 22529            return;
 530        }
 531
 2532        var errors = diagnostics?.Where(d => d.Severity == DiagnosticSeverity.Error).ToArray();
 1533        if (errors is not { Length: > 0 })
 534        {
 0535            return;
 536        }
 537
 1538        var sb = new StringBuilder();
 1539        _ = sb.AppendLine($"C# script compilation completed with {errors.Length} error(s):");
 4540        foreach (var error in errors)
 541        {
 1542            var location = error.Location.IsInSource
 1543                ? $" at line {error.Location.GetLineSpan().StartLinePosition.Line + 1}"
 1544                : string.Empty;
 1545            var msg = $"  Error [{error.Id}]: {error.GetMessage()}{location}";
 1546            log.Error(msg);
 1547            _ = sb.AppendLine(msg);
 548        }
 1549        throw new CompilationErrorException("C# route code compilation failed\n" + sb.ToString(), diagnostics ?? []);
 550    }
 551
 552    /// <summary>
 553    /// Logs warning messages if the compilation succeeded with warnings.
 554    /// </summary>
 555    /// <param name="diagnostics">The compilation diagnostics.</param>
 556    /// <param name="log">The logger to use for logging.</param>
 557    private static void LogWarnings(ImmutableArray<Diagnostic>? diagnostics, Serilog.ILogger log)
 558    {
 22559        var warnings = diagnostics?.Where(d => d.Severity == DiagnosticSeverity.Warning).ToArray();
 22560        if (warnings is not null && warnings.Length != 0)
 561        {
 0562            log.Warning($"C# script compilation completed with {warnings.Length} warning(s):");
 0563            foreach (var warning in warnings)
 564            {
 0565                var location = warning.Location.IsInSource
 0566                    ? $" at line {warning.Location.GetLineSpan().StartLinePosition.Line + 1}"
 0567                    : string.Empty;
 0568                log.Warning($"  Warning [{warning.Id}]: {warning.GetMessage()}{location}");
 569            }
 570        }
 22571    }
 572
 573    /// <summary>
 574    /// Logs a success message if the compilation succeeded without warnings.
 575    /// </summary>
 576    /// <param name="diagnostics">The compilation diagnostics.</param>
 577    /// <param name="log">The logger to use for logging.</param>
 578    private static void LogSuccessIfNoWarnings(ImmutableArray<Diagnostic>? diagnostics, Serilog.ILogger log)
 579    {
 22580        var warnings = diagnostics?.Where(d => d.Severity == DiagnosticSeverity.Warning).ToArray();
 22581        if (warnings != null && warnings.Length == 0 && log.IsEnabled(LogEventLevel.Debug))
 582        {
 22583            log.Debug("C# script compiled successfully with no warnings.");
 584        }
 22585    }
 586}

Methods/Properties

Build(System.String,Serilog.ILogger,System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.String[],System.Reflection.Assembly[],Microsoft.CodeAnalysis.CSharp.LanguageVersion)
Compile(System.String,Serilog.ILogger,System.String[],System.Reflection.Assembly[],System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Object>,Microsoft.CodeAnalysis.CSharp.LanguageVersion)
CollectLoadedAssemblyReferences(Serilog.ILogger)
CollectKestrunNamespaces(System.Reflection.Assembly)
CreateScriptOptions(System.Collections.Generic.IEnumerable`1<System.String>,System.Collections.Generic.IEnumerable`1<System.String>,System.Collections.Generic.IEnumerable`1<Microsoft.CodeAnalysis.MetadataReference>,Microsoft.CodeAnalysis.MetadataReference)
AddExtraImports(Microsoft.CodeAnalysis.Scripting.ScriptOptions,System.String[])
AddExtraReferences(Microsoft.CodeAnalysis.Scripting.ScriptOptions,System.Reflection.Assembly[],Serilog.ILogger)
AddLoadedAssemblyReferences(Microsoft.CodeAnalysis.Scripting.ScriptOptions,Serilog.ILogger)
BuildGlobalsAndLocalsPreamble(System.String,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Object>,Serilog.ILogger)
FormatTypeName(System.Type)
CompileAndGetDiagnostics(Microsoft.CodeAnalysis.Scripting.Script`1<System.Object>,Serilog.ILogger)
ThrowIfDiagnosticsNull(System.Nullable`1<System.Collections.Immutable.ImmutableArray`1<Microsoft.CodeAnalysis.Diagnostic>>)
ThrowOnErrors(System.Nullable`1<System.Collections.Immutable.ImmutableArray`1<Microsoft.CodeAnalysis.Diagnostic>>,Serilog.ILogger)
LogWarnings(System.Nullable`1<System.Collections.Immutable.ImmutableArray`1<Microsoft.CodeAnalysis.Diagnostic>>,Serilog.ILogger)
LogSuccessIfNoWarnings(System.Nullable`1<System.Collections.Immutable.ImmutableArray`1<Microsoft.CodeAnalysis.Diagnostic>>,Serilog.ILogger)