diff --git a/.gitignore b/.gitignore
index 23fd924..91e033c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -257,3 +257,19 @@ paket-files/
*.binlog
*.lscache
+
+# Aspire inner-loop
+.azurite/
+samples/*/bin/sample/
+bin/index/
+
+
+# Aspire CLI / tooling state
+.agents/
+.playwright/
+.modules/
+
+
+# HtmlGenerator drops Errors.txt into its working directory on startup.
+src/SourceBrowser/src/HtmlGenerator/Errors.txt
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index e17e5e9..0849208 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,3 +1,13 @@
-
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/NuGet.config b/NuGet.config
index 8ec79f9..2724eaf 100644
--- a/NuGet.config
+++ b/NuGet.config
@@ -1,11 +1,20 @@
-
+
+
-
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index e64b2d7..19ed0dd 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,7 @@
This repo contains the code for building http://source.dot.net
## Documentation
+- [Local inner loop (Aspire)](docs/inner-loop.md) - **Recommended for development.** Runs the full pipeline (sample build → BinLogToSln → HtmlGenerator → blob → SourceIndexServer) locally against a tiny sample library.
- [Source Selection Algorithm](docs/source-selection-algorithm.md) - How the indexer chooses the best implementation when multiple builds exist for the same assembly
## Build Status
@@ -10,17 +11,27 @@ This repo contains the code for building http://source.dot.net
## What Is It?
This repo uses https://github.com/KirillOsenkov/SourceBrowser (with a few additions here https://github.com/dotnet/SourceBrowser/tree/source-indexer) to index the dotnet sources and produce a navigatable and searchable website containing the full source code. This includes code from the runtime, winforms, wpf, aspnetcore, and msbuild, among others. For a full list see here https://github.com/dotnet/source-indexer/blob/main/src/index/repositories.props.
-## Build Prerequsites
-The build requires .NET 8.0 and Visual Studio 2022 to build.
+## Local development (Aspire)
+The repository ships with an [Aspire](https://aspire.dev) AppHost (`src/source-indexer.AppHost/`) that emulates the full production pipeline locally against a tiny sample library — **this is the recommended way to work on the repo**.
+
+```pwsh
+aspire start
+```
+
+The dashboard URL (with login token) is printed to the console. From there you can run the `bootstrap-all` step to build the sample, run `BinLogToSln`, run `HtmlGenerator`, upload to Azurite, and start the web app. See [docs/inner-loop.md](docs/inner-loop.md) for the full resource graph and walkthroughs.
+
+You can also open the repo in **Visual Studio** or **VS Code** to set breakpoints and debug individual components (e.g., `HtmlGenerator`, `SourceIndexServer`, `BinLogToSln`) while the rest of the pipeline runs under the AppHost.
+
+## Building the production index (Windows-only)
+The official pipeline build only works on Windows because `HtmlGenerator` is a .NET Framework executable. For local dev prefer the Aspire flow above; use this path only if you need to reproduce the exact pipeline build.
+
+**Prerequisites:** .NET 8.0 and Visual Studio 2022.
-## Build
-The build will only work on windows because the source indexer executable is a .net framework executable.
1. `git clone https://github.com/dotnet/source-indexer.git`
2. For each *.sln file `dotnet restore`
3. Find VS 2022 msbuild.exe on your machine, typically found at `C:\Program Files (x86)\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\MSBuild.exe`
4. `msbuild build.proj`
-## Running the built index
After the build is finished the index will exist in `bin\index` and can be run by running `dotnet Microsoft.SourceBrowser.SourceIndexServer.dll` in that folder. The index will be served on `http://localhost:5000`
## Deployment
diff --git a/aspire.config.json b/aspire.config.json
new file mode 100644
index 0000000..38a0f70
--- /dev/null
+++ b/aspire.config.json
@@ -0,0 +1,5 @@
+{
+ "appHost": {
+ "language": "csharp"
+ }
+}
\ No newline at end of file
diff --git a/docs/images/bootstrap-menu.png b/docs/images/bootstrap-menu.png
new file mode 100644
index 0000000..e195777
Binary files /dev/null and b/docs/images/bootstrap-menu.png differ
diff --git a/docs/inner-loop.md b/docs/inner-loop.md
new file mode 100644
index 0000000..5e1b871
--- /dev/null
+++ b/docs/inner-loop.md
@@ -0,0 +1,112 @@
+# Local inner loop (Aspire)
+
+`src/source-indexer.AppHost/` is an [Aspire](https://aspire.dev) AppHost that
+emulates the production indexing pipeline end-to-end against a tiny sample
+library (`samples/MiniRuntime/`). It lets you exercise the full
+binlog → HtmlGenerator → blob → SourceIndexServer chain locally without
+pushing to ADO.
+
+## Prerequisites
+
+- **.NET SDK 10** (pinned in root `global.json`).
+- **Docker / Podman** running locally (Azurite emulators run as containers).
+- **Aspire CLI** — see .
+- **Azure CLI** (`az`) on PATH — used by the `publish-index` step.
+
+Then from the repo root:
+
+```pwsh
+aspire start
+```
+
+The dashboard URL (with login token) is printed to the console.
+
+## Resource graph
+
+Auto-started (running as soon as the AppHost is up):
+
+| Resource | Type | Purpose |
+|---|---|---|
+| `stage1Storage` | Azurite emulator | Models `netsourceindexstage1` (V2 upstream upload destination). |
+| `prodStorage` | Azurite emulator | Models `netsourceindexprod` (final HTML index destination). |
+| `stage1` | Blob container under `stage1Storage` | Where `upload-stage1` drops the `.tar.gz`. |
+| `index-local` | Blob container under `prodStorage` | Where the generated HTML index is uploaded. The web app reads from here. |
+| `web` | `SourceIndexServer` project | ASP.NET Core app. `SOURCE_BROWSER_INDEX_PROXY_URL` is wired to `index-local` so it serves the indexed HTML out of Azurite. |
+
+Explicit-start (stopped by default — click **Start** in the dashboard):
+
+| Resource | Type | What it does |
+|---|---|---|
+| `step1-sample-build` | Executable: `dotnet build /bl:` | Builds `samples/MiniRuntime` and produces `samples/MiniRuntime/bin/sample/msbuild.binlog`. |
+| `step2-upload-stage1` | Project: `UploadIndexStage1` | Tars+gzips the sample folder + binlog and uploads it to `stage1`. Real V2 upstream tool — fully dogfooded. |
+| `step3-htmlgenerator` | Project: `HtmlGenerator` | Runs `HtmlGenerator` on the binlog from `step1-sample-build` to produce static HTML under `bin/index/`. |
+| `step4-publish-index` | Executable: `az storage blob upload-batch` | Uploads `bin/index/index/` to the `index-local` container in `prodStorage`. |
+
+## One-click bootstrap
+
+The `prodStorage` resource exposes a custom **Bootstrap full pipeline**
+command (internal id `bootstrap-all`) that runs the four pipeline
+resources in order (`step1-sample-build` → `step2-upload-stage1` →
+`step3-htmlgenerator` → `step4-publish-index`) and waits for each one to
+finish. This is the easy first-run path:
+
+1. `aspire start`
+2. Open the dashboard.
+3. On the `prodStorage` row, click the **⋯** button in the Actions
+ column and choose **Bootstrap full pipeline**.
+4. Wait for it to finish (watch the logs).
+5. Open the `web` URL — you should see `MiniRuntime`'s indexed HTML.
+
+
+
+Re-running any individual resource regenerates just that stage.
+
+## How this maps to prod
+
+| Prod | Local inner loop |
+|---|---|
+| V2 upstream repo publishes `.tar.gz` to `netsourceindexstage1/stage1//.tar.gz` (`UploadIndexStage1`) | `step2-upload-stage1` resource → `stage1Storage/stage1` |
+| `HtmlGenerator.exe` reads binlog + writes HTML to `bin/index/` (`src/index/index.proj`) | `step3-htmlgenerator` resource |
+| `AzureFileCopy@6` uploads `bin/index/index/*` to `netsourceindexprod/index-/` (`azure-pipelines.yml`) | `step4-publish-index` resource (`az storage blob upload-batch`) → `prodStorage/index-local` |
+| App Service slot setting `SOURCE_BROWSER_INDEX_PROXY_URL` flipped to the new container (`deployment/deploy-storage-proxy.ps1`) | `web` resource started with `SOURCE_BROWSER_INDEX_PROXY_URL` env var pointing at `prodStorage/index-local` |
+
+See `docs/handoff/03-indexing-pipeline.md` and
+`docs/handoff/05-azure-pipeline.md` for the full prod mechanics.
+
+## Auth: Azurite vs prod
+
+Production uses managed identity (`TokenCredential`) to talk to Azure
+Storage. Azurite only speaks shared-key / connection-string auth. To keep
+both paths working without a fork, `SourceIndexServer/Models/AzureBlobFileSystem.cs`
+and `UploadIndexStage1/Program.cs` check for `AZURE_STORAGE_CONNECTION_STRING`
+in the environment:
+
+- If set → use `BlobServiceClient(connectionString)` (Azurite-friendly).
+- If not set → use the original `BlobServiceClient(uri, TokenCredential)`
+ code path (prod-friendly).
+
+Aspire injects the connection string automatically via `WithReference(...)`
+on the blob resources in the AppHost. Prod is unaffected.
+
+## Persistence
+
+The two Azurite emulators use `ContainerLifetime.Persistent` and bind-mount
+their data directories to `.azurite/stage1` and `.azurite/prod` at the repo
+root. Blobs uploaded during a session survive `aspire start` restarts and
+container recreations — to wipe state, just delete the `.azurite/` folder.
+The folder is gitignored.
+
+## Debugging individual components
+
+The AppHost runs each stage as its own resource, but the underlying projects are normal .NET projects you can debug directly in **Visual Studio** or **VS Code**:
+
+- **`HtmlGenerator`** (`src/SourceBrowser/src/HtmlGenerator/`) — set breakpoints, then either start it from the IDE pointing at an existing binlog under `samples/MiniRuntime/bin/sample/`, or attach to the `step3-htmlgenerator` process after kicking it off from the dashboard.
+- **`SourceIndexServer`** (`src/SourceBrowser/src/SourceIndexServer/`) — F5 from the IDE for fast inner-loop on the web UI. To debug against Azurite data, set `AZURE_STORAGE_CONNECTION_STRING` and `SOURCE_BROWSER_INDEX_PROXY_URL` from the running AppHost (visible on the `web` resource's env vars panel in the dashboard) in your launch profile.
+- **`UploadIndexStage1`** (`src/UploadIndexStage1/`) — same pattern: copy the env vars off the AppHost's `step2-upload-stage1` resource and run from the IDE.
+- **`BinLogToSln`** (`src/SourceBrowser/src/BinLogToSln/`) — runnable directly against any `.binlog` produced by `step1-sample-build`.
+
+The Aspire dashboard's per-resource "Environment" tab is the source of truth for the env vars Aspire injects — copy those into your `launchSettings.json` to reproduce the AppHost environment under a debugger.
+
+## Open follow-ups
+
+- **End-to-end OTel validation**: ServiceDefaults are wired into `SourceIndexServer` and `UploadIndexStage1`, but flowing traces across `HtmlGenerator` (net472) into the dashboard hasn't been validated yet.
diff --git a/samples/MiniRuntime/MiniArray.cs b/samples/MiniRuntime/MiniArray.cs
new file mode 100644
index 0000000..c5ee1c7
--- /dev/null
+++ b/samples/MiniRuntime/MiniArray.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace MiniRuntime;
+
+///
+/// A minimal heap-backed array wrapper used to demonstrate generics and
+/// indexer documentation in the indexed HTML output.
+///
+/// The element type stored in the array.
+public sealed class MiniArray : IReadOnlyList
+{
+ private readonly T[] _items;
+
+ ///
+ /// Initializes a new with the given length.
+ ///
+ /// The number of elements the array will hold.
+ ///
+ /// Thrown if is negative.
+ ///
+ public MiniArray(int length)
+ {
+ if (length < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(length));
+ }
+
+ _items = new T[length];
+ }
+
+ ///
+ /// Gets or sets the element at the given index.
+ ///
+ /// The zero-based index.
+ public T this[int index]
+ {
+ get => _items[index];
+ set => _items[index] = value;
+ }
+
+ ///
+ /// Gets the number of elements in the array.
+ ///
+ public int Count => _items.Length;
+
+ ///
+ public IEnumerator GetEnumerator()
+ {
+ for (int i = 0; i < _items.Length; i++)
+ {
+ yield return _items[i];
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ internal T[] UnsafeBuffer => _items;
+}
diff --git a/samples/MiniRuntime/MiniRuntime.csproj b/samples/MiniRuntime/MiniRuntime.csproj
new file mode 100644
index 0000000..e2760ac
--- /dev/null
+++ b/samples/MiniRuntime/MiniRuntime.csproj
@@ -0,0 +1,16 @@
+
+
+
+ net9.0
+ MiniRuntime
+ MiniRuntime
+ true
+ enable
+ enable
+ $(NoWarn);CS1591
+
+ false
+
+
+
diff --git a/samples/MiniRuntime/MiniRuntimeInfo.cs b/samples/MiniRuntime/MiniRuntimeInfo.cs
new file mode 100644
index 0000000..6404938
--- /dev/null
+++ b/samples/MiniRuntime/MiniRuntimeInfo.cs
@@ -0,0 +1,33 @@
+namespace MiniRuntime;
+
+///
+/// Provides static information about the MiniRuntime fixture library.
+///
+///
+/// This type exists so the source indexer has something with XML doc
+/// comments, public surface, and internal helpers to render.
+///
+public static class MiniRuntimeInfo
+{
+ ///
+ /// Gets the human-readable name of this runtime fixture.
+ ///
+ public static string Name => "MiniRuntime";
+
+ ///
+ /// Gets the version string for this runtime fixture.
+ ///
+ public static string Version => "0.1.0-inner-loop";
+
+ ///
+ /// Returns a banner suitable for logging at startup.
+ ///
+ /// A formatted banner string.
+ public static string GetBanner() => Banners.Format(Name, Version);
+
+ internal static class Banners
+ {
+ internal static string Format(string name, string version)
+ => $"=== {name} v{version} ===";
+ }
+}
diff --git a/samples/MiniRuntime/MiniSpan.cs b/samples/MiniRuntime/MiniSpan.cs
new file mode 100644
index 0000000..1f24d31
--- /dev/null
+++ b/samples/MiniRuntime/MiniSpan.cs
@@ -0,0 +1,61 @@
+using System;
+
+namespace MiniRuntime;
+
+///
+/// A trivial slice-like view over a .
+///
+/// The element type.
+public readonly struct MiniSpan
+{
+ private readonly MiniArray _source;
+ private readonly int _start;
+ private readonly int _length;
+
+ ///
+ /// Initializes a new over the given array slice.
+ ///
+ /// The backing array.
+ /// The inclusive start index.
+ /// The slice length.
+ public MiniSpan(MiniArray source, int start, int length)
+ {
+ ArgumentNullException.ThrowIfNull(source);
+
+ if ((uint)start > (uint)source.Count)
+ {
+ throw new ArgumentOutOfRangeException(nameof(start));
+ }
+
+ if ((uint)length > (uint)(source.Count - start))
+ {
+ throw new ArgumentOutOfRangeException(nameof(length));
+ }
+
+ _source = source;
+ _start = start;
+ _length = length;
+ }
+
+ /// Gets the length of the span.
+ public int Length => _length;
+
+ /// Gets a reference to the element at the given offset.
+ /// The zero-based offset within the span.
+ public T this[int index]
+ {
+ get
+ {
+ if ((uint)index >= (uint)_length)
+ {
+ throw new IndexOutOfRangeException();
+ }
+
+ return _source[_start + index];
+ }
+ }
+
+ /// Slices this span further.
+ public MiniSpan Slice(int start, int length) =>
+ new MiniSpan(_source, _start + start, length);
+}
diff --git a/samples/MiniRuntime/README.md b/samples/MiniRuntime/README.md
new file mode 100644
index 0000000..704fc9f
--- /dev/null
+++ b/samples/MiniRuntime/README.md
@@ -0,0 +1,8 @@
+# MiniRuntime
+
+A tiny stand-in for a "real" upstream repo (think `dotnet/runtime`) used as a
+fixture by the local Aspire inner loop in `src/source-indexer.AppHost`.
+
+It exists purely so the inner loop has something small to build, capture a
+binlog from, push to Azurite, and index with `HtmlGenerator`. It is not
+published anywhere and is not part of the source-indexer product.
diff --git a/src/SourceBrowser/src/SourceIndexServer/Models/AzureBlobFileSystem.cs b/src/SourceBrowser/src/SourceIndexServer/Models/AzureBlobFileSystem.cs
index 5b380a7..e502aec 100644
--- a/src/SourceBrowser/src/SourceIndexServer/Models/AzureBlobFileSystem.cs
+++ b/src/SourceBrowser/src/SourceIndexServer/Models/AzureBlobFileSystem.cs
@@ -16,6 +16,17 @@ public class AzureBlobFileSystem : IFileSystem
public AzureBlobFileSystem(string uri)
{
+ // If AZURE_STORAGE_CONNECTION_STRING is set, initialize the client from
+ // the connection string. The container name still comes from `uri` so
+ // the existing SOURCE_BROWSER_INDEX_PROXY_URL contract is preserved.
+ string? connectionString = Environment.GetEnvironmentVariable("AZURE_STORAGE_CONNECTION_STRING");
+ if (!string.IsNullOrEmpty(connectionString))
+ {
+ string containerName = GetContainerNameFromUri(uri);
+ container = new BlobContainerClient(connectionString, containerName);
+ return;
+ }
+
var clientId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID");
credential = string.IsNullOrEmpty(clientId)
? new AzureCliCredential()
@@ -25,6 +36,39 @@ public AzureBlobFileSystem(string uri)
credential);
}
+ private static string GetContainerNameFromUri(string uri)
+ {
+ // The connection-string form of `uri` may include a ContainerName=
+ // segment (e.g. "DefaultEndpointsProtocol=...;ContainerName=index").
+ // Honor that segment first so callers can pass a full connection
+ // string instead of a URL.
+ if (uri.Contains("ContainerName=", StringComparison.OrdinalIgnoreCase))
+ {
+ foreach (var segment in uri.Split(';', StringSplitOptions.RemoveEmptyEntries))
+ {
+ if (segment.StartsWith("ContainerName=", StringComparison.OrdinalIgnoreCase))
+ {
+ return segment["ContainerName=".Length..].Trim();
+ }
+ }
+ }
+
+ var parsed = new Uri(uri);
+ // Azurite URLs are http://host:port/devstoreaccount1/, real
+ // Azure URLs are https://.blob.core.windows.net/.
+ // In both cases the last non-empty path segment is the container.
+ string[] segments = parsed.AbsolutePath
+ .Split('/', StringSplitOptions.RemoveEmptyEntries);
+ if (segments.Length == 0)
+ {
+ throw new ArgumentException(
+ $"URI '{uri}' has no path segments; cannot derive container name.",
+ nameof(uri));
+ }
+
+ return segments[segments.Length - 1];
+ }
+
public bool DirectoryExists(string name)
{
return true;
diff --git a/src/SourceBrowser/src/SourceIndexServer/Program.cs b/src/SourceBrowser/src/SourceIndexServer/Program.cs
index 97afa57..4ccdaaa 100644
--- a/src/SourceBrowser/src/SourceIndexServer/Program.cs
+++ b/src/SourceBrowser/src/SourceIndexServer/Program.cs
@@ -1,30 +1,85 @@
-using Microsoft.AspNetCore.Hosting;
+using System.IO;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.HttpOverrides;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.FileProviders.Physical;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
+using Microsoft.SourceBrowser.SourceIndexServer.Models;
namespace Microsoft.SourceBrowser.SourceIndexServer
{
public class Program
{
+ public static ILogger Logger { get; set; }
+
public static void Main(string[] args)
{
- BuildWebHost(args).Run();
- }
+ var builder = WebApplication.CreateBuilder(args);
- public static ILogger Logger { get; set; }
+ builder.AddServiceDefaults();
+
+ builder.Logging.ClearProviders();
+ builder.Logging.AddConsole();
+ builder.Logging.AddAzureWebAppDiagnostics();
+
+ builder.Host.UseWindowsService();
+
+ var rootPath = Path.Combine(builder.Environment.ContentRootPath, "index");
+ var subfolder = Path.Combine(rootPath, "index");
+ if (File.Exists(Path.Combine(subfolder, "Projects.txt")))
+ {
+ rootPath = subfolder;
+ }
+
+ builder.Services.AddSingleton(new Index(rootPath));
+ builder.Services.AddControllersWithViews();
+ builder.Services.AddRazorPages();
+
+ var app = builder.Build();
- public static IHost BuildWebHost(string[] args) =>
- Host.CreateDefaultBuilder(args)
- .ConfigureWebHostDefaults(
- builder => { builder
- .UseStartup(); })
- .ConfigureLogging(logging =>
- {
- logging.ClearProviders();
- logging.AddConsole();
- logging.AddAzureWebAppDiagnostics();
- })
- .UseWindowsService()
- .Build();
+ app.MapDefaultEndpoints();
+
+ app.UseForwardedHeaders(new ForwardedHeadersOptions
+ {
+ ForwardedHeaders = ForwardedHeaders.XForwardedFor |
+ ForwardedHeaders.XForwardedProto |
+ ForwardedHeaders.XForwardedHost,
+ KnownNetworks = { },
+ KnownProxies = { }
+ });
+
+ app.Use(async (context, next) =>
+ {
+ context.Response.Headers["X-UA-Compatible"] = "IE=edge";
+ await next();
+ });
+
+ if (app.Environment.IsDevelopment())
+ {
+ app.UseDeveloperExceptionPage();
+ }
+
+ app.Use(Helpers.ServeProxiedIndex);
+
+ app.UseDefaultFiles();
+ if (Directory.Exists(rootPath))
+ {
+ app.UseStaticFiles(new StaticFileOptions
+ {
+ FileProvider = new PhysicalFileProvider(rootPath, ExclusionFilters.Sensitive & ~ExclusionFilters.DotPrefixed),
+ });
+ }
+ app.UseStaticFiles();
+ app.UseRouting();
+
+ app.MapRazorPages();
+ app.MapControllers();
+
+ Logger = app.Services.GetService>();
+
+ app.Run();
+ }
}
}
diff --git a/src/SourceBrowser/src/SourceIndexServer/SourceIndexServer.csproj b/src/SourceBrowser/src/SourceIndexServer/SourceIndexServer.csproj
index d2fe663..3c1b620 100644
--- a/src/SourceBrowser/src/SourceIndexServer/SourceIndexServer.csproj
+++ b/src/SourceBrowser/src/SourceIndexServer/SourceIndexServer.csproj
@@ -16,6 +16,7 @@
+
diff --git a/src/SourceBrowser/src/SourceIndexServer/Startup.cs b/src/SourceBrowser/src/SourceIndexServer/Startup.cs
deleted file mode 100644
index 782b8af..0000000
--- a/src/SourceBrowser/src/SourceIndexServer/Startup.cs
+++ /dev/null
@@ -1,145 +0,0 @@
-using System.Collections;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.HttpOverrides;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.FileProviders;
-using Microsoft.Extensions.FileProviders.Physical;
-using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.Logging;
-using Microsoft.SourceBrowser.SourceIndexServer.Models;
-
-namespace Microsoft.SourceBrowser.SourceIndexServer
-{
- public class Startup
- {
- public Startup(IWebHostEnvironment env)
- {
- Environment = env;
- }
-
- public IWebHostEnvironment Environment { get; }
-
- // This method gets called by the runtime. Use this method to add services to the container.
- // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
- public void ConfigureServices(IServiceCollection services)
- {
- RootPath = Path.Combine(Environment.ContentRootPath, "index");
-
- var subfolder = Path.Combine(RootPath, "index");
- if (File.Exists(Path.Combine(subfolder, "Projects.txt")))
- {
- RootPath = subfolder;
- }
-
- services.AddSingleton(new Index(RootPath));
- services.AddControllersWithViews();
- services.AddRazorPages();
-
- // Add health checks
- //services.AddHealthChecks()
- //.AddCheck(
- //name: "storage",
- //tags: ["ready"])
- //.AddCheck(
- //name: "startup",
- //check: () => HealthCheckResult.Healthy("Application is running"),
- //tags: ["alive"]);
- }
-
- public string RootPath { get; set; }
-
- // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
- public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
- {
- // Configure forwarded headers for Azure Front Door
- app.UseForwardedHeaders(new ForwardedHeadersOptions
- {
- ForwardedHeaders = ForwardedHeaders.XForwardedFor |
- ForwardedHeaders.XForwardedProto |
- ForwardedHeaders.XForwardedHost,
- KnownNetworks = { },
- KnownProxies = { }
- });
-
- app.Use(async (context, next) =>
- {
- context.Response.Headers["X-UA-Compatible"] = "IE=edge";
- await next();
- });
-
- if (env.IsDevelopment())
- {
- app.UseDeveloperExceptionPage();
- }
-
- app.Use(Helpers.ServeProxiedIndex);
-
- app.UseDefaultFiles();
- if (Directory.Exists(RootPath))
- {
- app.UseStaticFiles(new StaticFileOptions
- {
- FileProvider = new PhysicalFileProvider(RootPath, ExclusionFilters.Sensitive & ~ExclusionFilters.DotPrefixed),
- });
- }
- app.UseStaticFiles();
- app.UseRouting();
-
- app.UseEndpoints(endPoints =>
- {
- //const int healthCacheSeconds = 30;
-
- //static Task CacheableMinimalResponse(HttpContext context, HealthReport report)
- //{
- //context.Response.Headers.CacheControl = $"public,max-age={healthCacheSeconds}";
- //context.Response.Headers.Pragma = "public";
- //context.Response.Headers.Expires = "0";
- //return HealthChecks.HealthCheckResponseWriter.WriteMinimalResponse(context, report);
- //}
-
- //// Health check endpoints
- //// Basic health check with minimal information (cached by default)
- //endPoints.MapHealthChecks("/health", new HealthCheckOptions
- //{
- //Predicate = _ => true,
- //ResponseWriter = CacheableMinimalResponse
- //});
-
- //// Liveness probe (always healthy if app is running)
- //endPoints.MapHealthChecks("/health/alive", new HealthCheckOptions
- //{
- //Predicate = check => check.Tags.Contains("alive"),
- //ResponseWriter = HealthChecks.HealthCheckResponseWriter.WriteMinimalResponse
- //});
-
- //if (env.IsDevelopment() || Helpers.DebugLoggingEnabled)
- //{
- //// Detailed health check with full diagnostics
- //endPoints.MapHealthChecks("/health/detailed", new HealthCheckOptions
- //{
- //Predicate = _ => true,
- //ResponseWriter = HealthChecks.HealthCheckResponseWriter.WriteResponse
- //});
-
- //// Readiness probe (checks storage)
- //endPoints.MapHealthChecks("/health/ready", new HealthCheckOptions
- //{
- //Predicate = check => check.Tags.Contains("ready"),
- //ResponseWriter = HealthChecks.HealthCheckResponseWriter.WriteMinimalResponse
- //});
- //}
-
- endPoints.MapRazorPages();
- endPoints.MapControllers();
- });
-
- // Retrieve and store the logger
- Program.Logger = app.ApplicationServices.GetService>();
- }
- }
-}
diff --git a/src/UploadIndexStage1/Program.cs b/src/UploadIndexStage1/Program.cs
index 3b3d82a..6b0dfab 100644
--- a/src/UploadIndexStage1/Program.cs
+++ b/src/UploadIndexStage1/Program.cs
@@ -12,6 +12,7 @@
using Azure.Storage.Blobs.Models;
using ICSharpCode.SharpZipLib.GZip;
using ICSharpCode.SharpZipLib.Tar;
+using Microsoft.Extensions.Hosting;
using Mono.Options;
namespace UploadIndexStage1
@@ -19,12 +20,29 @@ namespace UploadIndexStage1
class Program
{
static async Task Main(string[] args)
+ {
+ var hostBuilder = Host.CreateApplicationBuilder(args);
+ hostBuilder.AddServiceDefaults();
+ using var host = hostBuilder.Build();
+ await host.StartAsync();
+ try
+ {
+ await RunAsync(args);
+ }
+ finally
+ {
+ await host.StopAsync();
+ }
+ }
+
+ static async Task RunAsync(string[] args)
{
string sourceFolder = null;
string repoName = null;
string clientId = null;
string storageAccount = null;
string blobContainer = null;
+ string connectionString = null;
var options = new OptionSet
{
{"i=", "The source folder", i => sourceFolder = i},
@@ -32,6 +50,7 @@ static async Task Main(string[] args)
{"c=", "The Azure Client ID (optional)", c => clientId = c},
{"s=", "The destination storage account name or URL", s => storageAccount = s},
{"b=", "The destination storage account container", b => blobContainer = b},
+ {"connection-string=", "Optional connection string for blob auth. When set, -s/-c are ignored.", cs => connectionString = cs},
};
List extra = options.Parse(args);
@@ -51,9 +70,9 @@ static async Task Main(string[] args)
Fatal("Missing argument -n");
}
- if (string.IsNullOrEmpty(storageAccount))
+ if (string.IsNullOrEmpty(connectionString))
{
- Fatal("Missing argument -s");
+ connectionString = Environment.GetEnvironmentVariable("AZURE_STORAGE_CONNECTION_STRING");
}
if (string.IsNullOrEmpty(blobContainer))
@@ -61,37 +80,52 @@ static async Task Main(string[] args)
Fatal("Missing argument -b");
}
- if (!storageAccount.StartsWith("https://"))
- {
- storageAccount = "https://" + storageAccount + ".blob.core.windows.net";
- }
-
+ BlobServiceClient blobServiceClient;
using AzureEventSourceListener listener = AzureEventSourceListener.CreateConsoleLogger();
- TokenCredential credential;
-
- if (string.IsNullOrEmpty(clientId) && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ARM_CLIENT_ID")))
+ if (!string.IsNullOrEmpty(connectionString))
{
- clientId = Environment.GetEnvironmentVariable("ARM_CLIENT_ID");
- System.Console.WriteLine("Found client ID in environment variable; using it");
- }
-
- if (string.IsNullOrEmpty(clientId))
- {
- credential = new AzureCliCredential();
- System.Console.WriteLine("Trying to use managed identity without default identity");
+ Console.WriteLine("Using connection-string for blob auth.");
+ blobServiceClient = new BlobServiceClient(connectionString);
}
else
{
- System.Console.WriteLine("Trying to use ManagedIdentityCredential with ClientID");
- credential = new ManagedIdentityCredential(clientId);
- }
+ if (string.IsNullOrEmpty(storageAccount))
+ {
+ Fatal("Missing argument -s (or supply --connection-string / AZURE_STORAGE_CONNECTION_STRING)");
+ }
+
+ if (!storageAccount.StartsWith("https://"))
+ {
+ storageAccount = "https://" + storageAccount + ".blob.core.windows.net";
+ }
+
+ TokenCredential credential;
- BlobServiceClient blobServiceClient = new(
- new Uri(storageAccount),
- credential);
+ if (string.IsNullOrEmpty(clientId) && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ARM_CLIENT_ID")))
+ {
+ clientId = Environment.GetEnvironmentVariable("ARM_CLIENT_ID");
+ System.Console.WriteLine("Found client ID in environment variable; using it");
+ }
+
+ if (string.IsNullOrEmpty(clientId))
+ {
+ credential = new AzureCliCredential();
+ System.Console.WriteLine("Trying to use managed identity without default identity");
+ }
+ else
+ {
+ System.Console.WriteLine("Trying to use ManagedIdentityCredential with ClientID");
+ credential = new ManagedIdentityCredential(clientId);
+ }
+
+ blobServiceClient = new BlobServiceClient(
+ new Uri(storageAccount),
+ credential);
+ }
var containerClient = blobServiceClient.GetBlobContainerClient(blobContainer);
+ await containerClient.CreateIfNotExistsAsync();
string newBlobName = $"{repoName}/{DateTime.UtcNow:O}.tar.gz";
BlobClient newBlobClient = containerClient.GetBlobClient(newBlobName);
diff --git a/src/UploadIndexStage1/UploadIndexStage1.csproj b/src/UploadIndexStage1/UploadIndexStage1.csproj
index 1af973c..739b06b 100644
--- a/src/UploadIndexStage1/UploadIndexStage1.csproj
+++ b/src/UploadIndexStage1/UploadIndexStage1.csproj
@@ -16,4 +16,8 @@
+
+
+
+
diff --git a/src/source-indexer.AppHost/AppHost.cs b/src/source-indexer.AppHost/AppHost.cs
new file mode 100644
index 0000000..7438345
--- /dev/null
+++ b/src/source-indexer.AppHost/AppHost.cs
@@ -0,0 +1,224 @@
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.Azure;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+var builder = DistributedApplication.CreateBuilder(args);
+
+// =============================================================================
+// Storage — emulates the two prod storage accounts via Azurite.
+//
+// Prod has TWO storage accounts:
+// netsourceindexstage1 ← V2 upstream repos push /.tar.gz here
+// netsourceindexprod ← HtmlGenerator's output is uploaded as index-/
+//
+// We model each as its own AddAzureStorage().RunAsEmulator() (= two Azurite
+// containers) so the boundary is visually distinct in the dashboard.
+// =============================================================================
+
+var stage1Storage = builder.AddAzureStorage("stage1Storage")
+ .RunAsEmulator(azurite =>
+ {
+ azurite.WithLifetime(ContainerLifetime.Persistent)
+ .WithDataBindMount(".azurite/stage1");
+ });
+
+var stage1Blobs = stage1Storage.AddBlobs("stage1-blobs");
+var stage1Container = stage1Storage.AddBlobContainer("stage1", blobContainerName: "stage1");
+
+var prodStorage = builder.AddAzureStorage("prodStorage")
+ .RunAsEmulator(azurite =>
+ {
+ azurite.WithLifetime(ContainerLifetime.Persistent)
+ .WithDataBindMount(".azurite/prod");
+ });
+
+var prodBlobs = prodStorage.AddBlobs("prod-blobs");
+var indexContainer = prodStorage.AddBlobContainer("index-local", blobContainerName: "index-local");
+
+// =============================================================================
+// Pipeline resources — all WithExplicitStart() so they only run when the user
+// clicks "Start" in the dashboard (or via the bootstrap-all composite command).
+// They emulate the prod stages from azure-pipelines.yml / src/index/index.proj.
+// =============================================================================
+
+string repoRoot = Path.GetFullPath(Path.Combine(builder.AppHostDirectory, "..", ".."));
+string sampleProj = Path.Combine(repoRoot, "samples", "MiniRuntime", "MiniRuntime.csproj");
+string sampleBinDir = Path.Combine(repoRoot, "samples", "MiniRuntime", "bin", "sample");
+string sampleBinlog = Path.Combine(sampleBinDir, "msbuild.binlog");
+string sampleSourceDir = Path.Combine(repoRoot, "samples", "MiniRuntime");
+string indexOutDir = Path.Combine(repoRoot, "bin", "index");
+string indexUploadDir = Path.Combine(indexOutDir, "index");
+
+// 1. sample-build — `dotnet build /bl:` on MiniRuntime, producing a binlog.
+// Emulates: Arcade-driven V1 repo build that produces a binlog.
+// Use /t:Rebuild so MSBuild always re-invokes the C# compiler — otherwise an
+// incremental no-op build produces a binlog with zero Csc invocations and
+// HtmlGenerator finds nothing to index.
+var sampleBuild = builder.AddExecutable(
+ "step1-sample-build",
+ "dotnet",
+ repoRoot,
+ "build",
+ sampleProj,
+ $"/bl:{sampleBinlog}",
+ "/t:Rebuild",
+ "-c", "Debug")
+ .WithExplicitStart();
+
+// 2. upload-stage1 — runs the real UploadIndexStage1 tool against Azurite.
+// Emulates: V2 upstream repos uploading their source bundle to
+// netsourceindexstage1/stage1//.tar.gz.
+// This is the same .NET console app that runs in upstream pipelines, so
+// you can attach a debugger to it directly from the Aspire dashboard.
+var uploadStage1 = builder.AddProject("step2-upload-stage1")
+ .WithExplicitStart()
+ .WithReference(stage1Blobs)
+ .WaitFor(stage1Container)
+ .WithEnvironment("AZURE_STORAGE_CONNECTION_STRING", stage1Blobs.Resource.ConnectionStringExpression)
+ .WithArgs(
+ "-i", sampleSourceDir,
+ "-n", "MiniRuntime",
+ "-b", "stage1");
+
+// 3. htmlgenerator — runs HtmlGenerator (net472) over the binlog from step 1
+// and produces static HTML under bin/index/. HtmlGenerator targets net472
+// so we can't link its output assembly into the net10 AppHost, but
+// `ReferenceOutputAssembly="false" SkipGetTargetFrameworkProperties="true"`
+// on the ProjectReference still gets us the Projects.HtmlGenerator
+// metadata, so we can use AddProject and get debugging / restart-on-
+// rebuild for free.
+var htmlGenerator = builder.AddProject("step3-htmlgenerator", launchProfileName: null)
+ .WithExplicitStart()
+ .WithArgs(
+ sampleBinlog,
+ $"/out:{indexOutDir}",
+ "/force",
+ $"/serverPath:{sampleSourceDir}=https://github.com/dotnet/source-indexer/tree/main/samples/MiniRuntime/");
+
+// 4. normalize-case — lowercase every filename under bin/index/index/.
+// SourceIndexServer.Helpers.ServeProxiedIndex lowercases incoming request
+// paths before querying the blob (case-sensitive in Azure Storage), so any
+// PascalCase output from HtmlGenerator (Projects.txt, results.html, etc.)
+// would 404 if uploaded as-is. Prod runs the same script — see
+// azure-pipelines.yml line ~165 (deployment/normalize-case.ps1).
+var normalizeCase = builder.AddExecutable(
+ "step4-normalize-case",
+ "pwsh",
+ repoRoot,
+ "-NoProfile",
+ "-File", Path.Combine(repoRoot, "deployment", "normalize-case.ps1"),
+ "-Root", indexUploadDir)
+ .WithExplicitStart();
+
+// 5. publish-index — wraps the Azure CLI (matching prod's AzureFileCopy@6
+// task). Uploads bin/index/index/* to prodStorage/index-local.
+var publishIndex = builder.AddExecutable(
+ "step5-publish-index",
+ "az",
+ repoRoot,
+ "storage", "blob", "upload-batch",
+ "-s", indexUploadDir,
+ "-d", "index-local",
+ "--overwrite",
+ "true")
+ .WithExplicitStart()
+ .WithReference(prodBlobs)
+ .WaitFor(indexContainer)
+ .WithEnvironment(ctx =>
+ {
+ // The az CLI reads AZURE_STORAGE_CONNECTION_STRING natively.
+ ctx.EnvironmentVariables["AZURE_STORAGE_CONNECTION_STRING"] =
+ prodBlobs.Resource.ConnectionStringExpression;
+ });
+
+// =============================================================================
+// Web — SourceIndexServer, the real app. Auto-starts and serves indexed HTML
+// out of prodStorage/index-local via SOURCE_BROWSER_INDEX_PROXY_URL (the same
+// env var contract prod uses; see deployment/deploy-storage-proxy.ps1).
+// =============================================================================
+
+var web = builder.AddProject("web")
+ .WithReference(prodBlobs)
+ .WaitFor(indexContainer)
+ .WithEnvironment("AZURE_STORAGE_CONNECTION_STRING", prodBlobs.Resource.ConnectionStringExpression)
+ .WithEnvironment("SOURCE_BROWSER_INDEX_PROXY_URL", indexContainer.Resource.ConnectionStringExpression)
+ .WithExternalHttpEndpoints();
+
+// =============================================================================
+// bootstrap-all — composite command that runs the full pipeline end-to-end.
+// Attached to prodStorage (the "end of the pipeline" resource) since
+// app-host-level commands aren't yet available in this Aspire SDK.
+// =============================================================================
+
+prodStorage.WithCommand(
+ "bootstrap-all",
+ "Bootstrap full pipeline",
+ async context =>
+ {
+ var ct = context.CancellationToken;
+ var commandService = context.ServiceProvider.GetRequiredService();
+ var notifications = context.ServiceProvider.GetRequiredService();
+ var logger = context.Logger;
+
+ var stages = new (string Name, IResource Resource)[]
+ {
+ ("step1-sample-build", sampleBuild.Resource),
+ ("step2-upload-stage1", uploadStage1.Resource),
+ ("step3-htmlgenerator", htmlGenerator.Resource),
+ ("step4-normalize-case", normalizeCase.Resource),
+ ("step5-publish-index", publishIndex.Resource),
+ };
+
+ foreach (var (name, resource) in stages)
+ {
+ logger.LogInformation("[bootstrap-all] Starting {Stage}...", name);
+
+ var start = await commandService.ExecuteCommandAsync(
+ resource: resource,
+ commandName: "resource-start",
+ cancellationToken: ct);
+
+ if (!start.Success)
+ {
+ return CommandResults.Failure($"Failed to start {name}: {start.Message}");
+ }
+
+ // Wait until the resource reaches a terminal state.
+ var snapshot = await notifications.WaitForResourceAsync(
+ resource.Name,
+ e => e.Snapshot.State?.Text is "Finished" or "Exited" or "FailedToStart" or "RuntimeUnhealthy",
+ ct);
+
+ var state = snapshot.Snapshot.State?.Text;
+ var exitCode = snapshot.Snapshot.ExitCode;
+
+ if (state != "Finished" && state != "Exited" || (exitCode is int code && code != 0))
+ {
+ return CommandResults.Failure(
+ $"{name} ended in state '{state}' with exit code {exitCode?.ToString() ?? ""}.");
+ }
+
+ logger.LogInformation("[bootstrap-all] {Stage} finished cleanly.", name);
+ }
+
+ // The web's IndexLoader only runs once at startup (Index ctor → Task.Run).
+ // In prod the app-service slot-setting flip restarts the app after publish;
+ // locally we mirror that by restarting `web` so it re-reads the freshly
+ // published container.
+ logger.LogInformation("[bootstrap-all] Restarting web to pick up the freshly published index...");
+ var restart = await commandService.ExecuteCommandAsync(
+ resource: web.Resource,
+ commandName: "resource-restart",
+ cancellationToken: ct);
+ if (!restart.Success)
+ {
+ return CommandResults.Failure($"Failed to restart web: {restart.Message}");
+ }
+ await notifications.WaitForResourceHealthyAsync(web.Resource.Name, ct);
+
+ return CommandResults.Success();
+ });
+
+builder.Build().Run();
+
diff --git a/src/source-indexer.AppHost/Properties/launchSettings.json b/src/source-indexer.AppHost/Properties/launchSettings.json
new file mode 100644
index 0000000..33d3c81
--- /dev/null
+++ b/src/source-indexer.AppHost/Properties/launchSettings.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:17258;http://localhost:15161",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21208",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22007"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15161",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19226",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20243"
+ }
+ }
+ }
+}
diff --git a/src/source-indexer.AppHost/appsettings.Development.json b/src/source-indexer.AppHost/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/src/source-indexer.AppHost/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/src/source-indexer.AppHost/appsettings.json b/src/source-indexer.AppHost/appsettings.json
new file mode 100644
index 0000000..31c092a
--- /dev/null
+++ b/src/source-indexer.AppHost/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "Warning"
+ }
+ }
+}
diff --git a/src/source-indexer.AppHost/aspire.config.json b/src/source-indexer.AppHost/aspire.config.json
new file mode 100644
index 0000000..0e212ac
--- /dev/null
+++ b/src/source-indexer.AppHost/aspire.config.json
@@ -0,0 +1,5 @@
+{
+ "appHost": {
+ "path": "source-indexer.AppHost.csproj"
+ }
+}
diff --git a/src/source-indexer.AppHost/source-indexer.AppHost.csproj b/src/source-indexer.AppHost/source-indexer.AppHost.csproj
new file mode 100644
index 0000000..eb5012e
--- /dev/null
+++ b/src/source-indexer.AppHost/source-indexer.AppHost.csproj
@@ -0,0 +1,27 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ fc6b83b7-dced-49f7-aa1c-78458fd03ee3
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/source-indexer.ServiceDefaults/Extensions.cs b/src/source-indexer.ServiceDefaults/Extensions.cs
new file mode 100644
index 0000000..4fdcccf
--- /dev/null
+++ b/src/source-indexer.ServiceDefaults/Extensions.cs
@@ -0,0 +1,127 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Diagnostics.HealthChecks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.ServiceDiscovery;
+using OpenTelemetry;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Trace;
+
+namespace Microsoft.Extensions.Hosting;
+
+// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
+// This project should be referenced by each service project in your solution.
+// To learn more about using this project, see https://aka.ms/aspire/service-defaults
+public static class Extensions
+{
+ private const string HealthEndpointPath = "/health";
+ private const string AlivenessEndpointPath = "/alive";
+
+ public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder
+ {
+ builder.ConfigureOpenTelemetry();
+
+ builder.AddDefaultHealthChecks();
+
+ builder.Services.AddServiceDiscovery();
+
+ builder.Services.ConfigureHttpClientDefaults(http =>
+ {
+ // Turn on resilience by default
+ http.AddStandardResilienceHandler();
+
+ // Turn on service discovery by default
+ http.AddServiceDiscovery();
+ });
+
+ // Uncomment the following to restrict the allowed schemes for service discovery.
+ // builder.Services.Configure(options =>
+ // {
+ // options.AllowedSchemes = ["https"];
+ // });
+
+ return builder;
+ }
+
+ public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder
+ {
+ builder.Logging.AddOpenTelemetry(logging =>
+ {
+ logging.IncludeFormattedMessage = true;
+ logging.IncludeScopes = true;
+ });
+
+ builder.Services.AddOpenTelemetry()
+ .WithMetrics(metrics =>
+ {
+ metrics.AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddRuntimeInstrumentation();
+ })
+ .WithTracing(tracing =>
+ {
+ tracing.AddSource(builder.Environment.ApplicationName)
+ .AddAspNetCoreInstrumentation(tracing =>
+ // Exclude health check requests from tracing
+ tracing.Filter = context =>
+ !context.Request.Path.StartsWithSegments(HealthEndpointPath)
+ && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath)
+ )
+ // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
+ //.AddGrpcClientInstrumentation()
+ .AddHttpClientInstrumentation();
+ });
+
+ builder.AddOpenTelemetryExporters();
+
+ return builder;
+ }
+
+ private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder
+ {
+ var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
+
+ if (useOtlpExporter)
+ {
+ builder.Services.AddOpenTelemetry().UseOtlpExporter();
+ }
+
+ // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
+ //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
+ //{
+ // builder.Services.AddOpenTelemetry()
+ // .UseAzureMonitor();
+ //}
+
+ return builder;
+ }
+
+ public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder
+ {
+ builder.Services.AddHealthChecks()
+ // Add a default liveness check to ensure app is responsive
+ .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
+
+ return builder;
+ }
+
+ public static WebApplication MapDefaultEndpoints(this WebApplication app)
+ {
+ // Adding health checks endpoints to applications in non-development environments has security implications.
+ // See https://aka.ms/aspire/healthchecks for details before enabling these endpoints in non-development environments.
+ if (app.Environment.IsDevelopment())
+ {
+ // All health checks must pass for app to be considered ready to accept traffic after starting
+ app.MapHealthChecks(HealthEndpointPath);
+
+ // Only health checks tagged with the "live" tag must pass for app to be considered alive
+ app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions
+ {
+ Predicate = r => r.Tags.Contains("live")
+ });
+ }
+
+ return app;
+ }
+}
diff --git a/src/source-indexer.ServiceDefaults/source-indexer.ServiceDefaults.csproj b/src/source-indexer.ServiceDefaults/source-indexer.ServiceDefaults.csproj
new file mode 100644
index 0000000..09110f1
--- /dev/null
+++ b/src/source-indexer.ServiceDefaults/source-indexer.ServiceDefaults.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net9.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/source-indexer.sln b/src/source-indexer.sln
index a9dce76..45070de 100644
--- a/src/source-indexer.sln
+++ b/src/source-indexer.sln
@@ -7,24 +7,109 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.SourceIndexer.Tas
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UploadIndexStage1", "UploadIndexStage1\UploadIndexStage1.csproj", "{72B1789D-B80E-4073-BDB4-672E3E2E45DA}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "source-indexer.AppHost", "source-indexer.AppHost\source-indexer.AppHost.csproj", "{8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SourceBrowser", "SourceBrowser", "{F69B90C1-642B-7466-139B-A7D79630D179}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{61FFE273-1437-3EAF-DA5A-B4D8CAE2E20F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceIndexServer", "SourceBrowser\src\SourceIndexServer\SourceIndexServer.csproj", "{EB2C8F73-63CD-4727-B11D-773E9E0A1E40}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "SourceBrowser\src\Common\Common.csproj", "{623DF7AB-9F20-41CA-B1A9-10C100028A17}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniRuntime", "..\samples\MiniRuntime\MiniRuntime.csproj", "{1F66CD86-D2AD-40C2-846A-292FBB020CF3}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Debug|x64.Build.0 = Debug|Any CPU
+ {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Debug|x86.Build.0 = Debug|Any CPU
{5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Release|x64.ActiveCfg = Release|Any CPU
+ {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Release|x64.Build.0 = Release|Any CPU
+ {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Release|x86.ActiveCfg = Release|Any CPU
+ {5D86D28A-FEB9-4561-8EA8-D3C3BB409FB3}.Release|x86.Build.0 = Release|Any CPU
{72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Debug|x64.Build.0 = Debug|Any CPU
+ {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Debug|x86.Build.0 = Debug|Any CPU
{72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Release|x64.ActiveCfg = Release|Any CPU
+ {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Release|x64.Build.0 = Release|Any CPU
+ {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Release|x86.ActiveCfg = Release|Any CPU
+ {72B1789D-B80E-4073-BDB4-672E3E2E45DA}.Release|x86.Build.0 = Release|Any CPU
+ {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Debug|x64.Build.0 = Debug|Any CPU
+ {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Debug|x86.Build.0 = Debug|Any CPU
+ {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Release|x64.ActiveCfg = Release|Any CPU
+ {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Release|x64.Build.0 = Release|Any CPU
+ {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Release|x86.ActiveCfg = Release|Any CPU
+ {8C2DD96E-D6A0-40F5-87AE-D3A2E97E1627}.Release|x86.Build.0 = Release|Any CPU
+ {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Debug|x64.Build.0 = Debug|Any CPU
+ {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Debug|x86.Build.0 = Debug|Any CPU
+ {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Release|x64.ActiveCfg = Release|Any CPU
+ {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Release|x64.Build.0 = Release|Any CPU
+ {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Release|x86.ActiveCfg = Release|Any CPU
+ {EB2C8F73-63CD-4727-B11D-773E9E0A1E40}.Release|x86.Build.0 = Release|Any CPU
+ {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Debug|x64.Build.0 = Debug|Any CPU
+ {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Debug|x86.Build.0 = Debug|Any CPU
+ {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Release|Any CPU.Build.0 = Release|Any CPU
+ {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Release|x64.ActiveCfg = Release|Any CPU
+ {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Release|x64.Build.0 = Release|Any CPU
+ {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Release|x86.ActiveCfg = Release|Any CPU
+ {623DF7AB-9F20-41CA-B1A9-10C100028A17}.Release|x86.Build.0 = Release|Any CPU
+ {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Debug|x64.Build.0 = Debug|Any CPU
+ {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Debug|x86.Build.0 = Debug|Any CPU
+ {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Release|x64.ActiveCfg = Release|Any CPU
+ {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Release|x64.Build.0 = Release|Any CPU
+ {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Release|x86.ActiveCfg = Release|Any CPU
+ {1F66CD86-D2AD-40C2-846A-292FBB020CF3}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {61FFE273-1437-3EAF-DA5A-B4D8CAE2E20F} = {F69B90C1-642B-7466-139B-A7D79630D179}
+ {EB2C8F73-63CD-4727-B11D-773E9E0A1E40} = {61FFE273-1437-3EAF-DA5A-B4D8CAE2E20F}
+ {623DF7AB-9F20-41CA-B1A9-10C100028A17} = {61FFE273-1437-3EAF-DA5A-B4D8CAE2E20F}
+ EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {CBA6774C-91B0-4634-94B4-B6C12A4519C8}
EndGlobalSection