AspireFileDisplayExtension–part 2–implementation

Building AspireFileDisplayExtension required solving three distinct challenges. Let me walk you through how I tackled each one, and show you the actual code that makes it work.

Challenge 1: Adding a URL Endpoint to an Aspire Resource

How do I attach a web server to an Aspire resource? The solution is surprisingly elegant: create a slim ASP.NET Core web server using WebApplication.CreateSlimBuilder().

When Aspire fires the AfterResourcesCreatedEvent, I spin up a dedicated Kestrel server. Here’s the core logic:

var builder = WebApplication.CreateSlimBuilder();
builder.WebHost.UseKestrelHttpsConfiguration();
builder.Logging.ClearProviders();
var app = builder.Build();
app.Urls.Add("http://127.0.0.1:" + FileDisplayResource.port);
app.MapGet("/", () => new HtmlResult(result));
app.MapGet("/files/{nameFile}", async ([FromRoute]string nameFile) =>
{
    var fileToDisplay = FileDisplayResource.files
        .First(it => it.NameFile() == nameFile);
    var result = await new DisplayFileTemplate(fileToDisplay)
        .RenderAsync(ct);
    return new HtmlResult(result);
});
await app.StartAsync(ct);

Challenge 2: Rendering Beautiful Code with Monaco Editor

How do I transform raw file contents into a professional, syntax-highlighted editor view? Monaco Editor is the answer—it’s the same editor powering VS Code.

I built a language detection system that maps file extensions to Monaco’s language identifiers. Here’s the mapping engine:

public static class MonacoLanguageExtensions
{
    public static string ToMonacoId(this MonacoLanguage language) =>
        MonacoLanguageIds.TryGetValue(language, out var id) ? id : "plaintext";

    public static bool TryGetFromExtension(string extension, out MonacoLanguage language)
    {
        var ext = extension.TrimStart('.').ToLowerInvariant();
        return ExtensionMap.TryGetValue(ext, out language);
    }
}

// Sample extension mappings
public static readonly Dictionary<string, MonacoLanguage> ExtensionMap = new()
{
    ["cs"] = MonacoLanguage.Csharp,
    ["js"] = MonacoLanguage.Javascript,
    ["ts"] = MonacoLanguage.Typescript,
    ["py"] = MonacoLanguage.Python,
    ["json"] = MonacoLanguage.Json,
    ["yaml"] = MonacoLanguage.Yaml,
    ["html"] = MonacoLanguage.Html,
    ["sql"] = MonacoLanguage.Sql,
    // ... supports 90+ language extensions
};

When you add a file with AddFile(), I validate the extension first:

public string AddFile(string relativePath, params string[] lines)
{
    var extension = Path.GetExtension(relativePath);
    if (!Monaco.MonacoLanguageExtensions.TryGetFromExtension(extension, out _))
    {
        throw new ArgumentOutOfRangeException(
            relativePath, 
            $"Cannot find extension {extension}");
    }
    files.Add(new FileToDisplay(relativePath, lines));
    return relativePath;
}

This ensures only supported file types make it into the viewer. Monaco handles the rest—syntax highlighting, line numbering, themes, and responsive rendering all work out of the box.

Challenge 3: Integrating File Contents into the UI with RazorBlade

How do I seamlessly merge file contents, line numbers, highlighting directives, and Monaco configuration into a cohesive HTML page? I need a templating engine that’s simple yet powerful.

I use RazorBlade to render dynamic HTML templates. The FileToDisplay class reads the file, caches its contents, and detects which lines to highlight:

record FileToDisplay
{
    public FileToDisplay(string relativePath, params string[] lines)
    {
        this.relativePath = relativePath;
        this.lines = lines;
    }
// code omitted

And the template

@inherits RazorBlade.PlainTextTemplate<AspireFileDisplayExtension.FileToDisplay>
@using System.Text.Json
@{
    var contents=await Model.Contents();
    bool hasLines=Model.indexFound.Length > 0;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>@Model.NameFile()</title>

Putting It All Together

Here’s how the three pieces work in concert:

  1. Aspire Integration — The resource creates a slim web server on startup
  2. Monaco Detection — File extensions are validated and mapped to Monaco language IDs
  3. RazorBlade Templates — File contents and metadata are rendered into a complete HTML document with highlighting directives

When a browser requests /files/AppHost.cs in the WebServer embedded in the AspireResource, the pipeline:

  1. Retrieves the cached FileToDisplay object
  2. Calls DisplayFileTemplate(fileToDisplay).RenderAsync()
  3. The template embeds the file contents, language ID, and highlighted line numbers
  4. Monaco Editor initializes client-side with the file content and highlights
  5. You see a beautiful, syntax-highlighted editor in your browser

No build step, no preprocessing—just file I/O, template rendering, and a little JavaScript magic.

aspireExtensions is open source on GitHub at https://github.com/ignatandrei/aspireExtensions

Next time, a demo


Posted

in

,

by

Tags: