Class Sealed
public sealed class ReplExecutionService

Namespace: Moka.Docs.Serve

Executes C# code snippets using Roslyn scripting. Captures console output, handles compilation errors, and enforces a timeout to prevent runaway code.

Constructors

NameDescription
ReplExecutionService(ReplExecutionService> logger) Creates a new REPL execution service.

ReplExecutionService(ReplExecutionService> logger)

ReplExecutionService.ReplExecutionService(ILogger<ReplExecutionService> logger)

Creates a new REPL execution service.

Parameters

NameTypeDescription
loggerILogger<Moka.Docs.Serve.ReplExecutionService>Logger instance.

Methods

NameDescription
ExecuteAsync(string code, CancellationToken ct) Executes the given C# code and returns the result.
LoadPackagesAsync(IReadOnlyList<string> packageSpecs, CancellationToken ct) Resolves the given NuGet package specifications and adds their assemblies and root namespaces to the REPL script options. Each spec is either "PackageName" or "PackageName@Version".
LoadProjectAssembly(string dllPath) Loads a project output assembly into the REPL so users can reference the documented project's types directly.

ExecuteAsync(string code, CancellationToken ct)

Task<ReplResult> ReplExecutionService.ExecuteAsync(string code, CancellationToken ct = null)

Executes the given C# code and returns the result.

Parameters

NameTypeDescription
codestringThe C# source code to execute.
ctCancellationTokenCancellation token.

Returns: The execution result containing output and/or error messages.

LoadPackagesAsync(IReadOnlyList packageSpecs, CancellationToken ct)

Task ReplExecutionService.LoadPackagesAsync(IReadOnlyList<string> packageSpecs, CancellationToken ct = null)

Resolves the given NuGet package specifications and adds their assemblies and root namespaces to the REPL script options. Each spec is either "PackageName" or "PackageName@Version".

LoadProjectAssembly(string dllPath)

void ReplExecutionService.LoadProjectAssembly(string dllPath)

Loads a project output assembly into the REPL so users can reference the documented project's types directly.

View Source
/// <summary>
///     Executes C# code snippets using Roslyn scripting. Captures console output,
///     handles compilation errors, and enforces a timeout to prevent runaway code.
/// </summary>
public sealed class ReplExecutionService
{
    private readonly ILogger<ReplExecutionService> _logger;
    private readonly TimeSpan _timeout = TimeSpan.FromSeconds(5);
    /// <summary>
    ///     Base script options with safe namespace imports and assembly references.
    ///     Additional packages and project assemblies are appended via <see cref = "LoadPackagesAsync"/>.
    /// </summary>
    private ScriptOptions _scriptOptions = ScriptOptions.Default.WithImports("System", "System.Linq", "System.Collections.Generic", "System.Text", "System.Math", "System.Text.RegularExpressions").WithReferences(typeof(object).Assembly, // System.Runtime
 typeof(Console).Assembly, // System.Console
 typeof(Enumerable).Assembly, // System.Linq
 typeof(Regex).Assembly, // System.Text.RegularExpressions
 Assembly.Load("System.Collections"), // System.Collections
 Assembly.Load("System.Runtime")); // System.Runtime
    /// <summary>
    ///     Creates a new REPL execution service.
    /// </summary>
    /// <param name = "logger">Logger instance.</param>
    public ReplExecutionService(ILogger<ReplExecutionService> logger)
    {
        _logger = logger;
    }

    /// <summary>
    ///     Resolves the given NuGet package specifications and adds their assemblies
    ///     and root namespaces to the REPL script options. Each spec is either
    ///     "PackageName" or "PackageName@Version".
    /// </summary>
    public async Task LoadPackagesAsync(IReadOnlyList<string> packageSpecs, CancellationToken ct = default)
    {
        if (packageSpecs.Count == 0)
        {
            return;
        }

        var resolver = new NuGetPackageResolver(_logger);
        NuGetPackageResolver.ResolvedPackages resolved = await resolver.ResolveAsync(packageSpecs, ct);
        AddResolvedPackages(resolved);
    }

    /// <summary>
    ///     Loads a project output assembly into the REPL so users can reference
    ///     the documented project's types directly.
    /// </summary>
    public void LoadProjectAssembly(string dllPath)
    {
        var resolver = new NuGetPackageResolver(_logger);
        NuGetPackageResolver.ResolvedPackages resolved = resolver.LoadAssemblyFromPath(dllPath);
        AddResolvedPackages(resolved);
    }

    private void AddResolvedPackages(NuGetPackageResolver.ResolvedPackages resolved)
    {
        if (resolved.Assemblies.Count > 0)
        {
            _scriptOptions = _scriptOptions.AddReferences(resolved.Assemblies);
        }

        if (resolved.Namespaces.Count > 0)
        {
            _scriptOptions = _scriptOptions.AddImports(resolved.Namespaces);
        }
    }

    /// <summary>
    ///     Executes the given C# code and returns the result.
    /// </summary>
    /// <param name = "code">The C# source code to execute.</param>
    /// <param name = "ct">Cancellation token.</param>
    /// <returns>The execution result containing output and/or error messages.</returns>
    public async Task<ReplResult> ExecuteAsync(string code, CancellationToken ct = default)
    {
        if (string.IsNullOrWhiteSpace(code))
        {
            return new ReplResult
            {
                Output = "",
                Error = "No code provided."
            };
        }

        // Enforce maximum code length to prevent abuse
        if (code.Length > 10_000)
        {
            return new ReplResult
            {
                Error = "Code exceeds maximum length of 10,000 characters."
            };
        }

        _logger.LogDebug("REPL: Executing {Length} characters of code", code.Length);
        // Capture Console.Out by redirecting to a StringWriter
        TextWriter originalOut = Console.Out;
        TextWriter originalError = Console.Error;
        using var outputWriter = new StringWriter();
        using var errorWriter = new StringWriter();
        try
        {
            Console.SetOut(outputWriter);
            Console.SetError(errorWriter);
            using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
            timeoutCts.CancelAfter(_timeout);
            Script<object>? script = CSharpScript.Create(code, _scriptOptions);
            ImmutableArray<Diagnostic> diagnostics = script.Compile(timeoutCts.Token);
            // Check for compilation errors
            var errors = diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToList();
            if (errors.Count > 0)
            {
                string errorMessages = string.Join("\n", errors.Select(e => e.GetMessage()));
                _logger.LogDebug("REPL: Compilation failed with {Count} error(s)", errors.Count);
                return new ReplResult
                {
                    Error = errorMessages
                };
            }

            // Run the script
            ScriptState<object>? result = await script.RunAsync(cancellationToken: timeoutCts.Token);
            string output = outputWriter.ToString();
            string errorOutput = errorWriter.ToString();
            // If the script returned a value and nothing was written to Console, show the return value
            if (result.ReturnValue is not null && string.IsNullOrEmpty(output))
            {
                output = result.ReturnValue.ToString() ?? "";
            }

            if (!string.IsNullOrEmpty(errorOutput))
            {
                output = string.IsNullOrEmpty(output) ? errorOutput : output + "\n" + errorOutput;
            }

            _logger.LogDebug("REPL: Execution completed successfully");
            return new ReplResult
            {
                Output = output
            };
        }
        catch (CompilationErrorException ex)
        {
            _logger.LogDebug("REPL: Compilation error — {Message}", ex.Message);
            return new ReplResult
            {
                Error = ex.Message
            };
        }
        catch (OperationCanceledException)when (!ct.IsCancellationRequested)
        {
            _logger.LogWarning("REPL: Execution timed out after {Timeout}s", _timeout.TotalSeconds);
            return new ReplResult
            {
                Error = $"Execution timed out after {_timeout.TotalSeconds} seconds."};
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "REPL: Runtime error");
            return new ReplResult
            {
                Error = $"Runtime error: {ex.Message}"};
        }
        finally
        {
            Console.SetOut(originalOut);
            Console.SetError(originalError);
        }
    }
}
Was this page helpful?