<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Borys Generalov</title>
    <description>The latest articles on DEV Community by Borys Generalov (@bgener).</description>
    <link>https://dev.to/bgener</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1369409%2Fd1f346cb-9663-41ed-bf97-ca75282e5d52.png</url>
      <title>DEV Community: Borys Generalov</title>
      <link>https://dev.to/bgener</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bgener"/>
    <language>en</language>
    <item>
      <title>Build an AI-Powered Developer Portal with Backstage and .NET</title>
      <dc:creator>Borys Generalov</dc:creator>
      <pubDate>Fri, 01 May 2026 21:17:40 +0000</pubDate>
      <link>https://dev.to/bgener/build-an-ai-powered-developer-portal-with-backstage-and-net-2728</link>
      <guid>https://dev.to/bgener/build-an-ai-powered-developer-portal-with-backstage-and-net-2728</guid>
      <description>&lt;h2&gt;
  
  
  Build an AI-Powered Developer Portal with Backstage and .NET
&lt;/h2&gt;

&lt;p&gt;Want to apply AI, not just read about it? Most tutorials stop at a "Hello World" chatbot. We are going to build something that actually solves a common engineering headache: stale documentation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who this is for
&lt;/h3&gt;

&lt;p&gt;This guide is for platform engineers and .NET developers who need to organize a growing software landscape without forcing teams to manually write YAML files.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you will build
&lt;/h3&gt;

&lt;p&gt;You will build a &lt;strong&gt;dynamic developer portal&lt;/strong&gt; using &lt;a href="https://backstage.io/" rel="noopener noreferrer"&gt;Backstage&lt;/a&gt; that automatically populates its service catalog. We will use a .NET CLI tool to scan source code and use local AI (&lt;a href="https://ollama.com/" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;) to generate summaries.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source repo:&lt;/strong&gt; &lt;a href="https://github.com/bgener/demo-backstage-catalog-generator" rel="noopener noreferrer"&gt;demo-backstage-catalog-generator&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constraint:&lt;/strong&gt; We use local inference only. No source code ever leaves your machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Have you ever needed to update a service, but forgot what it does? Or spent time trying to understand code you have not touched in months? We usually solve this with a README.md that nobody updates, or a wiki nobody keeps current.&lt;/p&gt;

&lt;p&gt;An Internal Developer Portal (IDP) solves this by making the software landscape visible, but only if the data is fresh. Automation is the only way to avoid the "stale metadata" trap.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
    &lt;strong&gt;Want to skip ahead?&lt;/strong&gt; Check out the &lt;a href="https://github.com/bgener/demo-backstage-catalog-generator" rel="noopener noreferrer"&gt;complete working demo on&lt;br&gt;
    GitHub&lt;/a&gt; with all&lt;br&gt;
    the code ready to run.&lt;br&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;Before we start, make sure you have the following installed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dotnet.microsoft.com/en-us/download" rel="noopener noreferrer"&gt;.NET SDK 8+&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nodejs.org/" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt; (includes &lt;code&gt;npx&lt;/code&gt; and &lt;code&gt;yarn&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ollama.com/" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; with the &lt;code&gt;llama3:8b&lt;/code&gt; model pulled&lt;/li&gt;
&lt;li&gt;A GitHub account (for hosting and deployment)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why Does an Internal Developer Portal Matter?
&lt;/h3&gt;

&lt;p&gt;The term "Internal Developer Portal" (IDP) can be a little misleading, since it sounds like a tool exclusively for developers only. In reality, it functions as an &lt;strong&gt;internal organizational portal focused entirely on your software portfolio&lt;/strong&gt;. Unlike a general-purpose SharePoint site where everything is dumped in one place and nothing is easy to find, an IDP is deliberately narrow in scope. It covers your software landscape and nothing else, which is exactly what makes it powerful.&lt;/p&gt;

&lt;p&gt;An IDP becomes the &lt;strong&gt;single source of truth&lt;/strong&gt; for your engineering organization. It answers critical questions across every role:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Engineers:&lt;/strong&gt; Which services exist, who owns them, what they do, and where the APIs are.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team leads and architects:&lt;/strong&gt; Team composition, squad ownership, and architectural decisions with their rationale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New joiners:&lt;/strong&gt; How to get up to speed on a codebase they have never seen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform and operations teams:&lt;/strong&gt; What is running in production, who is responsible, and the lifecycle status.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Beyond just listing services, a mature IDP centralizes &lt;strong&gt;Architecture Decision Records (ADRs)&lt;/strong&gt;. I'd argue those are more valuable than the service catalog itself, because they capture &lt;em&gt;why&lt;/em&gt; a decision was made. Without a central place to surface them, they end up in forgotten wiki pages or git repositories nobody checks.&lt;/p&gt;

&lt;p&gt;The challenge is keeping all of this populated and accurate. If you rely on engineers to manually maintain metadata YAML files, the data grows stale within weeks. Automation is the only sustainable path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Formulating the Architecture
&lt;/h3&gt;

&lt;p&gt;Here's the plan to extract metadata from source code and present it visually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backstage:&lt;/strong&gt; the UI layer where engineers browse and discover services, APIs, documentation, and team ownership&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.NET Core:&lt;/strong&gt; a CLI tool that scans project folders, extracts metadata, and generates Backstage-compatible YAML&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ollama:&lt;/strong&gt; runs AI inference locally. No source code leaves the machine, no API costs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static hosting:&lt;/strong&gt; deploy to Netlify, Azure Static Web Apps, or any provider of your choice&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;br&gt;
    &lt;em&gt;Why Ollama?&lt;/em&gt; Because it runs locally and you do not want to expose your code&lt;br&gt;
    to the AI agents over the public internet. You do not know what and how they&lt;br&gt;
    use it for, and it does not feel safe. If your employer finds out, you're&lt;br&gt;
    done.&lt;br&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Up the Project Infrastructure
&lt;/h3&gt;

&lt;p&gt;You can either follow along and build everything from scratch, or clone the &lt;a href="https://github.com/bgener/demo-backstage-catalog-generator" rel="noopener noreferrer"&gt;demo repository&lt;/a&gt; to get started immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To clone the demo:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/bgener/demo-backstage-catalog-generator.git
&lt;span class="nb"&gt;cd &lt;/span&gt;demo-backstage-catalog-generator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;To build from scratch&lt;/strong&gt;, start by downloading and running the LLM model we will use in this guide:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull llama3:8b
ollama serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;br&gt;
    We use &lt;code&gt;llama3:8b&lt;/code&gt; specifically. It is significantly faster for local&lt;br&gt;
    inference than the full-size model and produces more consistent, concise&lt;br&gt;
    output for our use case. If you have a powerful GPU, feel free to use &lt;code&gt;llama3&lt;/code&gt;&lt;br&gt;
    instead.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;Next, scaffold the baseline .NET services. We’ll create one Web API and one MVC project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;Backstage-Dev-Portal
&lt;span class="nb"&gt;cd &lt;/span&gt;Backstage-Dev-Portal

dotnet new webapi &lt;span class="nt"&gt;-n&lt;/span&gt; ServiceA
dotnet new mvc &lt;span class="nt"&gt;-n&lt;/span&gt; ServiceB

dotnet new sln &lt;span class="nt"&gt;-n&lt;/span&gt; Backstage-Dev-Portal
dotnet sln add ServiceA/ServiceA.csproj
dotnet sln add ServiceB/ServiceB.csproj
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can replace the default controllers with real logic later. These raw services represent the uncataloged microservices in your organization.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building a Smart Catalog Generator in .NET
&lt;/h3&gt;

&lt;p&gt;We will build a .NET CLI tool using &lt;a href="https://github.com/awaescher/OllamaSharp" rel="noopener noreferrer"&gt;OllamaSharp&lt;/a&gt;. It scans each project, sends relevant files to the local AI model, and generates a single &lt;code&gt;catalog-info.yaml&lt;/code&gt; file containing all services, ready for Backstage to consume.&lt;/p&gt;

&lt;p&gt;Create the tool and add the required package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet new console &lt;span class="nt"&gt;-n&lt;/span&gt; ProjectSummarizer
&lt;span class="nb"&gt;cd &lt;/span&gt;ProjectSummarizer
dotnet add package OllamaSharp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of sending every file to the AI, we take a smarter approach to avoid token limits and save compute time. We will send only &lt;code&gt;*.csproj&lt;/code&gt;, &lt;code&gt;Program.cs&lt;/code&gt;, and the folder structure. This is all the context the AI needs to understand the project structure and purpose.&lt;/p&gt;

&lt;p&gt;Replace &lt;code&gt;Program.cs&lt;/code&gt; with the implementation below. The full version is in the &lt;a href="https://github.com/bgener/demo-backstage-catalog-generator" rel="noopener noreferrer"&gt;demo repository&lt;/a&gt;. Here we focus on the key parts.&lt;/p&gt;

&lt;p&gt;First, set up the Ollama client and configure the system prompt. This is the most fragile part of the chain: the system prompt has to force the model into a YAML-safe format without it hallucinating markdown backticks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ollamaApiClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OllamaApiClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://localhost:11434"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;SelectedModel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"llama3:8b"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ollamaApiClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;"You are a technical documentation assistant. "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
    &lt;span class="s"&gt;"You produce concise, YAML-safe summaries of .NET projects. "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
    &lt;span class="s"&gt;"Output only plain text, no markdown, no bullet points, no quotes, no colons, no newlines."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of sending every file to the AI, we only send &lt;code&gt;*.csproj&lt;/code&gt;, &lt;code&gt;Program.cs&lt;/code&gt;, and the folder structure. This is all the context the model needs.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
    &lt;strong&gt;Prompt sanitization is critical.&lt;/strong&gt; If your &lt;code&gt;Program.cs&lt;/code&gt; contains complex&lt;br&gt;
    string literals or nested colons, the AI might pass them through to your YAML,&lt;br&gt;
    breaking the Backstage parser. Always sanitize the output before writing the&lt;br&gt;
    file.&lt;br&gt;
&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;StringBuilder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Project: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;projectName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Folder structure:"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;AppendFolderStructure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;projectDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadAllText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csprojPath&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;programPath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Directory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;projectDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Program.cs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SearchOption&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AllDirectories&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;programPath&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadAllText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;programPath&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt itself uses few-shot examples to guide the model toward the output format we want:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Summarize the project in 1-2 sentences based on the files provided. "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
             &lt;span class="s"&gt;"Do not output anything else. "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
             &lt;span class="s"&gt;"Examples of good output: "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
             &lt;span class="s"&gt;"REST API service providing weather forecasts with temperature data\n"&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
             &lt;span class="s"&gt;"ASP.NET MVC application with React frontend for managing todo items\n\n"&lt;/span&gt;
             &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;summaryBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, each summary is sanitized and assembled into a Backstage-compatible YAML entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;summaryBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\n"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;":"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;" -"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"'"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;yamlEntry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$@"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;apiVersion: backstage.io/v1alpha1&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;kind: Component&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;metadata:&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;  name: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;projectName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToLowerInvariant&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;  description: ""&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;spec:&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;  type: service&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;  lifecycle: production&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s"&gt;  owner: group:default/engineering"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the generator against the target directory (use &lt;code&gt;.&lt;/code&gt; if you are already in the project root):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet run &lt;span class="nt"&gt;--project&lt;/span&gt; ProjectSummarizer &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the AI streaming its summaries in real time:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr8w473r4i4y839n02e56.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr8w473r4i4y839n02e56.png" alt=" " width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most of the real work here is figuring out the prompt. Even a tiny change can produce a completely different output. I encourage you to experiment with the system prompt and the user prompt to see how it affects quality. That is the real learning here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Integrating the AI Catalog with Backstage
&lt;/h3&gt;

&lt;p&gt;With the &lt;code&gt;catalog-info.yaml&lt;/code&gt; ready, we can integrate it into a Backstage instance. Install Backstage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @backstage/create-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Follow the prompts to name it &lt;code&gt;dev-portal&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now point Backstage to your generated catalog file. Open &lt;code&gt;app-config.yaml&lt;/code&gt; in the &lt;code&gt;dev-portal&lt;/code&gt; directory and add the following under the &lt;code&gt;catalog&lt;/code&gt; section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;catalog&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;locations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;file&lt;/span&gt;
      &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../Backstage-Dev-Portal/catalog-info.yaml&lt;/span&gt;
      &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Component&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells Backstage where to find the AI-generated service metadata. The &lt;code&gt;target&lt;/code&gt; path is relative to the Backstage root directory. Adjust it to point to wherever your generator wrote the &lt;code&gt;catalog-info.yaml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To run it locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;dev-portal
yarn dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:3000&lt;/code&gt; in your browser. You should see all your services listed in the &lt;strong&gt;Software Catalog&lt;/strong&gt; with AI-generated summaries visible in the description column.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1gnd3uy6sjadiwto76lv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1gnd3uy6sjadiwto76lv.png" alt=" " width="800" height="506"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploying the Portal
&lt;/h3&gt;

&lt;p&gt;To host this, build your portal as a static site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn build:static
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Push the output to GitHub and deploy to any &lt;strong&gt;static hosting provider&lt;/strong&gt;: Netlify, Azure Static Web Apps, Vercel, or even self-hosted on Kubernetes. Set the build command to &lt;code&gt;yarn build:static&lt;/code&gt; and the publish directory to &lt;code&gt;dist&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
    A static Backstage build is great for read-only catalogs. If you need dynamic&lt;br&gt;
    features like &lt;strong&gt;authentication&lt;/strong&gt;, &lt;strong&gt;real-time plugin backends&lt;/strong&gt;, or &lt;strong&gt;write&lt;br&gt;
    operations&lt;/strong&gt;, you will need to deploy the full Backstage backend as a Node.js&lt;br&gt;
    service instead.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;The part that is easy to miss is that a static Backstage portal has no live catalog refresh. The catalog is baked at build time, so there is always a lag between a code change and the portal showing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating with CI/CD
&lt;/h2&gt;

&lt;p&gt;The catalog only stays current if the generator runs automatically. Here is a GitHub Actions workflow that regenerates summaries on every push to &lt;code&gt;main&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Update Backstage Catalog&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;generate-catalog&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup .NET&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-dotnet@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;dotnet-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;‘8.0.x’&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install and start Ollama&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;curl -fsSL https://ollama.com/install.sh | sh&lt;/span&gt;
          &lt;span class="s"&gt;ollama serve &amp;amp;&lt;/span&gt;
          &lt;span class="s"&gt;sleep 5&lt;/span&gt;
          &lt;span class="s"&gt;ollama pull llama3:8b&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate catalog&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dotnet run --project ProjectSummarizer -- "$GITHUB_WORKSPACE"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Commit updated catalog&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;git config user.name "github-actions"&lt;/span&gt;
          &lt;span class="s"&gt;git config user.email "github-actions@github.com"&lt;/span&gt;
          &lt;span class="s"&gt;git add catalog-info.yaml&lt;/span&gt;
          &lt;span class="s"&gt;git diff --cached --quiet || git commit -m "chore: regenerate AI catalog summaries"&lt;/span&gt;
          &lt;span class="s"&gt;git push&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;br&gt;
    &lt;strong&gt;CI Performance:&lt;/strong&gt; Running Ollama in CI uses CPU-only inference by default. A&lt;br&gt;
    &lt;code&gt;llama3:8b&lt;/code&gt; summary takes about 20-30 seconds per project on a standard GitHub&lt;br&gt;
    runner. For a large monorepo, your CI bill will spike. Consider using a&lt;br&gt;
    persistent self-hosted runner with a GPU if you scale this.&lt;br&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Final thoughts
&lt;/h3&gt;

&lt;p&gt;Automate the metadata generation where the code lives, and keep the UI (Backstage) as a thin, static client. That is the whole trick. Once those two are separate, the "stale documentation" problem mostly goes away.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use narrow context:&lt;/strong&gt; Don't send the whole repo. Files like &lt;code&gt;Program.cs&lt;/code&gt; and &lt;code&gt;*.csproj&lt;/code&gt; are usually enough context for the model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sanitize strictly:&lt;/strong&gt; AI output is non-deterministic. Always strip colons and newlines before writing to YAML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start static:&lt;/strong&gt; Read-only is much easier to maintain. Add a backend only when you actually need write operations or auth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would not use this in production unless I had tested the sanitization logic against a few real codebases first. The &lt;code&gt;Replace&lt;/code&gt; calls that strip colons and newlines are intentionally minimal. They break on YAML with complex string values, like connection strings or environment variable blocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can I use OpenAI instead of Ollama?&lt;/strong&gt;&lt;br&gt;
Yes, but you will be sending your source code (or at least your &lt;code&gt;Program.cs&lt;/code&gt;) to a third party. Use a local model if security is a concern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this replace README files?&lt;/strong&gt;&lt;br&gt;
No. It replaces the "Service Directory" that usually lives in a spreadsheet. It points engineers to the README they actually need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I handle project renames?&lt;/strong&gt;&lt;br&gt;
The generator uses the folder or &lt;code&gt;.csproj&lt;/code&gt; name. If you rename them, Backstage will see it as a new component unless you map the identity stably.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I help teams build exactly this kind of internal tooling, from developer portals to platform engineering. &lt;a href="https://blog.bgener.nl/resume" rel="noopener noreferrer"&gt;See my work&lt;/a&gt; or &lt;a href="https://blog.bgener.nl/contact" rel="noopener noreferrer"&gt;get in touch&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devex</category>
      <category>idp</category>
      <category>dotnet</category>
    </item>
  </channel>
</rss>
