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:
- Aspire Integration — The resource creates a slim web server on startup
- Monaco Detection — File extensions are validated and mapped to Monaco language IDs
- 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:
- Retrieves the cached
FileToDisplayobject - Calls
DisplayFileTemplate(fileToDisplay).RenderAsync() - The template embeds the file contents, language ID, and highlighted line numbers
- Monaco Editor initializes client-side with the file content and highlights
- 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