From 27e97bad6a6381ff9be58231f0dd8d0712be4cd8 Mon Sep 17 00:00:00 2001 From: David Pine <7679720+IEvangelist@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:33:19 -0500 Subject: [PATCH] Refresh sample data for Aspire 13.3 API updates Regenerates `src/frontend/src/data/samples.json` from microsoft/aspire-samples#1668 (`Update samples for Aspire 13.3 APIs`) so the sample detail pages on aspire.dev pick up the new APIs that PR exercises. Notable AppHost-code changes mirrored into the data file: - `aspire-with-azure-functions`: uses `WithComputeEnvironment(env)` and switches `AddAzureFunctionsProject` to the typed-name overload. - `aspire-with-javascript`: adds `WithBrowserLogs()` for Angular/React/Vue/Vite, swaps `PublishAsDockerFile` for `PublishAsStaticWebsite`, and adds `WithNpm(installCommand: \"ci\")` for npm-ci installs. - `aspire-with-node`: adds `WithBrowserLogs()` to the Node frontend. - `aspire-with-python`: adds `Aspire.Hosting.Browsers` package reference and `WithBrowserLogs()` + `PublishAsStaticWebsite` on the Vite frontend. - `image-gallery`: switches to `WithComputeEnvironment(env)` and the typed-name `AddAzureFunctionsProject` overload (plus README updates). - `node-express-redis` / `polyglot-task-queue` / `rag-document-qa-svelte` / `vite-csharp-postgres` / `vite-react-fastapi` / `vite-yarp-static`: pick up TypeScript AppHost `apphost.mts` updates and `aspire.config.json` additions. - `health-checks-ui` / `custom-resources`: pick up new `TestResource` plumbing and AppHost updates. Also normalizes a handful of em-dash characters from `\\u2014` ASCII-escapes back to literal Unicode (artifact from an earlier intermediate edit by the typo-fix script in #1189); `update-samples.ts` natively writes literal Unicode, so this restores the file to the script's native output format. JSON parses identically either way. Generated by temporarily pointing `update-samples.ts` at the PR's head branch (`ievangelist/dapine-aspire-api-updates`) via a `SAMPLES_BRANCH` env-var override, then reverting the script edit. `href` fields remain anchored to `tree/main` so links keep working after #1668 merges. Verified: 62/62 `custom-components.vitest.test.ts` pass, `pnpm lint` clean, `samples.json` parses (26 samples). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/frontend/src/data/samples.json | 46 +++++++++++++++--------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/frontend/src/data/samples.json b/src/frontend/src/data/samples.json index 5c989ee21..930044449 100644 --- a/src/frontend/src/data/samples.json +++ b/src/frontend/src/data/samples.json @@ -35,7 +35,7 @@ "thumbnail": "~/assets/samples/aspire-with-azure-functions/aspire-with-functions.png", "appHost": "csproj", "appHostPath": "ImageGallery.AppHost/AppHost.cs", - "appHostCode": "using Azure.Provisioning.Storage;\n\nvar builder = DistributedApplication.CreateBuilder(args);\n\nbuilder.AddAzureContainerAppEnvironment(\"env\");\n\nvar storage = builder.AddAzureStorage(\"storage\").RunAsEmulator()\n .ConfigureInfrastructure((infrastructure) =>\n {\n var storageAccount = infrastructure.GetProvisionableResources().OfType().FirstOrDefault(r => r.BicepIdentifier == \"storage\")\n ?? throw new InvalidOperationException($\"Could not find configured storage account with name 'storage'\");\n\n // Ensure that public access to blobs is disabled\n storageAccount.AllowBlobPublicAccess = false;\n })\n .WithUrls(c =>\n {\n // None of the URLs are usable in the browser so hide them from the summary page\n foreach (var url in c.Urls)\n {\n url.DisplayLocation = UrlDisplayLocation.DetailsOnly;\n }\n });\nvar blobs = storage.AddBlobs(\"blobs\");\nvar queues = storage.AddQueues(\"queues\");\n\nvar functions = builder.AddAzureFunctionsProject(\"functions\")\n .WithReference(queues)\n .WithReference(blobs)\n .WaitFor(storage)\n .WithRoleAssignments(storage,\n // Storage Account Contributor and Storage Blob Data Owner roles are required by the Azure Functions host\n StorageBuiltInRole.StorageAccountContributor, StorageBuiltInRole.StorageBlobDataOwner,\n // Queue Data Contributor role is required to send messages to the queue\n StorageBuiltInRole.StorageQueueDataContributor)\n .WithHostStorage(storage)\n .WithUrlForEndpoint(\"http\", u => u.DisplayText = \"Functions App\");\n\nbuilder.AddProject(\"frontend\")\n .WithReference(queues)\n .WithReference(blobs)\n .WaitFor(functions)\n .WithExternalHttpEndpoints()\n .WithUrlForEndpoint(\"https\", u => u.DisplayText = \"Frontend App\")\n .WithUrlForEndpoint(\"http\", u => u.DisplayLocation = UrlDisplayLocation.DetailsOnly);\n\nbuilder.Build().Run();" + "appHostCode": "using Azure.Provisioning.Storage;\n\nvar builder = DistributedApplication.CreateBuilder(args);\n\nvar env = builder.AddAzureContainerAppEnvironment(\"env\");\n\nvar storage = builder.AddAzureStorage(\"storage\").RunAsEmulator()\n .ConfigureInfrastructure((infrastructure) =>\n {\n var storageAccount = infrastructure.GetProvisionableResources().OfType().FirstOrDefault(r => r.BicepIdentifier == \"storage\")\n ?? throw new InvalidOperationException($\"Could not find configured storage account with name 'storage'\");\n\n // Ensure that public access to blobs is disabled\n storageAccount.AllowBlobPublicAccess = false;\n })\n .WithUrls(c =>\n {\n // None of the URLs are usable in the browser so hide them from the summary page\n foreach (var url in c.Urls)\n {\n url.DisplayLocation = UrlDisplayLocation.DetailsOnly;\n }\n });\nvar blobs = storage.AddBlobs(\"blobs\");\nvar queues = storage.AddQueues(\"queues\");\n\nvar functions = builder.AddAzureFunctionsProject(\"functions\", \"../ImageGallery.Functions/ImageGallery.Functions.csproj\")\n .WithComputeEnvironment(env)\n .WithReference(queues)\n .WithReference(blobs)\n .WaitFor(storage)\n .WithRoleAssignments(storage,\n // Storage Account Contributor and Storage Blob Data Owner roles are required by the Azure Functions host\n StorageBuiltInRole.StorageAccountContributor, StorageBuiltInRole.StorageBlobDataOwner,\n // Queue Data Contributor role is required to send messages to the queue\n StorageBuiltInRole.StorageQueueDataContributor)\n .WithHostStorage(storage)\n .WithUrlForEndpoint(\"http\", u => u.DisplayText = \"Functions App\");\n\nbuilder.AddProject(\"frontend\")\n .WithComputeEnvironment(env)\n .WithReference(queues)\n .WithReference(blobs)\n .WaitFor(functions)\n .WithExternalHttpEndpoints()\n .WithUrlForEndpoint(\"https\", u => u.DisplayText = \"Frontend App\")\n .WithUrlForEndpoint(\"http\", u => u.DisplayLocation = UrlDisplayLocation.DetailsOnly);\n\nbuilder.Build().Run();" }, { "name": "aspire-with-javascript", @@ -53,7 +53,7 @@ "thumbnail": "~/assets/samples/aspire-with-javascript/aspire-dashboard.png", "appHost": "csproj", "appHostPath": "AspireJavaScript.AppHost/AppHost.cs", - "appHostCode": "var builder = DistributedApplication.CreateBuilder(args);\n\nvar weatherApi = builder.AddProject(\"weatherapi\")\n .WithExternalHttpEndpoints();\n\nbuilder.AddJavaScriptApp(\"angular\", \"../AspireJavaScript.Angular\", runScriptName: \"start\")\n .WithReference(weatherApi)\n .WaitFor(weatherApi)\n .WithHttpEndpoint(env: \"PORT\")\n .WithExternalHttpEndpoints()\n .PublishAsDockerFile();\n\nbuilder.AddJavaScriptApp(\"react\", \"../AspireJavaScript.React\", runScriptName: \"start\")\n .WithReference(weatherApi)\n .WaitFor(weatherApi)\n .WithEnvironment(\"BROWSER\", \"none\") // Disable opening browser on npm start\n .WithHttpEndpoint(env: \"PORT\")\n .WithExternalHttpEndpoints()\n .PublishAsDockerFile();\n\nbuilder.AddJavaScriptApp(\"vue\", \"../AspireJavaScript.Vue\")\n .WithRunScript(\"start\")\n .WithNpm(installCommand: \"ci\")\n .WithReference(weatherApi)\n .WaitFor(weatherApi)\n .WithHttpEndpoint(env: \"PORT\")\n .WithExternalHttpEndpoints()\n .PublishAsDockerFile();\n\nvar reactVite = builder.AddViteApp(\"reactvite\", \"../AspireJavaScript.Vite\")\n .WithReference(weatherApi)\n .WithEnvironment(\"BROWSER\", \"none\");\n\n// Bundle the output of the Vite app into the wwwroot of the weather API\nweatherApi.PublishWithContainerFiles(reactVite, \"./wwwroot\");\n\nbuilder.Build().Run();" + "appHostCode": "#pragma warning disable ASPIREBROWSERLOGS001\n#pragma warning disable ASPIREJAVASCRIPT001\n\nvar builder = DistributedApplication.CreateBuilder(args);\n\nvar weatherApi = builder.AddProject(\"weatherapi\")\n .WithExternalHttpEndpoints();\n\nbuilder.AddJavaScriptApp(\"angular\", \"../AspireJavaScript.Angular\", runScriptName: \"start\")\n .WithNpm(installCommand: \"ci\")\n .WithReference(weatherApi)\n .WaitFor(weatherApi)\n .WithHttpEndpoint(env: \"PORT\")\n .WithExternalHttpEndpoints()\n .WithBrowserLogs()\n .PublishAsStaticWebsite(\"/api\", weatherApi, options => options.OutputPath = \"dist/weather/browser\");\n\nbuilder.AddJavaScriptApp(\"react\", \"../AspireJavaScript.React\", runScriptName: \"start\")\n .WithNpm(installCommand: \"ci\")\n .WithReference(weatherApi)\n .WaitFor(weatherApi)\n .WithEnvironment(\"BROWSER\", \"none\") // Disable opening browser on npm start\n .WithHttpEndpoint(env: \"PORT\")\n .WithExternalHttpEndpoints()\n .WithBrowserLogs()\n .PublishAsStaticWebsite(\"/api\", weatherApi);\n\nbuilder.AddJavaScriptApp(\"vue\", \"../AspireJavaScript.Vue\")\n .WithRunScript(\"start\")\n .WithNpm(installCommand: \"ci\")\n .WithReference(weatherApi)\n .WaitFor(weatherApi)\n .WithHttpEndpoint(env: \"PORT\")\n .WithExternalHttpEndpoints()\n .WithBrowserLogs()\n .PublishAsStaticWebsite(\"/api\", weatherApi);\n\nbuilder.AddViteApp(\"reactvite\", \"../AspireJavaScript.Vite\")\n .WithNpm(installCommand: \"ci\")\n .WithReference(weatherApi)\n .WithEnvironment(\"BROWSER\", \"none\")\n .WithBrowserLogs()\n .PublishAsStaticWebsite(\"/api\", weatherApi);\n\nbuilder.Build().Run();" }, { "name": "aspire-with-node", @@ -71,7 +71,7 @@ "thumbnail": null, "appHost": "csproj", "appHostPath": "AspireWithNode.AppHost/AppHost.cs", - "appHostCode": "var builder = DistributedApplication.CreateBuilder(args);\n\nvar cache = builder.AddRedis(\"cache\")\n .WithRedisInsight();\n\nvar weatherapi = builder.AddProject(\"weatherapi\")\n .WithHttpHealthCheck(\"/health\");\n\nbuilder.AddNodeApp(\"frontend\", \"../NodeFrontend\", \"./app.js\")\n .WithNpm()\n .WithRunScript(\"dev\")\n .WithHttpEndpoint(port: 5223, env: \"PORT\")\n .WithExternalHttpEndpoints()\n .WithHttpHealthCheck(\"/health\")\n .WithReference(weatherapi).WaitFor(weatherapi)\n .WithReference(cache).WaitFor(cache);\n\nbuilder.Build().Run();" + "appHostCode": "#pragma warning disable ASPIREBROWSERLOGS001\n\nvar builder = DistributedApplication.CreateBuilder(args);\n\nvar cache = builder.AddRedis(\"cache\")\n .WithRedisInsight();\n\nvar weatherapi = builder.AddProject(\"weatherapi\")\n .WithHttpHealthCheck(\"/health\");\n\nbuilder.AddNodeApp(\"frontend\", \"../NodeFrontend\", \"./app.js\")\n .WithNpm()\n .WithRunScript(\"dev\")\n .WithHttpEndpoint(port: 5223, env: \"PORT\")\n .WithExternalHttpEndpoints()\n .WithHttpHealthCheck(\"/health\")\n .WithBrowserLogs()\n .WithReference(weatherapi).WaitFor(weatherapi)\n .WithReference(cache).WaitFor(cache);\n\nbuilder.Build().Run();" }, { "name": "aspire-with-python", @@ -89,7 +89,7 @@ "thumbnail": null, "appHost": "file-based", "appHostPath": "apphost.cs", - "appHostCode": "#:sdk Aspire.AppHost.Sdk@13.4.0\n#:package Aspire.Hosting.JavaScript@13.4.0\n#:package Aspire.Hosting.Python@13.4.0\n#:package Aspire.Hosting.Redis@13.4.0\n\nvar builder = DistributedApplication.CreateBuilder(args);\n\nvar cache = builder.AddRedis(\"cache\");\n\nvar app = builder.AddUvicornApp(\"app\", \"./app\", \"main:app\")\n .WithUv()\n .WithExternalHttpEndpoints()\n .WithReference(cache)\n .WaitFor(cache)\n .WithHttpHealthCheck(\"/health\");\n\nvar frontend = builder.AddViteApp(\"frontend\", \"./frontend\")\n .WithReference(app)\n .WaitFor(app);\n\napp.PublishWithContainerFiles(frontend, \"./static\");\n\nbuilder.Build().Run();" + "appHostCode": "#pragma warning disable ASPIREBROWSERLOGS001\n#pragma warning disable ASPIREJAVASCRIPT001\n\n#:sdk Aspire.AppHost.Sdk@13.4.0\n#:package Aspire.Hosting.JavaScript@13.4.0\n#:package Aspire.Hosting.Browsers@13.4.0-preview.1.26281.18\n#:package Aspire.Hosting.Python@13.4.0\n#:package Aspire.Hosting.Redis@13.4.0\n\nvar builder = DistributedApplication.CreateBuilder(args);\n\nvar cache = builder.AddRedis(\"cache\");\n\nvar app = builder.AddUvicornApp(\"app\", \"./app\", \"main:app\")\n .WithUv()\n .WithExternalHttpEndpoints()\n .WithReference(cache)\n .WaitFor(cache)\n .WithHttpHealthCheck(\"/health\");\n\nbuilder.AddViteApp(\"frontend\", \"./frontend\")\n .WithReference(app)\n .WaitFor(app)\n .WithBrowserLogs()\n .PublishAsStaticWebsite(\"/api\", app);\n\nbuilder.Build().Run();" }, { "name": "client-apps-integration", @@ -113,8 +113,8 @@ "title": "Working with container-built resources in an Aspire application", "description": "This sample demonstrates integrating applications into an Aspire app via Dockerfiles and container-based builds. This is especially helpful to integrate applications written in languages that Aspire does not have a native integration for, or to reduce the prerequisites required to run the application.\n\n\nThe sample integrates a simple app written using [Go](https://go.dev/) and the [Gin Web Framework](https://gin-gonic.com/) by using a [Dockerfile](./ginapp/Dockerfile):\n\n- **ginapp**: This is a simple \"Hello, World\" HTTP API that returns a JSON object like `{ \"message\": \"Hello, World!\" }` from `/` and sends OpenTelemetry instrumentation to the Aspire dashboard.", "href": "https://github.com/microsoft/aspire-samples/tree/main/samples/container-build", - "readme": "# Working with container-built resources in an Aspire application\n\nThis sample demonstrates integrating applications into an Aspire app via Dockerfiles and container-based builds. This is especially helpful to integrate applications written in languages that Aspire does not have a native integration for, or to reduce the prerequisites required to run the application.\n\n![Screenshot of the Aspire dashboard showing the ginapp container resource built from a Dockerfile](~/assets/samples/container-build/aspire-dashboard-container-build.png)\n\nThe sample integrates a simple app written using [Go](https://go.dev/) and the [Gin Web Framework](https://gin-gonic.com/) by using a [Dockerfile](./ginapp/Dockerfile):\n\n- **ginapp**: This is a simple \"Hello, World\" HTTP API that returns a JSON object like `{ \"message\": \"Hello, World!\" }` from `/` and sends OpenTelemetry instrumentation to the Aspire dashboard.\n\n## Development Features\n\nThis sample includes **hot reload** for local development! The Go application uses:\n\n- **Bind mounts** - Your local source code is mounted directly into the container at `/app`\n- **[Air](https://github.com/air-verse/air)** - A live reload tool for Go that watches for file changes and rebuilds automatically\n- **Polling-based file watching** - Configured to work reliably with Docker bind mounts on Windows\n\nWhen you edit any `.go` file in the `ginapp` directory, Air automatically detects the change, rebuilds the Go binary, and restarts the app in just a few seconds\u2014without rebuilding the entire container. This provides a much faster development feedback loop compared to full container rebuilds.\n\n### How it works\n\n- **Development mode** (default): Uses `Dockerfile.dev` with Air for hot reload\n - Source files are bind-mounted from your local machine\n - Changes to `.go` files trigger automatic rebuilds\n - Dependencies are resolved on-demand via `go get`\n\n- **Production mode** (`aspire publish`): Uses the standard `Dockerfile`\n - Multi-stage build for optimized images\n - No bind mounts or development tools\n - Minimal runtime container based on distroless images\n\n## Pre-requisites\n\n- [Aspire development environment](https://aspire.dev/get-started/prerequisites/)\n- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)\n\n## Running the app\n\nIf using the Aspire CLI, run `aspire run` from this directory.\n\nIf using VS Code, open this directory as a workspace and launch the `apphost.cs` file using either the Aspire or C# debuggers.\n\nIf using the .NET CLI, run `dotnet run apphost.cs` from this directory.\n\nFrom the Aspire dashboard, click on the endpoint URL for the `ginapp` resource to see the response in the browser.\n", - "readmeRaw": "# Working with container-built resources in an Aspire application\n\nThis sample demonstrates integrating applications into an Aspire app via Dockerfiles and container-based builds. This is especially helpful to integrate applications written in languages that Aspire does not have a native integration for, or to reduce the prerequisites required to run the application.\n\n![Screenshot of the Aspire dashboard showing the ginapp container resource built from a Dockerfile](./images/aspire-dashboard-container-build.png)\n\nThe sample integrates a simple app written using [Go](https://go.dev/) and the [Gin Web Framework](https://gin-gonic.com/) by using a [Dockerfile](./ginapp/Dockerfile):\n\n- **ginapp**: This is a simple \"Hello, World\" HTTP API that returns a JSON object like `{ \"message\": \"Hello, World!\" }` from `/` and sends OpenTelemetry instrumentation to the Aspire dashboard.\n\n## Development Features\n\nThis sample includes **hot reload** for local development! The Go application uses:\n\n- **Bind mounts** - Your local source code is mounted directly into the container at `/app`\n- **[Air](https://github.com/air-verse/air)** - A live reload tool for Go that watches for file changes and rebuilds automatically\n- **Polling-based file watching** - Configured to work reliably with Docker bind mounts on Windows\n\nWhen you edit any `.go` file in the `ginapp` directory, Air automatically detects the change, rebuilds the Go binary, and restarts the app in just a few seconds\u2014without rebuilding the entire container. This provides a much faster development feedback loop compared to full container rebuilds.\n\n### How it works\n\n- **Development mode** (default): Uses `Dockerfile.dev` with Air for hot reload\n - Source files are bind-mounted from your local machine\n - Changes to `.go` files trigger automatic rebuilds\n - Dependencies are resolved on-demand via `go get`\n\n- **Production mode** (`aspire publish`): Uses the standard `Dockerfile`\n - Multi-stage build for optimized images\n - No bind mounts or development tools\n - Minimal runtime container based on distroless images\n\n## Pre-requisites\n\n- [Aspire development environment](https://aspire.dev/get-started/prerequisites/)\n- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)\n\n## Running the app\n\nIf using the Aspire CLI, run `aspire run` from this directory.\n\nIf using VS Code, open this directory as a workspace and launch the `apphost.cs` file using either the Aspire or C# debuggers.\n\nIf using the .NET CLI, run `dotnet run apphost.cs` from this directory.\n\nFrom the Aspire dashboard, click on the endpoint URL for the `ginapp` resource to see the response in the browser.\n", + "readme": "# Working with container-built resources in an Aspire application\n\nThis sample demonstrates integrating applications into an Aspire app via Dockerfiles and container-based builds. This is especially helpful to integrate applications written in languages that Aspire does not have a native integration for, or to reduce the prerequisites required to run the application.\n\n![Screenshot of the Aspire dashboard showing the ginapp container resource built from a Dockerfile](~/assets/samples/container-build/aspire-dashboard-container-build.png)\n\nThe sample integrates a simple app written using [Go](https://go.dev/) and the [Gin Web Framework](https://gin-gonic.com/) by using a [Dockerfile](./ginapp/Dockerfile):\n\n- **ginapp**: This is a simple \"Hello, World\" HTTP API that returns a JSON object like `{ \"message\": \"Hello, World!\" }` from `/` and sends OpenTelemetry instrumentation to the Aspire dashboard.\n\n## Development Features\n\nThis sample includes **hot reload** for local development! The Go application uses:\n\n- **Bind mounts** - Your local source code is mounted directly into the container at `/app`\n- **[Air](https://github.com/air-verse/air)** - A live reload tool for Go that watches for file changes and rebuilds automatically\n- **Polling-based file watching** - Configured to work reliably with Docker bind mounts on Windows\n\nWhen you edit any `.go` file in the `ginapp` directory, Air automatically detects the change, rebuilds the Go binary, and restarts the app in just a few seconds—without rebuilding the entire container. This provides a much faster development feedback loop compared to full container rebuilds.\n\n### How it works\n\n- **Development mode** (default): Uses `Dockerfile.dev` with Air for hot reload\n - Source files are bind-mounted from your local machine\n - Changes to `.go` files trigger automatic rebuilds\n - Dependencies are resolved on-demand via `go get`\n\n- **Production mode** (`aspire publish`): Uses the standard `Dockerfile`\n - Multi-stage build for optimized images\n - No bind mounts or development tools\n - Minimal runtime container based on distroless images\n\n## Pre-requisites\n\n- [Aspire development environment](https://aspire.dev/get-started/prerequisites/)\n- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)\n\n## Running the app\n\nIf using the Aspire CLI, run `aspire run` from this directory.\n\nIf using VS Code, open this directory as a workspace and launch the `apphost.cs` file using either the Aspire or C# debuggers.\n\nIf using the .NET CLI, run `dotnet run apphost.cs` from this directory.\n\nFrom the Aspire dashboard, click on the endpoint URL for the `ginapp` resource to see the response in the browser.\n", + "readmeRaw": "# Working with container-built resources in an Aspire application\n\nThis sample demonstrates integrating applications into an Aspire app via Dockerfiles and container-based builds. This is especially helpful to integrate applications written in languages that Aspire does not have a native integration for, or to reduce the prerequisites required to run the application.\n\n![Screenshot of the Aspire dashboard showing the ginapp container resource built from a Dockerfile](./images/aspire-dashboard-container-build.png)\n\nThe sample integrates a simple app written using [Go](https://go.dev/) and the [Gin Web Framework](https://gin-gonic.com/) by using a [Dockerfile](./ginapp/Dockerfile):\n\n- **ginapp**: This is a simple \"Hello, World\" HTTP API that returns a JSON object like `{ \"message\": \"Hello, World!\" }` from `/` and sends OpenTelemetry instrumentation to the Aspire dashboard.\n\n## Development Features\n\nThis sample includes **hot reload** for local development! The Go application uses:\n\n- **Bind mounts** - Your local source code is mounted directly into the container at `/app`\n- **[Air](https://github.com/air-verse/air)** - A live reload tool for Go that watches for file changes and rebuilds automatically\n- **Polling-based file watching** - Configured to work reliably with Docker bind mounts on Windows\n\nWhen you edit any `.go` file in the `ginapp` directory, Air automatically detects the change, rebuilds the Go binary, and restarts the app in just a few seconds—without rebuilding the entire container. This provides a much faster development feedback loop compared to full container rebuilds.\n\n### How it works\n\n- **Development mode** (default): Uses `Dockerfile.dev` with Air for hot reload\n - Source files are bind-mounted from your local machine\n - Changes to `.go` files trigger automatic rebuilds\n - Dependencies are resolved on-demand via `go get`\n\n- **Production mode** (`aspire publish`): Uses the standard `Dockerfile`\n - Multi-stage build for optimized images\n - No bind mounts or development tools\n - Minimal runtime container based on distroless images\n\n## Pre-requisites\n\n- [Aspire development environment](https://aspire.dev/get-started/prerequisites/)\n- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)\n\n## Running the app\n\nIf using the Aspire CLI, run `aspire run` from this directory.\n\nIf using VS Code, open this directory as a workspace and launch the `apphost.cs` file using either the Aspire or C# debuggers.\n\nIf using the .NET CLI, run `dotnet run apphost.cs` from this directory.\n\nFrom the Aspire dashboard, click on the endpoint URL for the `ginapp` resource to see the response in the browser.\n", "tags": [ "csharp", "dashboard", @@ -141,15 +141,15 @@ "thumbnail": null, "appHost": "csproj", "appHostPath": "CustomResources.AppHost/AppHost.cs", - "appHostCode": "using CustomResources.AppHost;\n\nvar builder = DistributedApplication.CreateBuilder(args);\n\nbuilder.AddTalkingClock(\"talking-clock\");\n\nbuilder.AddTestResource(\"test\");\n\nbuilder.Build().Run();" + "appHostCode": "using CustomResources.AppHost;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\n\nvar builder = DistributedApplication.CreateBuilder(args);\n\nbuilder.AddTalkingClock(\"talking-clock\");\n\nbuilder.AddTestResource(\"test\");\n\nbuilder.OnBeforeStart(static (@event, cancellationToken) =>\n{\n var logger = @event.Services.GetRequiredService()\n .CreateLogger(\"CustomResources.AppHost\");\n\n logger.LogInformation(\"Starting custom resources sample with {ResourceCount} resources.\", @event.Model.Resources.Count);\n\n return Task.CompletedTask;\n});\n\nbuilder.Eventing.Subscribe(static (@event, cancellationToken) =>\n{\n var logger = @event.Services.GetRequiredService()\n .CreateLogger(\"CustomResources.AppHost\");\n\n logger.LogInformation(\"Custom resources sample created {ResourceCount} resources.\", @event.Model.Resources.Count);\n\n return Task.CompletedTask;\n});\n\nbuilder.Build().Run();" }, { "name": "database-containers", "title": "Working with database containers in an Aspire application", - "description": "This sample demonstrates working with database containers in an Aspire app, using the features of the underlying container image to modify the default database created during container startup. This is especially helpful when not using an ORM like Entity Framework Core that can run migrations on application startup (e.g., [as in the Aspire Shop sample](../AspireShop/AspireShop.CatalogDbManager)) and handle cases when the database configured in the AppHost is not yet created.\n\n\nThe app uses the following database container types:\n\n- [Microsoft SQL Server](https://mcr.microsoft.com/en-us/product/mssql/server/about)\n- [MySQL](https://hub.docker.com/_/mysql)\n- [PostgreSQL](https://hub.docker.com/_/postgres/)\n\nThe app consists of an API service:\n\n- **ContainerDatabases.ApiService**: This is an HTTP API that returns data from each of the configured databases.", + "description": "This sample demonstrates working with database containers in an Aspire app, using the features of the underlying container image to modify the default database created during container startup. This is especially helpful when not using an ORM like Entity Framework Core that can run migrations on application startup (e.g., [as in the Aspire Shop sample](../aspire-shop/AspireShop.CatalogDbManager)) and handle cases when the database configured in the AppHost is not yet created.\n\n\nThe app uses the following database container types:\n\n- [Microsoft SQL Server](https://mcr.microsoft.com/en-us/product/mssql/server/about)\n- [MySQL](https://hub.docker.com/_/mysql)\n- [PostgreSQL](https://hub.docker.com/_/postgres/)\n\nThe app consists of an API service:\n\n- **ContainerDatabases.ApiService**: This is an HTTP API that returns data from each of the configured databases.", "href": "https://github.com/microsoft/aspire-samples/tree/main/samples/database-containers", - "readme": "# Working with database containers in an Aspire application\n\nThis sample demonstrates working with database containers in an Aspire app, using the features of the underlying container image to modify the default database created during container startup. This is especially helpful when not using an ORM like Entity Framework Core that can run migrations on application startup (e.g., [as in the Aspire Shop sample](../AspireShop/AspireShop.CatalogDbManager)) and handle cases when the database configured in the AppHost is not yet created.\n\n![Screenshot of the Swagger UI for the API service that returns data from the configured database containers](~/assets/samples/database-containers/db-containers-apiservice-swagger-ui.png)\n\nThe app uses the following database container types:\n\n- [Microsoft SQL Server](https://mcr.microsoft.com/en-us/product/mssql/server/about)\n- [MySQL](https://hub.docker.com/_/mysql)\n- [PostgreSQL](https://hub.docker.com/_/postgres/)\n\nThe app consists of an API service:\n\n- **ContainerDatabases.ApiService**: This is an HTTP API that returns data from each of the configured databases.\n\n## Prerequisites\n\n- [Aspire development environment](https://aspire.dev/get-started/prerequisites/)\n- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)\n\n## Running the app\n\nIf using the Aspire CLI, run `aspire run` from this directory.\n\nIf using VS Code, open this directory as a workspace and launch the `DatabaseContainers.AppHost` project using either the Aspire or C# debuggers.\n\nIf using Visual Studio, open the solution file `DatabaseContainers.slnx` and launch/debug the `DatabaseContainers.AppHost` project.\n\nIf using the .NET CLI, run `dotnet run` from the `DatabaseContainers.AppHost` directory.\n\nFrom the Aspire dashboard, click on the endpoint URL for the `DatabaseContainers.ApiService` project to launch the Swagger UI for the APIs. You can use the UI to call the APIs and see the results.\n", - "readmeRaw": "# Working with database containers in an Aspire application\n\nThis sample demonstrates working with database containers in an Aspire app, using the features of the underlying container image to modify the default database created during container startup. This is especially helpful when not using an ORM like Entity Framework Core that can run migrations on application startup (e.g., [as in the Aspire Shop sample](../AspireShop/AspireShop.CatalogDbManager)) and handle cases when the database configured in the AppHost is not yet created.\n\n![Screenshot of the Swagger UI for the API service that returns data from the configured database containers](./images/db-containers-apiservice-swagger-ui.png)\n\nThe app uses the following database container types:\n\n- [Microsoft SQL Server](https://mcr.microsoft.com/en-us/product/mssql/server/about)\n- [MySQL](https://hub.docker.com/_/mysql)\n- [PostgreSQL](https://hub.docker.com/_/postgres/)\n\nThe app consists of an API service:\n\n- **ContainerDatabases.ApiService**: This is an HTTP API that returns data from each of the configured databases.\n\n## Prerequisites\n\n- [Aspire development environment](https://aspire.dev/get-started/prerequisites/)\n- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)\n\n## Running the app\n\nIf using the Aspire CLI, run `aspire run` from this directory.\n\nIf using VS Code, open this directory as a workspace and launch the `DatabaseContainers.AppHost` project using either the Aspire or C# debuggers.\n\nIf using Visual Studio, open the solution file `DatabaseContainers.slnx` and launch/debug the `DatabaseContainers.AppHost` project.\n\nIf using the .NET CLI, run `dotnet run` from the `DatabaseContainers.AppHost` directory.\n\nFrom the Aspire dashboard, click on the endpoint URL for the `DatabaseContainers.ApiService` project to launch the Swagger UI for the APIs. You can use the UI to call the APIs and see the results.\n", + "readme": "# Working with database containers in an Aspire application\n\nThis sample demonstrates working with database containers in an Aspire app, using the features of the underlying container image to modify the default database created during container startup. This is especially helpful when not using an ORM like Entity Framework Core that can run migrations on application startup (e.g., [as in the Aspire Shop sample](../aspire-shop/AspireShop.CatalogDbManager)) and handle cases when the database configured in the AppHost is not yet created.\n\n![Screenshot of the Swagger UI for the API service that returns data from the configured database containers](~/assets/samples/database-containers/db-containers-apiservice-swagger-ui.png)\n\nThe app uses the following database container types:\n\n- [Microsoft SQL Server](https://mcr.microsoft.com/en-us/product/mssql/server/about)\n- [MySQL](https://hub.docker.com/_/mysql)\n- [PostgreSQL](https://hub.docker.com/_/postgres/)\n\nThe app consists of an API service:\n\n- **ContainerDatabases.ApiService**: This is an HTTP API that returns data from each of the configured databases.\n\n## Prerequisites\n\n- [Aspire development environment](https://aspire.dev/get-started/prerequisites/)\n- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)\n\n## Running the app\n\nIf using the Aspire CLI, run `aspire run` from this directory.\n\nIf using VS Code, open this directory as a workspace and launch the `DatabaseContainers.AppHost` project using either the Aspire or C# debuggers.\n\nIf using Visual Studio, open the solution file `DatabaseContainers.slnx` and launch/debug the `DatabaseContainers.AppHost` project.\n\nIf using the .NET CLI, run `dotnet run` from the `DatabaseContainers.AppHost` directory.\n\nFrom the Aspire dashboard, click on the endpoint URL for the `DatabaseContainers.ApiService` project to launch the Swagger UI for the APIs. You can use the UI to call the APIs and see the results.\n", + "readmeRaw": "# Working with database containers in an Aspire application\n\nThis sample demonstrates working with database containers in an Aspire app, using the features of the underlying container image to modify the default database created during container startup. This is especially helpful when not using an ORM like Entity Framework Core that can run migrations on application startup (e.g., [as in the Aspire Shop sample](../aspire-shop/AspireShop.CatalogDbManager)) and handle cases when the database configured in the AppHost is not yet created.\n\n![Screenshot of the Swagger UI for the API service that returns data from the configured database containers](./images/db-containers-apiservice-swagger-ui.png)\n\nThe app uses the following database container types:\n\n- [Microsoft SQL Server](https://mcr.microsoft.com/en-us/product/mssql/server/about)\n- [MySQL](https://hub.docker.com/_/mysql)\n- [PostgreSQL](https://hub.docker.com/_/postgres/)\n\nThe app consists of an API service:\n\n- **ContainerDatabases.ApiService**: This is an HTTP API that returns data from each of the configured databases.\n\n## Prerequisites\n\n- [Aspire development environment](https://aspire.dev/get-started/prerequisites/)\n- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)\n\n## Running the app\n\nIf using the Aspire CLI, run `aspire run` from this directory.\n\nIf using VS Code, open this directory as a workspace and launch the `DatabaseContainers.AppHost` project using either the Aspire or C# debuggers.\n\nIf using Visual Studio, open the solution file `DatabaseContainers.slnx` and launch/debug the `DatabaseContainers.AppHost` project.\n\nIf using the .NET CLI, run `dotnet run` from the `DatabaseContainers.AppHost` directory.\n\nFrom the Aspire dashboard, click on the endpoint URL for the `DatabaseContainers.ApiService` project to launch the Swagger UI for the APIs. You can use the UI to call the APIs and see the results.\n", "tags": [ "csharp", "dashboard", @@ -224,15 +224,15 @@ "thumbnail": "~/assets/samples/health-checks-ui/healthchecksui.png", "appHost": "csproj", "appHostPath": "HealthChecksUI.AppHost/AppHost.cs", - "appHostCode": "var builder = DistributedApplication.CreateBuilder(args);\n\nbuilder.AddDockerComposeEnvironment(\"compose\");\n\nvar cache = builder.AddRedis(\"cache\");\n\nvar apiService = builder.AddProject(\"apiservice\")\n .WithHttpHealthCheck(\"/health\")\n .WithHttpProbe(ProbeType.Liveness, \"/alive\")\n .WithFriendlyUrls(displayText: \"API\");\n\nvar webFrontend = builder.AddProject(\"webfrontend\")\n .WithReference(cache)\n .WaitFor(cache)\n .WithReference(apiService)\n .WaitFor(apiService)\n .WithHttpProbe(ProbeType.Liveness, \"/alive\")\n .WithHttpHealthCheck(\"/health\")\n .WithFriendlyUrls(\"Web Frontend\")\n .WithExternalHttpEndpoints();\n\nvar healthChecksUI = builder.AddHealthChecksUI(\"healthchecksui\")\n .WithReference(apiService)\n .WithReference(webFrontend)\n .WithFriendlyUrls(\"HealthChecksUI Dashboard\", \"http\")\n .WithHttpProbe(ProbeType.Liveness, \"/\")\n // This will make the HealthChecksUI dashboard available from external networks when deployed.\n // In a production environment, you should consider adding authentication to the ingress layer\n // to restrict access to the dashboard.\n .WithExternalHttpEndpoints();\n\nif (builder.ExecutionContext.IsRunMode)\n{\n healthChecksUI.WithHostPort(7230);\n}\n\nbuilder.Build().Run();\n\n\nstatic class UrlHelpers\n{\n extension(IResourceBuilder builder) where T : IResource\n {\n public IResourceBuilder WithFriendlyUrls(string? displayText = null, string? endpointName = null, string? path = null)\n {\n return builder.WithUrls(c =>\n {\n List endpointNames = [endpointName, \"https\", \"http\"];\n var endpoint = endpointNames\n .Where(n => n is not null)\n .Select(n => c.GetEndpoint(n!))\n .FirstOrDefault(e => e?.Exists ?? false);\n\n if (endpoint is null) return;\n\n displayText ??= builder.Resource.Name;\n foreach (var url in c.Urls)\n {\n url.DisplayLocation = UrlDisplayLocation.DetailsOnly;\n }\n\n c.Urls.Add(new()\n {\n Endpoint = endpoint,\n DisplayText = displayText,\n DisplayLocation = UrlDisplayLocation.SummaryAndDetails,\n Url = path ?? \"/\"\n });\n\n });\n }\n }\n}" + "appHostCode": "var builder = DistributedApplication.CreateBuilder(args);\n\nvar compose = builder.AddDockerComposeEnvironment(\"compose\");\n\nvar cache = builder.AddRedis(\"cache\")\n .WithComputeEnvironment(compose);\n\nvar apiService = builder.AddProject(\"apiservice\")\n .WithComputeEnvironment(compose)\n .WithHttpHealthCheck(\"/health\")\n .WithHttpProbe(ProbeType.Liveness, \"/alive\")\n .WithFriendlyUrls(displayText: \"API\");\n\nvar webFrontend = builder.AddProject(\"webfrontend\")\n .WithComputeEnvironment(compose)\n .WithReference(cache)\n .WaitFor(cache)\n .WithReference(apiService)\n .WaitFor(apiService)\n .WithHttpProbe(ProbeType.Liveness, \"/alive\")\n .WithHttpHealthCheck(\"/health\")\n .WithFriendlyUrls(\"Web Frontend\")\n .WithExternalHttpEndpoints();\n\nvar healthChecksUI = builder.AddHealthChecksUI(\"healthchecksui\")\n .WithComputeEnvironment(compose)\n .WithReference(apiService)\n .WithReference(webFrontend)\n .WithFriendlyUrls(\"HealthChecksUI Dashboard\", \"http\")\n .WithHttpProbe(ProbeType.Liveness, \"/\")\n // This will make the HealthChecksUI dashboard available from external networks when deployed.\n // In a production environment, you should consider adding authentication to the ingress layer\n // to restrict access to the dashboard.\n .WithExternalHttpEndpoints();\n\nif (builder.ExecutionContext.IsRunMode)\n{\n healthChecksUI.WithHostPort(7230);\n}\n\nbuilder.Build().Run();\n\n\nstatic class UrlHelpers\n{\n extension(IResourceBuilder builder) where T : IResource\n {\n public IResourceBuilder WithFriendlyUrls(string? displayText = null, string? endpointName = null, string? path = null)\n {\n return builder.WithUrls(c =>\n {\n List endpointNames = [endpointName, \"https\", \"http\"];\n var endpoint = endpointNames\n .Where(n => n is not null)\n .Select(n => c.GetEndpoint(n!))\n .FirstOrDefault(e => e?.Exists ?? false);\n\n if (endpoint is null) return;\n\n displayText ??= builder.Resource.Name;\n foreach (var url in c.Urls)\n {\n url.DisplayLocation = UrlDisplayLocation.DetailsOnly;\n }\n\n c.Urls.Add(new()\n {\n Endpoint = endpoint,\n DisplayText = displayText,\n DisplayLocation = UrlDisplayLocation.SummaryAndDetails,\n Url = path ?? \"/\"\n });\n\n });\n }\n }\n}" }, { "name": "image-gallery", "title": "Image Gallery with Event-Triggered Azure Container Apps Jobs", "description": "Upload images to Azure Blob Storage with queue-triggered thumbnail generation. Demonstrates event-driven Container Apps Jobs with queue-based autoscaling, managed identity authentication, and Azure SQL free tier - **can run entirely within Azure free tier limits**.", "href": "https://github.com/microsoft/aspire-samples/tree/main/samples/image-gallery", - "readme": "# Image Gallery with Event-Triggered Azure Container Apps Jobs\n\nUpload images to Azure Blob Storage with queue-triggered thumbnail generation. Demonstrates event-driven Container Apps Jobs with queue-based autoscaling, managed identity authentication, and Azure SQL free tier - **can run entirely within Azure free tier limits**.\n\n## Architecture\n\n**Run Mode:**\n```mermaid\nflowchart LR\n Browser --> Vite[Vite Dev Server
HMR enabled]\n Vite -->|Proxy /api| API[C# API]\n API --> Azurite[Azurite Emulator
Blobs + Queues]\n API --> SQL[SQL Server]\n Worker[Background Worker
Runs continuously] --> Azurite\n Worker --> SQL\n Azurite -.Queue Message.-> Worker\n```\n\n**Publish Mode:**\n```mermaid\nflowchart LR\n Browser --> API[C# API serving
Vite build output
'npm run build']\n API --> Blobs[Azure Blob Storage]\n API --> Queue[Azure Storage Queue]\n API --> SQL[Azure SQL]\n Job[Container Apps Job
Event-triggered
Scales on queue depth] --> Blobs\n Job --> SQL\n Queue -.Queue Trigger.-> Job\n```\n\n## How It Works\n\n### Event-Driven Thumbnail Processing\n\n1. **Upload**: User uploads image \u2192 API saves to Azure Blob Storage and metadata to Azure SQL\n2. **Queue**: API enqueues thumbnail generation message to Azure Storage Queue\n3. **Trigger**: Azure monitors queue depth and automatically starts a Container Apps Job instance\n4. **Process**: Job processes messages in batches (up to 10), generates thumbnails using SkiaSharp\n5. **Scale Down**: After 2 empty polls (~5 seconds), job exits; new instances start automatically when messages arrive\n\n### Local Development vs Production\n\n**Production (Event-Triggered):**\n- Job starts when queue depth > 0, exits within ~5 seconds when empty\n- API and worker both scale to zero when idle\n\n**Local Development (Continuous):**\n- Worker runs continuously, polls every 5 seconds\n- Instant feedback with Azurite emulator and SQL Server container\n\n## What This Demonstrates\n\n- **Event-Driven Jobs**: Container Apps Jobs with queue-based autoscaling rules using Azure.Provisioning APIs\n- **Dual-Mode Resources**: Azurite/SQL Server containers locally, Azure services in production (`.RunAsEmulator()`, `.RunAsContainer()`)\n- **Free Tier Deployment**: Azure SQL free tier with serverless auto-pause, Container Apps scale-to-zero\n- **Managed Identity**: Password-less authentication to all Azure resources (Storage, SQL, Queues)\n- **Polyglot Stack**: Vite+React frontend embedded in C# API container, SkiaSharp for image processing\n- **OpenTelemetry**: Distributed tracing across upload \u2192 queue \u2192 worker pipeline\n\n## Running Locally\n\n```bash\naspire run\n```\n\nNo Azure resources required - uses Azurite emulator and SQL Server container.\n\n## Deploying to Azure\n\n**Prerequisites for deployment:**\n- [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli)\n- [Azure subscription](https://azure.microsoft.com/free/)\n\n**Commands:**\n```bash\naspire run # Run locally with Azurite\naspire publish # Generate Bicep files to explore deployment artifacts (output in ./aspire-output)\naspire deploy # Deploy to Azure Container Apps\n```\n\n## Security Notes\n\n**Implemented:**\n- \u2705 **Managed Identity**: Password-less authentication to all Azure resources (no connection strings or secrets)\n- \u2705 **XSRF Protection**: Antiforgery tokens protect upload/delete endpoints from cross-site request forgery attacks ([docs](https://learn.microsoft.com/aspnet/core/security/anti-request-forgery))\n- \u2705 **Input Validation**: 10 MB file size limit, extension allowlist (.jpg, .jpeg, .png, .gif, .webp), and server-side image byte validation before saving or queueing uploads ([file upload security](https://learn.microsoft.com/aspnet/core/mvc/models/file-uploads))\n- \u2705 **Filename Sanitization**: Path traversal prevention, 255 char limit\n- \u2705 **Resource Limits**: Pagination (max 100 items), retry limits (3 attempts), size checks (20 MB max)\n\n**Not Implemented (Required for Production):**\n- \u274c **Authentication & Authorization**: Endpoints are public - anyone can upload/delete\n- \u274c **Rate Limiting**: No protection against abuse or DoS ([docs](https://learn.microsoft.com/aspnet/core/performance/rate-limit))\n- \u274c **Malware Scanning**: Image byte validation rejects unsupported or malformed images, but production upload workflows should consider malware scanning and the broader [ASP.NET Core security guidance](https://learn.microsoft.com/aspnet/core/security/)\n\n## Key Aspire Patterns\n\n**Azure Storage Emulation** - Automatic Azurite in run mode, real Azure in publish:\n```csharp\nvar storage = builder.AddAzureStorage(\"storage\").RunAsEmulator();\nvar blobs = storage.AddBlobContainer(\"images\");\nvar queues = storage.AddQueues(\"queues\");\n```\n\n**Azure SQL Dual Mode** - SQL Server container locally, Azure SQL free tier in production:\n```csharp\nvar sql = builder.AddAzureSqlServer(\"sql\")\n .RunAsContainer()\n .AddDatabase(\"imagedb\");\n```\n\nDefaults to Azure SQL free tier with serverless auto-pause (SKU: GP_S_Gen5_2).\n\n**Scale to Zero** - API only runs when handling requests:\n```csharp\napi.PublishAsAzureContainerApp((infra, app) =>\n{\n app.Template.Scale.MinReplicas = 0;\n});\n```\n\n**Event-Triggered Container App Job** - Direct control over Azure resources with Azure.Provisioning libraries:\n\nThis example demonstrates using the **Azure.Provisioning** libraries to directly configure low-level Azure resources (Bicep/ARM) from the AppHost. This provides complete flexibility and control when Aspire's higher-level APIs don't expose specific features.\n\n```csharp\nworker.PublishAsAzureContainerAppJob((infra, job) =>\n{\n // Direct access to Azure Provisioning APIs - full control over Bicep/ARM templates\n\n // Get storage account name for queue authentication\n var accountNameParameter = queues.Resource.Parent.NameOutputReference.AsProvisioningParameter(infra);\n\n // Configure event-driven trigger using Container Apps Job APIs\n job.Configuration.TriggerType = ContainerAppJobTriggerType.Event;\n job.Configuration.EventTriggerConfig.Scale.Rules.Add(new ContainerAppJobScaleRule\n {\n Name = \"queue-rule\",\n JobScaleRuleType = \"azure-queue\",\n Metadata = new ObjectExpression(\n // Bicep expressions - referencing other resources dynamically\n new PropertyExpression(\"accountName\", new IdentifierExpression(accountNameParameter.BicepIdentifier)),\n new PropertyExpression(\"queueName\", new StringLiteralExpression(\"thumbnails\")),\n new PropertyExpression(\"queueLength\", new IntLiteralExpression(1)) // Start job when 1+ messages\n ),\n Identity = identityAnnotation.IdentityResource.Id.AsProvisioningParameter(infra) // Use managed identity\n });\n});\n```\n\nThis approach gives you full control over Azure resource configuration without waiting for Aspire abstractions to expose every feature. The C# strongly-typed APIs generate correct Bicep/ARM templates with automatic dependency tracking and parameter passing between resources. You can seamlessly mix high-level Aspire APIs with low-level Azure Provisioning when you need fine-grained control.\n\n**Dual-Mode Worker** - Continuous in run mode, event-triggered in publish mode:\n```csharp\n// AppHost: Set environment variable only in run mode\nif (builder.ExecutionContext.IsRunMode)\n{\n worker = worker.WithEnvironment(\"WORKER_RUN_CONTINUOUSLY\", \"true\");\n}\n\n// Worker: Adapt behavior based on mode\nif (_configuration.GetValue(\"WORKER_RUN_CONTINUOUSLY\"))\n{\n // Local dev: poll every 5 seconds, run forever\n await ExecuteContinuousAsync(stoppingToken);\n}\nelse\n{\n // Production: poll up to 2 times (5s intervals), exit when empty\n // MaxEmptyPolls = 2, EmptyPollWaitSeconds = 5\n await ExecuteScheduledAsync(stoppingToken);\n}\n```\n\n**Graceful Shutdown** - Event-triggered mode always stops, exceptions crash naturally:\n```csharp\nif (_configuration.GetValue(\"WORKER_RUN_CONTINUOUSLY\"))\n{\n // Continuous mode: run forever, let exceptions crash the app\n await ExecuteContinuousAsync(stoppingToken);\n}\nelse\n{\n try\n {\n // Event-triggered mode: process messages until queue empty, then shutdown\n await ExecuteScheduledAsync(stoppingToken);\n }\n finally\n {\n // Always stop application when done (success or exception)\n // New instances will start automatically when queue has messages\n _hostApplicationLifetime.StopApplication();\n }\n}\n```\n\n**Container Files Publishing** - Embed Vite build output in API container:\n```csharp\napi.PublishWithContainerFiles(frontend, \"wwwroot\");\n```\n\n## Performance & Cost Characteristics\n\n**Response Times:**\n- Thumbnail generation: typically ready within seconds of upload\n- Local development: instant feedback with continuous polling\n\n**Cost Optimization:**\n- **Compute**: API and worker scale to zero when idle (~5 seconds), only pay for active processing\n- **SQL**: Free tier with serverless auto-pause (GP_S_Gen5_2), free up to monthly limits\n- **Storage**: Pay only for blob storage used, queues/blobs have minimal costs at low volumes\n- **Result**: Can run entirely within Azure free tier limits\n\n**Further optimization:** Using SAS URLs instead of API blob streaming would eliminate compute/egress costs for serving images\n\n**Scalability:**\n- Parallel job instances spawn automatically for high queue depth\n- Batch processing (up to 10 messages per poll)\n- Managed identity eliminates secrets management overhead\n", - "readmeRaw": "# Image Gallery with Event-Triggered Azure Container Apps Jobs\n\nUpload images to Azure Blob Storage with queue-triggered thumbnail generation. Demonstrates event-driven Container Apps Jobs with queue-based autoscaling, managed identity authentication, and Azure SQL free tier - **can run entirely within Azure free tier limits**.\n\n## Architecture\n\n**Run Mode:**\n```mermaid\nflowchart LR\n Browser --> Vite[Vite Dev Server
HMR enabled]\n Vite -->|Proxy /api| API[C# API]\n API --> Azurite[Azurite Emulator
Blobs + Queues]\n API --> SQL[SQL Server]\n Worker[Background Worker
Runs continuously] --> Azurite\n Worker --> SQL\n Azurite -.Queue Message.-> Worker\n```\n\n**Publish Mode:**\n```mermaid\nflowchart LR\n Browser --> API[C# API serving
Vite build output
'npm run build']\n API --> Blobs[Azure Blob Storage]\n API --> Queue[Azure Storage Queue]\n API --> SQL[Azure SQL]\n Job[Container Apps Job
Event-triggered
Scales on queue depth] --> Blobs\n Job --> SQL\n Queue -.Queue Trigger.-> Job\n```\n\n## How It Works\n\n### Event-Driven Thumbnail Processing\n\n1. **Upload**: User uploads image \u2192 API saves to Azure Blob Storage and metadata to Azure SQL\n2. **Queue**: API enqueues thumbnail generation message to Azure Storage Queue\n3. **Trigger**: Azure monitors queue depth and automatically starts a Container Apps Job instance\n4. **Process**: Job processes messages in batches (up to 10), generates thumbnails using SkiaSharp\n5. **Scale Down**: After 2 empty polls (~5 seconds), job exits; new instances start automatically when messages arrive\n\n### Local Development vs Production\n\n**Production (Event-Triggered):**\n- Job starts when queue depth > 0, exits within ~5 seconds when empty\n- API and worker both scale to zero when idle\n\n**Local Development (Continuous):**\n- Worker runs continuously, polls every 5 seconds\n- Instant feedback with Azurite emulator and SQL Server container\n\n## What This Demonstrates\n\n- **Event-Driven Jobs**: Container Apps Jobs with queue-based autoscaling rules using Azure.Provisioning APIs\n- **Dual-Mode Resources**: Azurite/SQL Server containers locally, Azure services in production (`.RunAsEmulator()`, `.RunAsContainer()`)\n- **Free Tier Deployment**: Azure SQL free tier with serverless auto-pause, Container Apps scale-to-zero\n- **Managed Identity**: Password-less authentication to all Azure resources (Storage, SQL, Queues)\n- **Polyglot Stack**: Vite+React frontend embedded in C# API container, SkiaSharp for image processing\n- **OpenTelemetry**: Distributed tracing across upload \u2192 queue \u2192 worker pipeline\n\n## Running Locally\n\n```bash\naspire run\n```\n\nNo Azure resources required - uses Azurite emulator and SQL Server container.\n\n## Deploying to Azure\n\n**Prerequisites for deployment:**\n- [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli)\n- [Azure subscription](https://azure.microsoft.com/free/)\n\n**Commands:**\n```bash\naspire run # Run locally with Azurite\naspire publish # Generate Bicep files to explore deployment artifacts (output in ./aspire-output)\naspire deploy # Deploy to Azure Container Apps\n```\n\n## Security Notes\n\n**Implemented:**\n- \u2705 **Managed Identity**: Password-less authentication to all Azure resources (no connection strings or secrets)\n- \u2705 **XSRF Protection**: Antiforgery tokens protect upload/delete endpoints from cross-site request forgery attacks ([docs](https://learn.microsoft.com/aspnet/core/security/anti-request-forgery))\n- \u2705 **Input Validation**: 10 MB file size limit, extension allowlist (.jpg, .jpeg, .png, .gif, .webp), and server-side image byte validation before saving or queueing uploads ([file upload security](https://learn.microsoft.com/aspnet/core/mvc/models/file-uploads))\n- \u2705 **Filename Sanitization**: Path traversal prevention, 255 char limit\n- \u2705 **Resource Limits**: Pagination (max 100 items), retry limits (3 attempts), size checks (20 MB max)\n\n**Not Implemented (Required for Production):**\n- \u274c **Authentication & Authorization**: Endpoints are public - anyone can upload/delete\n- \u274c **Rate Limiting**: No protection against abuse or DoS ([docs](https://learn.microsoft.com/aspnet/core/performance/rate-limit))\n- \u274c **Malware Scanning**: Image byte validation rejects unsupported or malformed images, but production upload workflows should consider malware scanning and the broader [ASP.NET Core security guidance](https://learn.microsoft.com/aspnet/core/security/)\n\n## Key Aspire Patterns\n\n**Azure Storage Emulation** - Automatic Azurite in run mode, real Azure in publish:\n```csharp\nvar storage = builder.AddAzureStorage(\"storage\").RunAsEmulator();\nvar blobs = storage.AddBlobContainer(\"images\");\nvar queues = storage.AddQueues(\"queues\");\n```\n\n**Azure SQL Dual Mode** - SQL Server container locally, Azure SQL free tier in production:\n```csharp\nvar sql = builder.AddAzureSqlServer(\"sql\")\n .RunAsContainer()\n .AddDatabase(\"imagedb\");\n```\n\nDefaults to Azure SQL free tier with serverless auto-pause (SKU: GP_S_Gen5_2).\n\n**Scale to Zero** - API only runs when handling requests:\n```csharp\napi.PublishAsAzureContainerApp((infra, app) =>\n{\n app.Template.Scale.MinReplicas = 0;\n});\n```\n\n**Event-Triggered Container App Job** - Direct control over Azure resources with Azure.Provisioning libraries:\n\nThis example demonstrates using the **Azure.Provisioning** libraries to directly configure low-level Azure resources (Bicep/ARM) from the AppHost. This provides complete flexibility and control when Aspire's higher-level APIs don't expose specific features.\n\n```csharp\nworker.PublishAsAzureContainerAppJob((infra, job) =>\n{\n // Direct access to Azure Provisioning APIs - full control over Bicep/ARM templates\n\n // Get storage account name for queue authentication\n var accountNameParameter = queues.Resource.Parent.NameOutputReference.AsProvisioningParameter(infra);\n\n // Configure event-driven trigger using Container Apps Job APIs\n job.Configuration.TriggerType = ContainerAppJobTriggerType.Event;\n job.Configuration.EventTriggerConfig.Scale.Rules.Add(new ContainerAppJobScaleRule\n {\n Name = \"queue-rule\",\n JobScaleRuleType = \"azure-queue\",\n Metadata = new ObjectExpression(\n // Bicep expressions - referencing other resources dynamically\n new PropertyExpression(\"accountName\", new IdentifierExpression(accountNameParameter.BicepIdentifier)),\n new PropertyExpression(\"queueName\", new StringLiteralExpression(\"thumbnails\")),\n new PropertyExpression(\"queueLength\", new IntLiteralExpression(1)) // Start job when 1+ messages\n ),\n Identity = identityAnnotation.IdentityResource.Id.AsProvisioningParameter(infra) // Use managed identity\n });\n});\n```\n\nThis approach gives you full control over Azure resource configuration without waiting for Aspire abstractions to expose every feature. The C# strongly-typed APIs generate correct Bicep/ARM templates with automatic dependency tracking and parameter passing between resources. You can seamlessly mix high-level Aspire APIs with low-level Azure Provisioning when you need fine-grained control.\n\n**Dual-Mode Worker** - Continuous in run mode, event-triggered in publish mode:\n```csharp\n// AppHost: Set environment variable only in run mode\nif (builder.ExecutionContext.IsRunMode)\n{\n worker = worker.WithEnvironment(\"WORKER_RUN_CONTINUOUSLY\", \"true\");\n}\n\n// Worker: Adapt behavior based on mode\nif (_configuration.GetValue(\"WORKER_RUN_CONTINUOUSLY\"))\n{\n // Local dev: poll every 5 seconds, run forever\n await ExecuteContinuousAsync(stoppingToken);\n}\nelse\n{\n // Production: poll up to 2 times (5s intervals), exit when empty\n // MaxEmptyPolls = 2, EmptyPollWaitSeconds = 5\n await ExecuteScheduledAsync(stoppingToken);\n}\n```\n\n**Graceful Shutdown** - Event-triggered mode always stops, exceptions crash naturally:\n```csharp\nif (_configuration.GetValue(\"WORKER_RUN_CONTINUOUSLY\"))\n{\n // Continuous mode: run forever, let exceptions crash the app\n await ExecuteContinuousAsync(stoppingToken);\n}\nelse\n{\n try\n {\n // Event-triggered mode: process messages until queue empty, then shutdown\n await ExecuteScheduledAsync(stoppingToken);\n }\n finally\n {\n // Always stop application when done (success or exception)\n // New instances will start automatically when queue has messages\n _hostApplicationLifetime.StopApplication();\n }\n}\n```\n\n**Container Files Publishing** - Embed Vite build output in API container:\n```csharp\napi.PublishWithContainerFiles(frontend, \"wwwroot\");\n```\n\n## Performance & Cost Characteristics\n\n**Response Times:**\n- Thumbnail generation: typically ready within seconds of upload\n- Local development: instant feedback with continuous polling\n\n**Cost Optimization:**\n- **Compute**: API and worker scale to zero when idle (~5 seconds), only pay for active processing\n- **SQL**: Free tier with serverless auto-pause (GP_S_Gen5_2), free up to monthly limits\n- **Storage**: Pay only for blob storage used, queues/blobs have minimal costs at low volumes\n- **Result**: Can run entirely within Azure free tier limits\n\n**Further optimization:** Using SAS URLs instead of API blob streaming would eliminate compute/egress costs for serving images\n\n**Scalability:**\n- Parallel job instances spawn automatically for high queue depth\n- Batch processing (up to 10 messages per poll)\n- Managed identity eliminates secrets management overhead\n", + "readme": "# Image Gallery with Event-Triggered Azure Container Apps Jobs\n\nUpload images to Azure Blob Storage with queue-triggered thumbnail generation. Demonstrates event-driven Container Apps Jobs with queue-based autoscaling, managed identity authentication, and Azure SQL free tier - **can run entirely within Azure free tier limits**.\n\n## Architecture\n\n**Run Mode:**\n```mermaid\nflowchart LR\n Browser --> Vite[Vite Dev Server
HMR enabled]\n Vite -->|Proxy /api| API[C# API]\n API --> Azurite[Azurite Emulator
Blobs + Queues]\n API --> SQL[SQL Server]\n Worker[Background Worker
Runs continuously] --> Azurite\n Worker --> SQL\n Azurite -.Queue Message.-> Worker\n```\n\n**Publish Mode:**\n```mermaid\nflowchart LR\n Browser --> Frontend[Static website serving
Vite build output
'npm run build']\n Frontend -->|Proxy /api| API[C# API]\n API --> Blobs[Azure Blob Storage]\n API --> Queue[Azure Storage Queue]\n API --> SQL[Azure SQL]\n Job[Container Apps Job
Event-triggered
Scales on queue depth] --> Blobs\n Job --> SQL\n Queue -.Queue Trigger.-> Job\n```\n\n## How It Works\n\n### Event-Driven Thumbnail Processing\n\n1. **Upload**: User uploads image → API saves to Azure Blob Storage and metadata to Azure SQL\n2. **Queue**: API enqueues thumbnail generation message to Azure Storage Queue\n3. **Trigger**: Azure monitors queue depth and automatically starts a Container Apps Job instance\n4. **Process**: Job processes messages in batches (up to 10), generates thumbnails using SkiaSharp\n5. **Scale Down**: After 2 empty polls (~5 seconds), job exits; new instances start automatically when messages arrive\n\n### Local Development vs Production\n\n**Production (Event-Triggered):**\n- Job starts when queue depth > 0, exits within ~5 seconds when empty\n- API and worker both scale to zero when idle\n\n**Local Development (Continuous):**\n- Worker runs continuously, polls every 5 seconds\n- Instant feedback with Azurite emulator and SQL Server container\n\n## What This Demonstrates\n\n- **Event-Driven Jobs**: Container Apps Jobs with queue-based autoscaling rules using Azure.Provisioning APIs\n- **Dual-Mode Resources**: Azurite/SQL Server containers locally, Azure services in production (`.RunAsEmulator()`, `.RunAsContainer()`)\n- **Free Tier Deployment**: Azure SQL free tier with serverless auto-pause, Container Apps scale-to-zero\n- **Managed Identity**: Password-less authentication to all Azure resources (Storage, SQL, Queues)\n- **Polyglot Stack**: Vite+React frontend published as a static website, SkiaSharp for image processing\n- **OpenTelemetry**: Distributed tracing across upload → queue → worker pipeline\n\n## Running Locally\n\n```bash\naspire run\n```\n\nNo Azure resources required - uses Azurite emulator and SQL Server container.\n\n## Deploying to Azure\n\n**Prerequisites for deployment:**\n- [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli)\n- [Azure subscription](https://azure.microsoft.com/free/)\n\n**Commands:**\n```bash\naspire run # Run locally with Azurite\naspire publish # Generate Bicep files to explore deployment artifacts (output in ./aspire-output)\naspire deploy # Deploy to Azure Container Apps\n```\n\n## Security Notes\n\n**Implemented:**\n- ✅ **Managed Identity**: Password-less authentication to all Azure resources (no connection strings or secrets)\n- ✅ **XSRF Protection**: Antiforgery tokens protect upload/delete endpoints from cross-site request forgery attacks ([docs](https://learn.microsoft.com/aspnet/core/security/anti-request-forgery))\n- ✅ **Input Validation**: 10 MB file size limit, extension allowlist (.jpg, .jpeg, .png, .gif, .webp), and server-side image byte validation before saving or queueing uploads ([file upload security](https://learn.microsoft.com/aspnet/core/mvc/models/file-uploads))\n- ✅ **Filename Sanitization**: Path traversal prevention, 255 char limit\n- ✅ **Resource Limits**: Pagination (max 100 items), retry limits (3 attempts), size checks (20 MB max)\n\n**Not Implemented (Required for Production):**\n- ❌ **Authentication & Authorization**: Endpoints are public - anyone can upload/delete\n- ❌ **Rate Limiting**: No protection against abuse or DoS ([docs](https://learn.microsoft.com/aspnet/core/performance/rate-limit))\n- ❌ **Malware Scanning**: Image byte validation rejects unsupported or malformed images, but production upload workflows should consider malware scanning and the broader [ASP.NET Core security guidance](https://learn.microsoft.com/aspnet/core/security/)\n\n## Key Aspire Patterns\n\n**Azure Storage Emulation** - Automatic Azurite in run mode, real Azure in publish:\n```csharp\nvar storage = builder.AddAzureStorage(\"storage\").RunAsEmulator();\nvar blobs = storage.AddBlobContainer(\"images\");\nvar queues = storage.AddQueues(\"queues\");\n```\n\n**Azure SQL Dual Mode** - SQL Server container locally, Azure SQL free tier in production:\n```csharp\nvar sql = builder.AddAzureSqlServer(\"sql\")\n .RunAsContainer()\n .AddDatabase(\"imagedb\");\n```\n\nDefaults to Azure SQL free tier with serverless auto-pause (SKU: GP_S_Gen5_2).\n\n**Scale to Zero** - API only runs when handling requests:\n```csharp\napi.PublishAsAzureContainerApp((infra, app) =>\n{\n app.Template.Scale.MinReplicas = 0;\n});\n```\n\n**Event-Triggered Container App Job** - Direct control over Azure resources with Azure.Provisioning libraries:\n\nThis example demonstrates using the **Azure.Provisioning** libraries to directly configure low-level Azure resources (Bicep/ARM) from the AppHost. This provides complete flexibility and control when Aspire's higher-level APIs don't expose specific features.\n\n```csharp\nworker.PublishAsAzureContainerAppJob((infra, job) =>\n{\n // Direct access to Azure Provisioning APIs - full control over Bicep/ARM templates\n\n // Get storage account name for queue authentication\n var accountNameParameter = queues.Resource.Parent.NameOutputReference.AsProvisioningParameter(infra);\n\n // Configure event-driven trigger using Container Apps Job APIs\n job.Configuration.TriggerType = ContainerAppJobTriggerType.Event;\n job.Configuration.EventTriggerConfig.Scale.Rules.Add(new ContainerAppJobScaleRule\n {\n Name = \"queue-rule\",\n JobScaleRuleType = \"azure-queue\",\n Metadata = new ObjectExpression(\n // Bicep expressions - referencing other resources dynamically\n new PropertyExpression(\"accountName\", new IdentifierExpression(accountNameParameter.BicepIdentifier)),\n new PropertyExpression(\"queueName\", new StringLiteralExpression(\"thumbnails\")),\n new PropertyExpression(\"queueLength\", new IntLiteralExpression(1)) // Start job when 1+ messages\n ),\n Identity = identityAnnotation.IdentityResource.Id.AsProvisioningParameter(infra) // Use managed identity\n });\n});\n```\n\nThis approach gives you full control over Azure resource configuration without waiting for Aspire abstractions to expose every feature. The C# strongly-typed APIs generate correct Bicep/ARM templates with automatic dependency tracking and parameter passing between resources. You can seamlessly mix high-level Aspire APIs with low-level Azure Provisioning when you need fine-grained control.\n\n**Dual-Mode Worker** - Continuous in run mode, event-triggered in publish mode:\n```csharp\n// AppHost: Set environment variable only in run mode\nif (builder.ExecutionContext.IsRunMode)\n{\n worker = worker.WithEnvironment(\"WORKER_RUN_CONTINUOUSLY\", \"true\");\n}\n\n// Worker: Adapt behavior based on mode\nif (_configuration.GetValue(\"WORKER_RUN_CONTINUOUSLY\"))\n{\n // Local dev: poll every 5 seconds, run forever\n await ExecuteContinuousAsync(stoppingToken);\n}\nelse\n{\n // Production: poll up to 2 times (5s intervals), exit when empty\n // MaxEmptyPolls = 2, EmptyPollWaitSeconds = 5\n await ExecuteScheduledAsync(stoppingToken);\n}\n```\n\n**Graceful Shutdown** - Event-triggered mode always stops, exceptions crash naturally:\n```csharp\nif (_configuration.GetValue(\"WORKER_RUN_CONTINUOUSLY\"))\n{\n // Continuous mode: run forever, let exceptions crash the app\n await ExecuteContinuousAsync(stoppingToken);\n}\nelse\n{\n try\n {\n // Event-triggered mode: process messages until queue empty, then shutdown\n await ExecuteScheduledAsync(stoppingToken);\n }\n finally\n {\n // Always stop application when done (success or exception)\n // New instances will start automatically when queue has messages\n _hostApplicationLifetime.StopApplication();\n }\n}\n```\n\n**Static Website Publishing** - Publish the Vite app as a static website with API proxying:\n```csharp\nfrontend.PublishAsStaticWebsite(\"/api\", api);\n```\n\n## Performance & Cost Characteristics\n\n**Response Times:**\n- Thumbnail generation: typically ready within seconds of upload\n- Local development: instant feedback with continuous polling\n\n**Cost Optimization:**\n- **Compute**: API and worker scale to zero when idle (~5 seconds), only pay for active processing\n- **SQL**: Free tier with serverless auto-pause (GP_S_Gen5_2), free up to monthly limits\n- **Storage**: Pay only for blob storage used, queues/blobs have minimal costs at low volumes\n- **Result**: Can run entirely within Azure free tier limits\n\n**Further optimization:** Using SAS URLs instead of API blob streaming would eliminate compute/egress costs for serving images\n\n**Scalability:**\n- Parallel job instances spawn automatically for high queue depth\n- Batch processing (up to 10 messages per poll)\n- Managed identity eliminates secrets management overhead\n", + "readmeRaw": "# Image Gallery with Event-Triggered Azure Container Apps Jobs\n\nUpload images to Azure Blob Storage with queue-triggered thumbnail generation. Demonstrates event-driven Container Apps Jobs with queue-based autoscaling, managed identity authentication, and Azure SQL free tier - **can run entirely within Azure free tier limits**.\n\n## Architecture\n\n**Run Mode:**\n```mermaid\nflowchart LR\n Browser --> Vite[Vite Dev Server
HMR enabled]\n Vite -->|Proxy /api| API[C# API]\n API --> Azurite[Azurite Emulator
Blobs + Queues]\n API --> SQL[SQL Server]\n Worker[Background Worker
Runs continuously] --> Azurite\n Worker --> SQL\n Azurite -.Queue Message.-> Worker\n```\n\n**Publish Mode:**\n```mermaid\nflowchart LR\n Browser --> Frontend[Static website serving
Vite build output
'npm run build']\n Frontend -->|Proxy /api| API[C# API]\n API --> Blobs[Azure Blob Storage]\n API --> Queue[Azure Storage Queue]\n API --> SQL[Azure SQL]\n Job[Container Apps Job
Event-triggered
Scales on queue depth] --> Blobs\n Job --> SQL\n Queue -.Queue Trigger.-> Job\n```\n\n## How It Works\n\n### Event-Driven Thumbnail Processing\n\n1. **Upload**: User uploads image → API saves to Azure Blob Storage and metadata to Azure SQL\n2. **Queue**: API enqueues thumbnail generation message to Azure Storage Queue\n3. **Trigger**: Azure monitors queue depth and automatically starts a Container Apps Job instance\n4. **Process**: Job processes messages in batches (up to 10), generates thumbnails using SkiaSharp\n5. **Scale Down**: After 2 empty polls (~5 seconds), job exits; new instances start automatically when messages arrive\n\n### Local Development vs Production\n\n**Production (Event-Triggered):**\n- Job starts when queue depth > 0, exits within ~5 seconds when empty\n- API and worker both scale to zero when idle\n\n**Local Development (Continuous):**\n- Worker runs continuously, polls every 5 seconds\n- Instant feedback with Azurite emulator and SQL Server container\n\n## What This Demonstrates\n\n- **Event-Driven Jobs**: Container Apps Jobs with queue-based autoscaling rules using Azure.Provisioning APIs\n- **Dual-Mode Resources**: Azurite/SQL Server containers locally, Azure services in production (`.RunAsEmulator()`, `.RunAsContainer()`)\n- **Free Tier Deployment**: Azure SQL free tier with serverless auto-pause, Container Apps scale-to-zero\n- **Managed Identity**: Password-less authentication to all Azure resources (Storage, SQL, Queues)\n- **Polyglot Stack**: Vite+React frontend published as a static website, SkiaSharp for image processing\n- **OpenTelemetry**: Distributed tracing across upload → queue → worker pipeline\n\n## Running Locally\n\n```bash\naspire run\n```\n\nNo Azure resources required - uses Azurite emulator and SQL Server container.\n\n## Deploying to Azure\n\n**Prerequisites for deployment:**\n- [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli)\n- [Azure subscription](https://azure.microsoft.com/free/)\n\n**Commands:**\n```bash\naspire run # Run locally with Azurite\naspire publish # Generate Bicep files to explore deployment artifacts (output in ./aspire-output)\naspire deploy # Deploy to Azure Container Apps\n```\n\n## Security Notes\n\n**Implemented:**\n- ✅ **Managed Identity**: Password-less authentication to all Azure resources (no connection strings or secrets)\n- ✅ **XSRF Protection**: Antiforgery tokens protect upload/delete endpoints from cross-site request forgery attacks ([docs](https://learn.microsoft.com/aspnet/core/security/anti-request-forgery))\n- ✅ **Input Validation**: 10 MB file size limit, extension allowlist (.jpg, .jpeg, .png, .gif, .webp), and server-side image byte validation before saving or queueing uploads ([file upload security](https://learn.microsoft.com/aspnet/core/mvc/models/file-uploads))\n- ✅ **Filename Sanitization**: Path traversal prevention, 255 char limit\n- ✅ **Resource Limits**: Pagination (max 100 items), retry limits (3 attempts), size checks (20 MB max)\n\n**Not Implemented (Required for Production):**\n- ❌ **Authentication & Authorization**: Endpoints are public - anyone can upload/delete\n- ❌ **Rate Limiting**: No protection against abuse or DoS ([docs](https://learn.microsoft.com/aspnet/core/performance/rate-limit))\n- ❌ **Malware Scanning**: Image byte validation rejects unsupported or malformed images, but production upload workflows should consider malware scanning and the broader [ASP.NET Core security guidance](https://learn.microsoft.com/aspnet/core/security/)\n\n## Key Aspire Patterns\n\n**Azure Storage Emulation** - Automatic Azurite in run mode, real Azure in publish:\n```csharp\nvar storage = builder.AddAzureStorage(\"storage\").RunAsEmulator();\nvar blobs = storage.AddBlobContainer(\"images\");\nvar queues = storage.AddQueues(\"queues\");\n```\n\n**Azure SQL Dual Mode** - SQL Server container locally, Azure SQL free tier in production:\n```csharp\nvar sql = builder.AddAzureSqlServer(\"sql\")\n .RunAsContainer()\n .AddDatabase(\"imagedb\");\n```\n\nDefaults to Azure SQL free tier with serverless auto-pause (SKU: GP_S_Gen5_2).\n\n**Scale to Zero** - API only runs when handling requests:\n```csharp\napi.PublishAsAzureContainerApp((infra, app) =>\n{\n app.Template.Scale.MinReplicas = 0;\n});\n```\n\n**Event-Triggered Container App Job** - Direct control over Azure resources with Azure.Provisioning libraries:\n\nThis example demonstrates using the **Azure.Provisioning** libraries to directly configure low-level Azure resources (Bicep/ARM) from the AppHost. This provides complete flexibility and control when Aspire's higher-level APIs don't expose specific features.\n\n```csharp\nworker.PublishAsAzureContainerAppJob((infra, job) =>\n{\n // Direct access to Azure Provisioning APIs - full control over Bicep/ARM templates\n\n // Get storage account name for queue authentication\n var accountNameParameter = queues.Resource.Parent.NameOutputReference.AsProvisioningParameter(infra);\n\n // Configure event-driven trigger using Container Apps Job APIs\n job.Configuration.TriggerType = ContainerAppJobTriggerType.Event;\n job.Configuration.EventTriggerConfig.Scale.Rules.Add(new ContainerAppJobScaleRule\n {\n Name = \"queue-rule\",\n JobScaleRuleType = \"azure-queue\",\n Metadata = new ObjectExpression(\n // Bicep expressions - referencing other resources dynamically\n new PropertyExpression(\"accountName\", new IdentifierExpression(accountNameParameter.BicepIdentifier)),\n new PropertyExpression(\"queueName\", new StringLiteralExpression(\"thumbnails\")),\n new PropertyExpression(\"queueLength\", new IntLiteralExpression(1)) // Start job when 1+ messages\n ),\n Identity = identityAnnotation.IdentityResource.Id.AsProvisioningParameter(infra) // Use managed identity\n });\n});\n```\n\nThis approach gives you full control over Azure resource configuration without waiting for Aspire abstractions to expose every feature. The C# strongly-typed APIs generate correct Bicep/ARM templates with automatic dependency tracking and parameter passing between resources. You can seamlessly mix high-level Aspire APIs with low-level Azure Provisioning when you need fine-grained control.\n\n**Dual-Mode Worker** - Continuous in run mode, event-triggered in publish mode:\n```csharp\n// AppHost: Set environment variable only in run mode\nif (builder.ExecutionContext.IsRunMode)\n{\n worker = worker.WithEnvironment(\"WORKER_RUN_CONTINUOUSLY\", \"true\");\n}\n\n// Worker: Adapt behavior based on mode\nif (_configuration.GetValue(\"WORKER_RUN_CONTINUOUSLY\"))\n{\n // Local dev: poll every 5 seconds, run forever\n await ExecuteContinuousAsync(stoppingToken);\n}\nelse\n{\n // Production: poll up to 2 times (5s intervals), exit when empty\n // MaxEmptyPolls = 2, EmptyPollWaitSeconds = 5\n await ExecuteScheduledAsync(stoppingToken);\n}\n```\n\n**Graceful Shutdown** - Event-triggered mode always stops, exceptions crash naturally:\n```csharp\nif (_configuration.GetValue(\"WORKER_RUN_CONTINUOUSLY\"))\n{\n // Continuous mode: run forever, let exceptions crash the app\n await ExecuteContinuousAsync(stoppingToken);\n}\nelse\n{\n try\n {\n // Event-triggered mode: process messages until queue empty, then shutdown\n await ExecuteScheduledAsync(stoppingToken);\n }\n finally\n {\n // Always stop application when done (success or exception)\n // New instances will start automatically when queue has messages\n _hostApplicationLifetime.StopApplication();\n }\n}\n```\n\n**Static Website Publishing** - Publish the Vite app as a static website with API proxying:\n```csharp\nfrontend.PublishAsStaticWebsite(\"/api\", api);\n```\n\n## Performance & Cost Characteristics\n\n**Response Times:**\n- Thumbnail generation: typically ready within seconds of upload\n- Local development: instant feedback with continuous polling\n\n**Cost Optimization:**\n- **Compute**: API and worker scale to zero when idle (~5 seconds), only pay for active processing\n- **SQL**: Free tier with serverless auto-pause (GP_S_Gen5_2), free up to monthly limits\n- **Storage**: Pay only for blob storage used, queues/blobs have minimal costs at low volumes\n- **Result**: Can run entirely within Azure free tier limits\n\n**Further optimization:** Using SAS URLs instead of API blob streaming would eliminate compute/egress costs for serving images\n\n**Scalability:**\n- Parallel job instances spawn automatically for high queue depth\n- Batch processing (up to 10 messages per poll)\n- Managed identity eliminates secrets management overhead\n", "tags": [ "azure", "azure-storage", @@ -244,7 +244,7 @@ "thumbnail": null, "appHost": "file-based", "appHostPath": "apphost.cs", - "appHostCode": "#pragma warning disable ASPIRECSHARPAPPS001\n#pragma warning disable ASPIREAZURE002\n\n#:sdk Aspire.AppHost.Sdk@13.4.0\n#:package Aspire.Hosting.Azure.Storage@13.4.0\n#:package Aspire.Hosting.Azure.Sql@13.4.0\n#:package Aspire.Hosting.JavaScript@13.4.0\n#:package Aspire.Hosting.Azure.AppContainers@13.4.0\n\nusing Aspire.Hosting.Azure;\nusing Azure.Provisioning.AppContainers;\nusing Azure.Provisioning.Expressions;\n\nvar builder = DistributedApplication.CreateBuilder(args);\n\nbuilder.AddAzureContainerAppEnvironment(\"env\");\n\n// Storage: Use Azurite emulator in run mode, real Azure in publish mode\nvar storage = builder.AddAzureStorage(\"storage\")\n .RunAsEmulator();\n\nvar blobs = storage.AddBlobContainer(\"images\");\nvar queues = storage.AddQueues(\"queues\");\n\n// Azure SQL Database\nvar sql = builder.AddAzureSqlServer(\"sql\")\n .RunAsContainer(c => c.WithLifetime(ContainerLifetime.Persistent))\n .AddDatabase(\"imagedb\");\n\n// API: Upload images, queue thumbnail jobs, serve metadata\nvar api = builder.AddCSharpApp(\"api\", \"./api\")\n .WithHttpHealthCheck(\"/health\")\n .WithExternalHttpEndpoints()\n .WaitFor(sql)\n .WithReference(blobs)\n .WithReference(queues)\n .WithReference(sql)\n .WithUrls(context =>\n {\n foreach (var u in context.Urls)\n {\n u.DisplayLocation = UrlDisplayLocation.DetailsOnly;\n }\n\n context.Urls.Add(new()\n {\n Url = \"/scalar\",\n DisplayText = \"API Reference\",\n Endpoint = context.GetEndpoint(\"https\")\n });\n })\n .PublishAsAzureContainerApp((infra, app) =>\n {\n // Scale to zero when idle\n app.Template.Scale.MinReplicas = 0;\n });\n\n// Worker: Container Apps Job for queue-triggered thumbnail generation\n// Event-driven: starts when messages arrive, exits within ~5 seconds when queue is empty\nvar worker = builder.AddCSharpApp(\"worker\", \"./worker\")\n .WithReference(blobs)\n .WithReference(queues)\n .WithReference(sql)\n .WaitFor(sql)\n .WaitFor(queues);\n\nif (builder.ExecutionContext.IsRunMode)\n{\n // In run mode, keep worker running continuously for fast local development\n worker = worker.WithEnvironment(\"WORKER_RUN_CONTINUOUSLY\", \"true\");\n}\nelse\n{\n // In publish mode, use event-driven scaling based on queue depth\n worker.PublishAsAzureContainerAppJob((infra, job) =>\n {\n var accountNameParameter = queues.Resource.Parent.NameOutputReference.AsProvisioningParameter(infra);\n\n // Resolve the identity annotation added to the worker app\n if (!worker.Resource.TryGetLastAnnotation(out var identityAnnotation))\n {\n throw new InvalidOperationException(\"Identity annotation not found.\");\n }\n\n job.Configuration.TriggerType = ContainerAppJobTriggerType.Event;\n job.Configuration.EventTriggerConfig.Scale.PollingIntervalInSeconds = 1;\n job.Configuration.EventTriggerConfig.Scale.Rules.Add(new ContainerAppJobScaleRule\n {\n Name = \"queue-rule\",\n JobScaleRuleType = \"azure-queue\",\n Metadata = new ObjectExpression(\n new PropertyExpression(\"accountName\", new IdentifierExpression(accountNameParameter.BicepIdentifier)),\n new PropertyExpression(\"queueName\", new StringLiteralExpression(\"thumbnails\")),\n new PropertyExpression(\"queueLength\", new IntLiteralExpression(1))\n ),\n Identity = identityAnnotation.IdentityResource.Id.AsProvisioningParameter(infra)\n });\n });\n}\n\n// Frontend: Vite+React for upload and gallery UI\nvar frontend = builder.AddViteApp(\"frontend\", \"./frontend\")\n .WithEndpoint(\"http\", e => e.Port = 9080)\n .WithReference(api)\n .WithUrl(\"\", \"Image Gallery\");\n\n// Publish: Embed frontend build output in API container\napi.PublishWithContainerFiles(frontend, \"wwwroot\");\n\nbuilder.Build().Run();" + "appHostCode": "#pragma warning disable ASPIRECSHARPAPPS001\n#pragma warning disable ASPIREAZURE002\n#pragma warning disable ASPIREBROWSERLOGS001\n#pragma warning disable ASPIREJAVASCRIPT001\n\n#:sdk Aspire.AppHost.Sdk@13.4.0\n#:package Aspire.Hosting.Azure.Storage@13.4.0\n#:package Aspire.Hosting.Azure.Sql@13.4.0\n#:package Aspire.Hosting.JavaScript@13.4.0\n#:package Aspire.Hosting.Browsers@13.4.0-preview.1.26281.18\n#:package Aspire.Hosting.Azure.AppContainers@13.4.0\n\nusing Aspire.Hosting.Azure;\nusing Azure.Provisioning.AppContainers;\nusing Azure.Provisioning.Expressions;\n\nvar builder = DistributedApplication.CreateBuilder(args);\n\nvar env = builder.AddAzureContainerAppEnvironment(\"env\");\n\n// Storage: Use Azurite emulator in run mode, real Azure in publish mode\nvar storage = builder.AddAzureStorage(\"storage\")\n .RunAsEmulator();\n\nvar blobs = storage.AddBlobContainer(\"images\");\nvar queues = storage.AddQueues(\"queues\");\n\n// Azure SQL Database\nvar sql = builder.AddAzureSqlServer(\"sql\")\n .RunAsContainer(c => c.WithLifetime(ContainerLifetime.Persistent))\n .AddDatabase(\"imagedb\");\n\n// API: Upload images, queue thumbnail jobs, serve metadata\nvar api = builder.AddCSharpApp(\"api\", \"./api\")\n .WithComputeEnvironment(env)\n .WithHttpHealthCheck(\"/health\")\n .WithExternalHttpEndpoints()\n .WaitFor(sql)\n .WithReference(blobs)\n .WithReference(queues)\n .WithReference(sql)\n .WithUrls(context =>\n {\n foreach (var u in context.Urls)\n {\n u.DisplayLocation = UrlDisplayLocation.DetailsOnly;\n }\n\n context.Urls.Add(new()\n {\n Url = \"/scalar\",\n DisplayText = \"API Reference\",\n Endpoint = context.GetEndpoint(\"https\")\n });\n })\n .PublishAsAzureContainerApp((infra, app) =>\n {\n // Scale to zero when idle\n app.Template.Scale.MinReplicas = 0;\n });\n\n// Worker: Container Apps Job for queue-triggered thumbnail generation\n// Event-driven: starts when messages arrive, exits within ~5 seconds when queue is empty\nvar worker = builder.AddCSharpApp(\"worker\", \"./worker\")\n .WithComputeEnvironment(env)\n .WithReference(blobs)\n .WithReference(queues)\n .WithReference(sql)\n .WaitFor(sql)\n .WaitFor(queues);\n\nif (builder.ExecutionContext.IsRunMode)\n{\n // In run mode, keep worker running continuously for fast local development\n worker = worker.WithEnvironment(\"WORKER_RUN_CONTINUOUSLY\", \"true\");\n}\nelse\n{\n // In publish mode, use event-driven scaling based on queue depth\n worker.PublishAsAzureContainerAppJob((infra, job) =>\n {\n var accountNameParameter = queues.Resource.Parent.NameOutputReference.AsProvisioningParameter(infra);\n\n // Resolve the identity annotation added to the worker app\n if (!worker.Resource.TryGetLastAnnotation(out var identityAnnotation))\n {\n throw new InvalidOperationException(\"Identity annotation not found.\");\n }\n\n job.Configuration.TriggerType = ContainerAppJobTriggerType.Event;\n job.Configuration.EventTriggerConfig.Scale.PollingIntervalInSeconds = 1;\n job.Configuration.EventTriggerConfig.Scale.Rules.Add(new ContainerAppJobScaleRule\n {\n Name = \"queue-rule\",\n JobScaleRuleType = \"azure-queue\",\n Metadata = new ObjectExpression(\n new PropertyExpression(\"accountName\", new IdentifierExpression(accountNameParameter.BicepIdentifier)),\n new PropertyExpression(\"queueName\", new StringLiteralExpression(\"thumbnails\")),\n new PropertyExpression(\"queueLength\", new IntLiteralExpression(1))\n ),\n Identity = identityAnnotation.IdentityResource.Id.AsProvisioningParameter(infra)\n });\n });\n}\n\n// Frontend: Vite+React for upload and gallery UI\nbuilder.AddViteApp(\"frontend\", \"./frontend\")\n .WithEndpoint(\"http\", e => e.Port = 9080)\n .WithReference(api)\n .WithUrl(\"\", \"Image Gallery\")\n .WithBrowserLogs()\n .WithComputeEnvironment(env)\n .PublishAsStaticWebsite(\"/api\", api);\n\nbuilder.Build().Run();" }, { "name": "Metrics", @@ -282,7 +282,7 @@ "thumbnail": "~/assets/samples/node-express-redis/node-express-redis-primary-page.png", "appHost": "typescript", "appHostPath": "apphost.mts", - "appHostCode": "import { createBuilder } from \"./.aspire/modules/aspire.mjs\";\n\nconst builder = await createBuilder();\nconst executionContext = await builder.executionContext();\n\nawait builder.addDockerComposeEnvironment(\"dc\");\n\nconst redis = await builder.addRedis(\"redis\")\n .withRedisInsight();\n\nconst api = await builder.addNodeApp(\"api\", \"./api\", \"index.js\")\n .withHttpEndpoint({ env: \"PORT\" })\n .withHttpHealthCheck({ path: \"/health\" })\n .waitFor(redis)\n .withReference(redis);\n\nconst frontend = await builder.addViteApp(\"frontend\", \"./frontend\")\n .withReference(api);\n\nawait builder.addYarp(\"app\")\n .withConfiguration(async (yarp) =>\n {\n const apiCluster = await yarp.addClusterFromResource(api);\n await (await yarp.addRoute(\"api/{**catch-all}\", apiCluster))\n .withTransformPathRemovePrefix(\"/api\");\n\n if (await executionContext.isRunMode())\n {\n const frontendCluster = await yarp.addClusterFromResource(frontend);\n await yarp.addRoute(\"{**catch-all}\", frontendCluster);\n }\n })\n .withExternalHttpEndpoints()\n .publishWithStaticFiles(frontend)\n .withExplicitStart();\n\nawait builder.build().run();" + "appHostCode": "import { createBuilder } from \"./.aspire/modules/aspire.mjs\";\n\nconst builder = await createBuilder();\nconst executionContext = await builder.executionContext();\n\nconst dc = await builder.addDockerComposeEnvironment(\"dc\");\n\nconst redis = await builder.addRedis(\"redis\")\n .withRedisInsight()\n .withComputeEnvironment(dc);\n\nconst api = await builder.addNodeApp(\"api\", \"./api\", \"index.js\")\n .withHttpEndpoint({ env: \"PORT\" })\n .withHttpHealthCheck({ path: \"/health\" })\n .withComputeEnvironment(dc)\n .waitFor(redis)\n .withReference(redis);\n\nconst frontend = await builder.addViteApp(\"frontend\", \"./frontend\")\n .withReference(api);\n\nawait builder.addYarp(\"app\")\n .withConfiguration(async (yarp) =>\n {\n const apiCluster = await yarp.addClusterFromResource(api);\n await (await yarp.addRoute(\"api/{**catch-all}\", apiCluster))\n .withTransformPathRemovePrefix(\"/api\");\n\n if (await executionContext.isRunMode())\n {\n const frontendCluster = await yarp.addClusterFromResource(frontend);\n await yarp.addRoute(\"{**catch-all}\", frontendCluster);\n }\n })\n .withExternalHttpEndpoints()\n .withBrowserLogs()\n .publishWithStaticFiles(frontend)\n .withComputeEnvironment(dc)\n .withExplicitStart();\n\nawait builder.build().run();" }, { "name": "orleans-voting", @@ -320,7 +320,7 @@ "thumbnail": "~/assets/samples/polyglot-task-queue/polyglot-task-queue-primary-page.png", "appHost": "typescript", "appHostPath": "apphost.mts", - "appHostCode": "import { ContainerLifetime, UrlDisplayLocation, createBuilder } from \"./.aspire/modules/aspire.mjs\";\n\nconst builder = await createBuilder();\n\nawait builder.addDockerComposeEnvironment(\"dc\");\n\nconst rabbitmq = await builder.addRabbitMQ(\"messaging\")\n .withManagementPlugin()\n .withLifetime(ContainerLifetime.Persistent)\n .withUrlForEndpoint(\"tcp\", async (url) =>\n {\n url.displayLocation = UrlDisplayLocation.DetailsOnly;\n })\n .withUrlForEndpoint(\"management\", async (url) =>\n {\n url.displayText = \"RabbitMQ Management UI\";\n });\n\nconst api = await builder.addNodeApp(\"api\", \"./api\", \"index.js\")\n .withHttpEndpoint({ env: \"PORT\" })\n .withHttpHealthCheck({ path: \"/health\" })\n .waitFor(rabbitmq)\n .withReference(rabbitmq);\n\nconst frontend = await builder.addViteApp(\"frontend\", \"./frontend\")\n .withReference(api)\n .withUrl(\"\", { displayText: \"Task Queue UI\" });\n\nawait builder.addPythonApp(\"worker-python\", \"./worker-python\", \"main.py\")\n .withUv()\n .waitFor(rabbitmq)\n .withReference(rabbitmq);\n\nawait builder.addCSharpApp(\"worker-csharp\", \"./worker-csharp\")\n .waitFor(rabbitmq)\n .withReference(rabbitmq);\n\nawait api.publishWithContainerFiles(frontend, \"public\");\n\nawait builder.build().run();" + "appHostCode": "import { ContainerLifetime, UrlDisplayLocation, createBuilder } from \"./.aspire/modules/aspire.mjs\";\n\nconst builder = await createBuilder();\n\nconst dc = await builder.addDockerComposeEnvironment(\"dc\");\n\nconst rabbitmq = await builder.addRabbitMQ(\"messaging\")\n .withManagementPlugin()\n .withComputeEnvironment(dc)\n .withLifetime(ContainerLifetime.Persistent)\n .withUrlForEndpoint(\"tcp\", async (url) =>\n {\n url.displayLocation = UrlDisplayLocation.DetailsOnly;\n })\n .withUrlForEndpoint(\"management\", async (url) =>\n {\n url.displayText = \"RabbitMQ Management UI\";\n });\n\nconst api = await builder.addNodeApp(\"api\", \"./api\", \"index.js\")\n .withHttpEndpoint({ env: \"PORT\" })\n .withHttpHealthCheck({ path: \"/health\" })\n .withComputeEnvironment(dc)\n .waitFor(rabbitmq)\n .withReference(rabbitmq);\n\nconst frontend = await builder.addViteApp(\"frontend\", \"./frontend\")\n .withReference(api)\n .withUrl(\"\", { displayText: \"Task Queue UI\" })\n .withBrowserLogs();\n\nawait builder.addPythonApp(\"worker-python\", \"./worker-python\", \"main.py\")\n .withUv()\n .withComputeEnvironment(dc)\n .waitFor(rabbitmq)\n .withReference(rabbitmq);\n\nawait builder.addCSharpApp(\"worker-csharp\", \"./worker-csharp\")\n .withComputeEnvironment(dc)\n .waitFor(rabbitmq)\n .withReference(rabbitmq);\n\nawait api.publishWithContainerFiles(frontend, \"public\");\n\nawait builder.build().run();" }, { "name": "python-fastapi-postgres", @@ -385,8 +385,8 @@ "title": "RAG Document Q&A with Svelte", "description": "Upload documents and ask questions using Retrieval Augmented Generation with vector search.", "href": "https://github.com/microsoft/aspire-samples/tree/main/samples/rag-document-qa-svelte", - "readme": "# RAG Document Q&A with Svelte\n\n![Screenshot of the RAG Document Q&A sample UI](~/assets/samples/rag-document-qa-svelte/rag-document-qa-svelte-primary-page.png)\n\nUpload documents and ask questions using Retrieval Augmented Generation with vector search.\n\n## Architecture\n\n**Run Mode:**\n```mermaid\nflowchart LR\n User --> Svelte[Vite Dev Server
HMR enabled]\n Svelte -->|Proxy /api| API[FastAPI]\n API --> Qdrant[Qdrant Vector DB]\n API --> OpenAI[OpenAI API]\n```\n\n**Publish Mode:**\n```mermaid\nflowchart LR\n User --> API[FastAPI serving
Vite build output
'npm run build']\n API --> Qdrant[Qdrant Vector DB]\n API --> OpenAI[OpenAI API]\n```\n\n## What This Demonstrates\n\n- **RAG Pattern**: Document upload \u2192 chunk \u2192 embed \u2192 vector search \u2192 GPT answer\n- **addUvicornApp**: Python FastAPI backend with uv package manager\n- **addViteApp**: Svelte 5 frontend with Vite\n- **addQdrant**: Vector database for semantic search\n- **addOpenAI**: Secure API key management\n- **publishWithContainerFiles**: Frontend embedded in API for publish mode\n\n## Running\n\n```bash\naspire run\n```\n\nAspire will prompt for your OpenAI API key on first run.\n\n## Security Notes\n\nThis is a local-first sample, not a production-ready document service. Uploaded documents are untrusted input and the API only accepts UTF-8 `.txt` text uploads with size, chunk, question length, and per-client rate limits to reduce accidental OpenAI cost or quota burn.\n\nRAG apps can be affected by prompt injection and data disclosure risks because retrieved document text is placed into model context. The sample does not provide tenant isolation, document deletion controls, malware/content scanning, or durable data-retention policies.\n\nBefore adapting this for production, add real authentication and authorization, data retention and deletion workflows, monitoring, and malware/content scanning as appropriate for your data. Relevant references include [FastAPI security](https://fastapi.tiangolo.com/tutorial/security/), [OWASP LLM01 Prompt Injection](https://genai.owasp.org/llmrisk/llm01-prompt-injection/), [OWASP LLM02 Sensitive Information Disclosure](https://genai.owasp.org/llmrisk/llm022025-sensitive-information-disclosure/), [OWASP LLM08 Vector and Embedding Weaknesses](https://genai.owasp.org/llmrisk/llm082025-vector-and-embedding-weaknesses/), and OpenAI's [production best practices](https://developers.openai.com/api/docs/guides/production-best-practices) and [safety best practices](https://developers.openai.com/api/docs/guides/safety-best-practices).\n\n## Commands\n\n```bash\naspire run # Run locally\naspire deploy # Deploy to Docker Compose\naspire do docker-compose-down-dc # Teardown deployment\n```\n\n## Key Aspire Patterns\n\n**Static File Embedding** - Frontend proxied in run mode, embedded in publish mode:\n```ts\nconst openAiApiKey = await builder.addParameter(\"openai-api-key\", { secret: true });\nconst qdrant = await builder.addQdrant(\"qdrant\");\n\nawait builder.addOpenAI(\"openai\")\n .withApiKey(openAiApiKey);\n\nconst api = await builder.addUvicornApp(\"api\", \"./api\", \"main:app\")\n .withUv()\n .waitFor(qdrant)\n .withReference(qdrant)\n .withEnvironment(\"OPENAI_APIKEY\", openAiApiKey);\n\nconst frontend = await builder.addViteApp(\"frontend\", \"./frontend\")\n .withReference(api)\n .withUrl(\"\", { displayText: \"RAG UI\" });\n\nawait api.publishWithContainerFiles(frontend, \"public\");\n```\n\n**Python + uv** - Fast dependency installation from `pyproject.toml`\n\n**Vector Database** - `addQdrant()` for semantic search\n\n**OpenAI Integration** - `addOpenAI()` prompts for API key on first run\n", - "readmeRaw": "# RAG Document Q&A with Svelte\n\n![Screenshot of the RAG Document Q&A sample UI](./images/rag-document-qa-svelte-primary-page.png)\n\nUpload documents and ask questions using Retrieval Augmented Generation with vector search.\n\n## Architecture\n\n**Run Mode:**\n```mermaid\nflowchart LR\n User --> Svelte[Vite Dev Server
HMR enabled]\n Svelte -->|Proxy /api| API[FastAPI]\n API --> Qdrant[Qdrant Vector DB]\n API --> OpenAI[OpenAI API]\n```\n\n**Publish Mode:**\n```mermaid\nflowchart LR\n User --> API[FastAPI serving
Vite build output
'npm run build']\n API --> Qdrant[Qdrant Vector DB]\n API --> OpenAI[OpenAI API]\n```\n\n## What This Demonstrates\n\n- **RAG Pattern**: Document upload \u2192 chunk \u2192 embed \u2192 vector search \u2192 GPT answer\n- **addUvicornApp**: Python FastAPI backend with uv package manager\n- **addViteApp**: Svelte 5 frontend with Vite\n- **addQdrant**: Vector database for semantic search\n- **addOpenAI**: Secure API key management\n- **publishWithContainerFiles**: Frontend embedded in API for publish mode\n\n## Running\n\n```bash\naspire run\n```\n\nAspire will prompt for your OpenAI API key on first run.\n\n## Security Notes\n\nThis is a local-first sample, not a production-ready document service. Uploaded documents are untrusted input and the API only accepts UTF-8 `.txt` text uploads with size, chunk, question length, and per-client rate limits to reduce accidental OpenAI cost or quota burn.\n\nRAG apps can be affected by prompt injection and data disclosure risks because retrieved document text is placed into model context. The sample does not provide tenant isolation, document deletion controls, malware/content scanning, or durable data-retention policies.\n\nBefore adapting this for production, add real authentication and authorization, data retention and deletion workflows, monitoring, and malware/content scanning as appropriate for your data. Relevant references include [FastAPI security](https://fastapi.tiangolo.com/tutorial/security/), [OWASP LLM01 Prompt Injection](https://genai.owasp.org/llmrisk/llm01-prompt-injection/), [OWASP LLM02 Sensitive Information Disclosure](https://genai.owasp.org/llmrisk/llm022025-sensitive-information-disclosure/), [OWASP LLM08 Vector and Embedding Weaknesses](https://genai.owasp.org/llmrisk/llm082025-vector-and-embedding-weaknesses/), and OpenAI's [production best practices](https://developers.openai.com/api/docs/guides/production-best-practices) and [safety best practices](https://developers.openai.com/api/docs/guides/safety-best-practices).\n\n## Commands\n\n```bash\naspire run # Run locally\naspire deploy # Deploy to Docker Compose\naspire do docker-compose-down-dc # Teardown deployment\n```\n\n## Key Aspire Patterns\n\n**Static File Embedding** - Frontend proxied in run mode, embedded in publish mode:\n```ts\nconst openAiApiKey = await builder.addParameter(\"openai-api-key\", { secret: true });\nconst qdrant = await builder.addQdrant(\"qdrant\");\n\nawait builder.addOpenAI(\"openai\")\n .withApiKey(openAiApiKey);\n\nconst api = await builder.addUvicornApp(\"api\", \"./api\", \"main:app\")\n .withUv()\n .waitFor(qdrant)\n .withReference(qdrant)\n .withEnvironment(\"OPENAI_APIKEY\", openAiApiKey);\n\nconst frontend = await builder.addViteApp(\"frontend\", \"./frontend\")\n .withReference(api)\n .withUrl(\"\", { displayText: \"RAG UI\" });\n\nawait api.publishWithContainerFiles(frontend, \"public\");\n```\n\n**Python + uv** - Fast dependency installation from `pyproject.toml`\n\n**Vector Database** - `addQdrant()` for semantic search\n\n**OpenAI Integration** - `addOpenAI()` prompts for API key on first run\n", + "readme": "# RAG Document Q&A with Svelte\n\n![Screenshot of the RAG Document Q&A sample UI](~/assets/samples/rag-document-qa-svelte/rag-document-qa-svelte-primary-page.png)\n\nUpload documents and ask questions using Retrieval Augmented Generation with vector search.\n\n## Architecture\n\n**Run Mode:**\n```mermaid\nflowchart LR\n User --> Svelte[Vite Dev Server
HMR enabled]\n Svelte -->|Proxy /api| API[FastAPI]\n API --> Qdrant[Qdrant Vector DB]\n API --> OpenAI[OpenAI API]\n```\n\n**Publish Mode:**\n```mermaid\nflowchart LR\n User --> API[FastAPI serving
Vite build output
'npm run build']\n API --> Qdrant[Qdrant Vector DB]\n API --> OpenAI[OpenAI API]\n```\n\n## What This Demonstrates\n\n- **RAG Pattern**: Document upload → chunk → embed → vector search → GPT answer\n- **addUvicornApp**: Python FastAPI backend with uv package manager\n- **addViteApp**: Svelte 5 frontend with Vite\n- **addQdrant**: Vector database for semantic search\n- **addOpenAI**: Secure API key management\n- **publishWithContainerFiles**: Frontend embedded in API for publish mode\n\n## Running\n\n```bash\naspire run\n```\n\nAspire will prompt for your OpenAI API key on first run.\n\n## Security Notes\n\nThis is a local-first sample, not a production-ready document service. Uploaded documents are untrusted input and the API only accepts UTF-8 `.txt` text uploads with size, chunk, question length, and per-client rate limits to reduce accidental OpenAI cost or quota burn.\n\nRAG apps can be affected by prompt injection and data disclosure risks because retrieved document text is placed into model context. The sample does not provide tenant isolation, document deletion controls, malware/content scanning, or durable data-retention policies.\n\nBefore adapting this for production, add real authentication and authorization, data retention and deletion workflows, monitoring, and malware/content scanning as appropriate for your data. Relevant references include [FastAPI security](https://fastapi.tiangolo.com/tutorial/security/), [OWASP LLM01 Prompt Injection](https://genai.owasp.org/llmrisk/llm01-prompt-injection/), [OWASP LLM02 Sensitive Information Disclosure](https://genai.owasp.org/llmrisk/llm022025-sensitive-information-disclosure/), [OWASP LLM08 Vector and Embedding Weaknesses](https://genai.owasp.org/llmrisk/llm082025-vector-and-embedding-weaknesses/), and OpenAI's [production best practices](https://developers.openai.com/api/docs/guides/production-best-practices) and [safety best practices](https://developers.openai.com/api/docs/guides/safety-best-practices).\n\n## Commands\n\n```bash\naspire run # Run locally\naspire deploy # Deploy to Docker Compose\naspire do docker-compose-down-dc # Teardown deployment\n```\n\n## Key Aspire Patterns\n\n**Static File Embedding** - Frontend proxied in run mode, embedded in publish mode:\n```ts\nconst openAiApiKey = await builder.addParameter(\"openai-api-key\", { secret: true });\nconst qdrant = await builder.addQdrant(\"qdrant\");\n\nawait builder.addOpenAI(\"openai\")\n .withApiKey(openAiApiKey);\n\nconst api = await builder.addUvicornApp(\"api\", \"./api\", \"main:app\")\n .withUv()\n .waitFor(qdrant)\n .withReference(qdrant)\n .withEnvironment(\"OPENAI_APIKEY\", openAiApiKey);\n\nconst frontend = await builder.addViteApp(\"frontend\", \"./frontend\")\n .withReference(api)\n .withUrl(\"\", { displayText: \"RAG UI\" });\n\nawait api.publishWithContainerFiles(frontend, \"public\");\n```\n\n**Python + uv** - Fast dependency installation from `pyproject.toml`\n\n**Vector Database** - `addQdrant()` for semantic search\n\n**OpenAI Integration** - `addOpenAI()` prompts for API key on first run\n", + "readmeRaw": "# RAG Document Q&A with Svelte\n\n![Screenshot of the RAG Document Q&A sample UI](./images/rag-document-qa-svelte-primary-page.png)\n\nUpload documents and ask questions using Retrieval Augmented Generation with vector search.\n\n## Architecture\n\n**Run Mode:**\n```mermaid\nflowchart LR\n User --> Svelte[Vite Dev Server
HMR enabled]\n Svelte -->|Proxy /api| API[FastAPI]\n API --> Qdrant[Qdrant Vector DB]\n API --> OpenAI[OpenAI API]\n```\n\n**Publish Mode:**\n```mermaid\nflowchart LR\n User --> API[FastAPI serving
Vite build output
'npm run build']\n API --> Qdrant[Qdrant Vector DB]\n API --> OpenAI[OpenAI API]\n```\n\n## What This Demonstrates\n\n- **RAG Pattern**: Document upload → chunk → embed → vector search → GPT answer\n- **addUvicornApp**: Python FastAPI backend with uv package manager\n- **addViteApp**: Svelte 5 frontend with Vite\n- **addQdrant**: Vector database for semantic search\n- **addOpenAI**: Secure API key management\n- **publishWithContainerFiles**: Frontend embedded in API for publish mode\n\n## Running\n\n```bash\naspire run\n```\n\nAspire will prompt for your OpenAI API key on first run.\n\n## Security Notes\n\nThis is a local-first sample, not a production-ready document service. Uploaded documents are untrusted input and the API only accepts UTF-8 `.txt` text uploads with size, chunk, question length, and per-client rate limits to reduce accidental OpenAI cost or quota burn.\n\nRAG apps can be affected by prompt injection and data disclosure risks because retrieved document text is placed into model context. The sample does not provide tenant isolation, document deletion controls, malware/content scanning, or durable data-retention policies.\n\nBefore adapting this for production, add real authentication and authorization, data retention and deletion workflows, monitoring, and malware/content scanning as appropriate for your data. Relevant references include [FastAPI security](https://fastapi.tiangolo.com/tutorial/security/), [OWASP LLM01 Prompt Injection](https://genai.owasp.org/llmrisk/llm01-prompt-injection/), [OWASP LLM02 Sensitive Information Disclosure](https://genai.owasp.org/llmrisk/llm022025-sensitive-information-disclosure/), [OWASP LLM08 Vector and Embedding Weaknesses](https://genai.owasp.org/llmrisk/llm082025-vector-and-embedding-weaknesses/), and OpenAI's [production best practices](https://developers.openai.com/api/docs/guides/production-best-practices) and [safety best practices](https://developers.openai.com/api/docs/guides/safety-best-practices).\n\n## Commands\n\n```bash\naspire run # Run locally\naspire deploy # Deploy to Docker Compose\naspire do docker-compose-down-dc # Teardown deployment\n```\n\n## Key Aspire Patterns\n\n**Static File Embedding** - Frontend proxied in run mode, embedded in publish mode:\n```ts\nconst openAiApiKey = await builder.addParameter(\"openai-api-key\", { secret: true });\nconst qdrant = await builder.addQdrant(\"qdrant\");\n\nawait builder.addOpenAI(\"openai\")\n .withApiKey(openAiApiKey);\n\nconst api = await builder.addUvicornApp(\"api\", \"./api\", \"main:app\")\n .withUv()\n .waitFor(qdrant)\n .withReference(qdrant)\n .withEnvironment(\"OPENAI_APIKEY\", openAiApiKey);\n\nconst frontend = await builder.addViteApp(\"frontend\", \"./frontend\")\n .withReference(api)\n .withUrl(\"\", { displayText: \"RAG UI\" });\n\nawait api.publishWithContainerFiles(frontend, \"public\");\n```\n\n**Python + uv** - Fast dependency installation from `pyproject.toml`\n\n**Vector Database** - `addQdrant()` for semantic search\n\n**OpenAI Integration** - `addOpenAI()` prompts for API key on first run\n", "tags": [ "databases", "docker", @@ -397,7 +397,7 @@ "thumbnail": "~/assets/samples/rag-document-qa-svelte/rag-document-qa-svelte-primary-page.png", "appHost": "typescript", "appHostPath": "apphost.mts", - "appHostCode": "import { createBuilder } from \"./.aspire/modules/aspire.mjs\";\n\nconst builder = await createBuilder();\n\nconst openAiApiKey = await builder.addParameter(\"openai-api-key\", { secret: true });\n\nconst qdrant = await builder.addQdrant(\"qdrant\");\n\nawait builder.addOpenAI(\"openai\")\n .withApiKey(openAiApiKey);\n\nconst api = await builder.addUvicornApp(\"api\", \"./api\", \"main:app\")\n .withUv()\n .withHttpHealthCheck({ path: \"/health\" })\n .waitFor(qdrant)\n .withReference(qdrant)\n .withEnvironment(\"OPENAI_APIKEY\", openAiApiKey)\n .withExternalHttpEndpoints();\n\nconst frontend = await builder.addViteApp(\"frontend\", \"./frontend\")\n .withReference(api)\n .withUrl(\"\", { displayText: \"RAG UI\" });\n\nawait api.publishWithContainerFiles(frontend, \"public\");\n\nawait builder.build().run();" + "appHostCode": "import { createBuilder } from \"./.aspire/modules/aspire.mjs\";\n\nconst builder = await createBuilder();\n\nconst openAiApiKey = await builder.addParameter(\"openai-api-key\", { secret: true });\n\nconst qdrant = await builder.addQdrant(\"qdrant\");\n\nawait builder.addOpenAI(\"openai\")\n .withApiKey(openAiApiKey);\n\nconst api = await builder.addUvicornApp(\"api\", \"./api\", \"main:app\")\n .withUv()\n .withHttpHealthCheck({ path: \"/health\" })\n .waitFor(qdrant)\n .withReference(qdrant)\n .withEnvironment(\"OPENAI_APIKEY\", openAiApiKey)\n .withExternalHttpEndpoints();\n\nconst frontend = await builder.addViteApp(\"frontend\", \"./frontend\")\n .withReference(api)\n .withUrl(\"\", { displayText: \"RAG UI\" })\n .withBrowserLogs();\n\nawait api.publishWithContainerFiles(frontend, \"public\");\n\nawait builder.build().run();" }, { "name": "standalone-dashboard", @@ -438,7 +438,7 @@ "thumbnail": "~/assets/samples/vite-csharp-postgres/vite-csharp-postgres-primary-page.png", "appHost": "typescript", "appHostPath": "apphost.mts", - "appHostCode": "import { ContainerLifetime, UrlDisplayLocation, createBuilder } from \"./.aspire/modules/aspire.mjs\";\n\nconst builder = await createBuilder();\n\nawait builder.addDockerComposeEnvironment(\"dc\");\n\nconst postgres = await builder.addPostgres(\"postgres\")\n .withDataVolume()\n .withLifetime(ContainerLifetime.Persistent)\n .withPgAdmin({\n configureContainer: async (pgAdmin) =>\n {\n await pgAdmin.withLifetime(ContainerLifetime.Persistent);\n }\n });\n\nconst db = await postgres.addDatabase(\"db\");\n\nconst api = await builder.addCSharpApp(\"api\", \"./api\")\n .withHttpHealthCheck({ path: \"/health\" })\n .withExternalHttpEndpoints()\n .waitFor(db)\n .withReference(db)\n .withUrlForEndpoint(\"http\", async (url) =>\n {\n url.displayLocation = UrlDisplayLocation.DetailsOnly;\n })\n .withUrlForEndpoint(\"https\", async (url) =>\n {\n url.displayLocation = UrlDisplayLocation.DetailsOnly;\n })\n .withUrls(async (ctx) =>\n {\n const endpoint = ctx.getEndpoint(\"https\");\n const urls = await ctx.urls();\n await urls.addForEndpoint(endpoint, `${await endpoint.url()}/scalar`, { displayText: \"API Reference\" });\n })\n .publishAsDockerComposeService(async (_, service) =>\n {\n await service.restart.set(\"always\");\n });\n\nconst frontend = await builder.addViteApp(\"frontend\", \"./frontend\")\n .withReference(api)\n .withUrl(\"\", { displayText: \"Todo UI\" });\n\nawait api.publishWithContainerFiles(frontend, \"wwwroot\");\n\nawait builder.build().run();" + "appHostCode": "import { ContainerLifetime, UrlDisplayLocation, createBuilder } from \"./.aspire/modules/aspire.mjs\";\n\nconst builder = await createBuilder();\n\nconst dc = await builder.addDockerComposeEnvironment(\"dc\");\n\nconst postgres = await builder.addPostgres(\"postgres\")\n .withComputeEnvironment(dc)\n .withDataVolume()\n .withLifetime(ContainerLifetime.Persistent)\n .withPgAdmin({\n configureContainer: async (pgAdmin) =>\n {\n await pgAdmin.withLifetime(ContainerLifetime.Persistent);\n }\n });\n\nconst db = await postgres.addDatabase(\"db\");\n\nconst api = await builder.addCSharpApp(\"api\", \"./api\")\n .withHttpHealthCheck({ path: \"/health\" })\n .withExternalHttpEndpoints()\n .withComputeEnvironment(dc)\n .waitFor(db)\n .withReference(db)\n .withUrlForEndpoint(\"http\", async (url) =>\n {\n url.displayLocation = UrlDisplayLocation.DetailsOnly;\n })\n .withUrlForEndpoint(\"https\", async (url) =>\n {\n url.displayLocation = UrlDisplayLocation.DetailsOnly;\n })\n .withUrls(async (ctx) =>\n {\n const endpoint = ctx.getEndpoint(\"https\");\n const urls = await ctx.urls();\n await urls.addForEndpoint(endpoint, `${await endpoint.url()}/scalar`, { displayText: \"API Reference\" });\n })\n .publishAsDockerComposeService(async (_, service) =>\n {\n await service.restart.set(\"always\");\n });\n\nconst frontend = await builder.addViteApp(\"frontend\", \"./frontend\")\n .withReference(api)\n .withUrl(\"\", { displayText: \"Todo UI\" })\n .withBrowserLogs();\n\nawait api.publishWithContainerFiles(frontend, \"wwwroot\");\n\nawait builder.build().run();" }, { "name": "vite-react-fastapi", @@ -457,7 +457,7 @@ "thumbnail": "~/assets/samples/vite-react-fastapi/vite-react-fastapi-primary-page.png", "appHost": "typescript", "appHostPath": "apphost.mts", - "appHostCode": "import { createBuilder } from \"./.aspire/modules/aspire.mjs\";\n\nconst builder = await createBuilder();\nconst executionContext = await builder.executionContext();\n\nawait builder.addDockerComposeEnvironment(\"dc\");\n\nconst api = await builder.addUvicornApp(\"api\", \"./api\", \"main:app\")\n .withHttpHealthCheck({ path: \"/health\" });\n\nconst frontend = await builder.addViteApp(\"frontend\", \"./frontend\")\n .withReference(api);\n\nawait builder.addYarp(\"app\")\n .withConfiguration(async (yarp) =>\n {\n const apiCluster = await (await yarp.addClusterWithDestination(\"api\", \"https://api\"))\n .withHttpClientConfig({ dangerousAcceptAnyServerCertificate: true });\n await (await yarp.addRoute(\"api/{**catch-all}\", apiCluster))\n .withTransformPathRemovePrefix(\"/api\");\n\n if (await executionContext.isRunMode())\n {\n const frontendCluster = await yarp.addClusterFromResource(frontend);\n await yarp.addRoute(\"{**catch-all}\", frontendCluster);\n }\n })\n .withReference(api)\n .withExternalHttpEndpoints()\n .publishWithStaticFiles(frontend)\n .withExplicitStart();\n\nawait builder.build().run();" + "appHostCode": "import { createBuilder } from \"./.aspire/modules/aspire.mjs\";\n\nconst builder = await createBuilder();\nconst executionContext = await builder.executionContext();\n\nconst dc = await builder.addDockerComposeEnvironment(\"dc\");\n\nconst api = await builder.addUvicornApp(\"api\", \"./api\", \"main:app\")\n .withHttpHealthCheck({ path: \"/health\" })\n .withComputeEnvironment(dc);\n\nconst frontend = await builder.addViteApp(\"frontend\", \"./frontend\")\n .withReference(api);\n\nawait builder.addYarp(\"app\")\n .withConfiguration(async (yarp) =>\n {\n const apiCluster = await (await yarp.addClusterWithDestination(\"api\", \"https://api\"))\n .withHttpClientConfig({ dangerousAcceptAnyServerCertificate: true });\n await (await yarp.addRoute(\"api/{**catch-all}\", apiCluster))\n .withTransformPathRemovePrefix(\"/api\");\n\n if (await executionContext.isRunMode())\n {\n const frontendCluster = await yarp.addClusterFromResource(frontend);\n await yarp.addRoute(\"{**catch-all}\", frontendCluster);\n }\n })\n .withReference(api)\n .withExternalHttpEndpoints()\n .withBrowserLogs()\n .publishWithStaticFiles(frontend)\n .withComputeEnvironment(dc)\n .withExplicitStart();\n\nawait builder.build().run();" }, { "name": "vite-yarp-static", @@ -474,7 +474,7 @@ "thumbnail": "~/assets/samples/vite-yarp-static/vite-yarp-static-primary-page.png", "appHost": "typescript", "appHostPath": "apphost.mts", - "appHostCode": "import { createBuilder } from \"./.aspire/modules/aspire.mjs\";\n\nconst builder = await createBuilder();\nconst executionContext = await builder.executionContext();\n\nawait builder.addDockerComposeEnvironment(\"dc\");\n\nconst frontend = await builder.addViteApp(\"frontend\", \"./frontend\");\n\nawait builder.addYarp(\"app\")\n .withConfiguration(async (yarp) =>\n {\n if (await executionContext.isRunMode())\n {\n const frontendCluster = await yarp.addClusterFromResource(frontend);\n await yarp.addRoute(\"{**catch-all}\", frontendCluster);\n }\n })\n .withExternalHttpEndpoints()\n .publishWithStaticFiles(frontend);\n\nawait builder.build().run();" + "appHostCode": "import { createBuilder } from \"./.aspire/modules/aspire.mjs\";\n\nconst builder = await createBuilder();\nconst executionContext = await builder.executionContext();\n\nconst dc = await builder.addDockerComposeEnvironment(\"dc\");\n\nconst frontend = await builder.addViteApp(\"frontend\", \"./frontend\");\n\nawait builder.addYarp(\"app\")\n .withConfiguration(async (yarp) =>\n {\n if (await executionContext.isRunMode())\n {\n const frontendCluster = await yarp.addClusterFromResource(frontend);\n await yarp.addRoute(\"{**catch-all}\", frontendCluster);\n }\n })\n .withExternalHttpEndpoints()\n .withBrowserLogs()\n .publishWithStaticFiles(frontend)\n .withComputeEnvironment(dc);\n\nawait builder.build().run();" }, { "name": "volume-mount", @@ -503,4 +503,4 @@ "appHostPath": "VolumeMount.AppHost/AppHost.cs", "appHostCode": "var builder = DistributedApplication.CreateBuilder(args);\n\nvar sqlserver = builder.AddSqlServer(\"sqlserver\")\n .WithDataVolume()\n .WithLifetime(ContainerLifetime.Persistent);\n\nvar sqlDatabase = sqlserver.AddDatabase(\"sqldb\");\n\nvar blobs = builder.AddAzureStorage(\"Storage\")\n // Use the Azurite storage emulator for local development\n .RunAsEmulator(emulator => emulator.WithDataVolume())\n .AddBlobs(\"BlobConnection\");\n\nbuilder.AddProject(\"blazorweb\")\n .WithReference(sqlDatabase)\n .WaitFor(sqlDatabase)\n .WithReference(blobs)\n .WaitFor(blobs);\n\nbuilder.Build().Run();" } -] +] \ No newline at end of file