Class Sealed
public sealed class BlazorPreviewService

Namespace: Moka.Docs.Serve

Renders Blazor/Razor component source into a static HTML preview using real Roslyn compilation and Blazor's HtmlRenderer for server-side rendering.

Constructors

NameDescription
BlazorPreviewService(…)

Methods

NameDescription
RenderAsync(string source, CancellationToken ct) Renders the given Blazor/Razor component source to an HTML preview using real Roslyn compilation and Blazor HtmlRenderer.

RenderAsync(string source, CancellationToken ct)

Task<BlazorPreviewResult> BlazorPreviewService.RenderAsync(string source, CancellationToken ct = null)

Renders the given Blazor/Razor component source to an HTML preview using real Roslyn compilation and Blazor HtmlRenderer.

View Source
/// <summary>
///     Renders Blazor/Razor component source into a static HTML preview using real
///     Roslyn compilation and Blazor's HtmlRenderer for server-side rendering.
/// </summary>
public sealed class BlazorPreviewService
{
    private const int _maxSourceLength = 50_000;
    private readonly ICompilationService _compilationService;
    private readonly IReadOnlyList<string> _extraUsings;
    private readonly ILogger<BlazorPreviewService> _logger;
    private readonly ILoggerFactory _loggerFactory;
    private readonly IServiceProvider _serviceProvider;
    public BlazorPreviewService(ICompilationService compilationService, ILoggerFactory loggerFactory, ILogger<BlazorPreviewService> logger, IEnumerable<string>? extraUsings = null, IEnumerable<string>? runtimeAssemblyPaths = null)
    {
        _compilationService = compilationService;
        _loggerFactory = loggerFactory;
        _logger = logger;
        _extraUsings = extraUsings?.ToList() ?? [];
        // Pre-load runtime assemblies so HtmlRenderer can resolve child component types.
        // MetadataReference.CreateFromFile only covers compilation; for rendering we need
        // the assemblies actually present in the AppDomain.
        if (runtimeAssemblyPaths is not null)
        {
            foreach (string path in runtimeAssemblyPaths)
            {
                try
                {
                    Assembly.LoadFrom(path);
                }
                catch (Exception ex)
                {
                    logger.LogWarning("Could not load runtime assembly {Path}: {Message}", path, ex.Message);
                }
            }
        }

        // Minimal service provider for HtmlRenderer — components rendered in preview
        // don't have access to app-level services (intentional isolation)
        var services = new ServiceCollection();
        services.AddLogging(b => b.AddProvider(new ForwardingLoggerProvider(loggerFactory)));
        _serviceProvider = services.BuildServiceProvider();
    }

    /// <summary>
    ///     Renders the given Blazor/Razor component source to an HTML preview
    ///     using real Roslyn compilation and Blazor HtmlRenderer.
    /// </summary>
    public async Task<BlazorPreviewResult> RenderAsync(string source, CancellationToken ct = default)
    {
        if (string.IsNullOrWhiteSpace(source))
        {
            return new BlazorPreviewResult
            {
                Error = "No source code provided."
            };
        }

        if (source.Length > _maxSourceLength)
        {
            return new BlazorPreviewResult
            {
                Error = $"Source exceeds maximum length of {_maxSourceLength:N0} characters."};
        }

        try
        {
            // 1. Build a project from the source
            var project = new ReplProject
            {
                Name = "BlazorPreview"
            };
            project.Files.Add(ProjectFile.CreateRazor("Preview.razor", source));
            // Add common usings
            project.GlobalUsings.Add("System");
            project.GlobalUsings.Add("System.Collections.Generic");
            project.GlobalUsings.Add("System.Linq");
            project.GlobalUsings.Add("Microsoft.AspNetCore.Components");
            project.GlobalUsings.Add("Microsoft.AspNetCore.Components.Web");
            // Add any extra usings provided at construction time (e.g., Moka.Red namespaces)
            foreach (string u in _extraUsings)
            {
                project.GlobalUsings.Add(u);
            }

            // 2. Compile with Roslyn
            CompilationResult result = await _compilationService.CompileAsync(project, ct);
            if (!result.Success)
            {
                IEnumerable<string> errors = result.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).Select(d => d.Message);
                return new BlazorPreviewResult
                {
                    Error = string.Join("\n", errors)
                };
            }

            if (result.AssemblyBytes is null)
            {
                return new BlazorPreviewResult
                {
                    Error = "Compilation produced no assembly."
                };
            }

            // 3. Load assembly and find the component type
            var assembly = Assembly.Load(result.AssemblyBytes);
            Type? componentType = !string.IsNullOrEmpty(result.EntryPointTypeName) ? assembly.GetType(result.EntryPointTypeName) : null;
            componentType ??= assembly.GetTypes().FirstOrDefault(t => typeof(IComponent).IsAssignableFrom(t) && !t.IsAbstract);
            if (componentType is null)
            {
                return new BlazorPreviewResult
                {
                    Error = "No renderable component found in compiled assembly."
                };
            }

            // 4. Render to HTML using Blazor's HtmlRenderer
            await using var htmlRenderer = new HtmlRenderer(_serviceProvider, _loggerFactory);
            string html = await htmlRenderer.Dispatcher.InvokeAsync(async () =>
            {
                HtmlRootComponent output = await htmlRenderer.RenderComponentAsync(componentType);
                return output.ToHtmlString();
            });
            _logger.LogDebug("Blazor preview: Compiled and rendered {Length} chars of source to {HtmlLength} chars of HTML", source.Length, html.Length);
            return new BlazorPreviewResult
            {
                Html = html
            };
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Blazor preview: Rendering error");
            return new BlazorPreviewResult
            {
                Error = $"Rendering error: {ex.Message}"};
        }
    }

    /// <summary>
    ///     Forwards log messages from the HtmlRenderer's internal service provider
    ///     to the host application's logging infrastructure.
    /// </summary>
    private sealed class ForwardingLoggerProvider(ILoggerFactory factory) : ILoggerProvider
    {
        public ILogger CreateLogger(string categoryName) => factory.CreateLogger(categoryName);
        public void Dispose()
        {
        }
    }
}
Was this page helpful?