From 3c0c485bb05fbdaad7276fa90141b0219ef8a1fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Jun 2025 10:27:51 +0000 Subject: [PATCH 1/8] Initial plan for issue From fb46a0185aad179055f5cd0d1490ed64f4d70f93 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Jun 2025 10:39:35 +0000 Subject: [PATCH 2/8] Implement Scalar as Aspire primitive with extension methods Co-authored-by: indcoder <2099300+indcoder@users.noreply.github.com> --- Visage.AppHost/Program.cs | 11 +-- Visage.AppHost/ScalarExtensions.cs | 51 +++++++++++++ Visage.ServiceDefaults/Extensions.cs | 2 + Visage.ServiceDefaults/ScalarExtensions.cs | 72 +++++++++++++++++++ .../Visage.ServiceDefaults.csproj | 3 + Visage.Services.Registrations/Program.cs | 14 +--- services/Visage.Services.Eventing/Program.cs | 18 +---- .../ScalarIntegrationTest.cs | 53 ++++++++++++++ 8 files changed, 186 insertions(+), 38 deletions(-) create mode 100644 Visage.AppHost/ScalarExtensions.cs create mode 100644 Visage.ServiceDefaults/ScalarExtensions.cs create mode 100644 tests/Visage.Test.Aspire/ScalarIntegrationTest.cs diff --git a/Visage.AppHost/Program.cs b/Visage.AppHost/Program.cs index 7829926..6640782 100644 --- a/Visage.AppHost/Program.cs +++ b/Visage.AppHost/Program.cs @@ -29,13 +29,7 @@ #region EventAPI var EventAPI = builder.AddProject("event-api") - .WithUrlForEndpoint("http", - url => url.DisplayLocation = UrlDisplayLocation.DetailsOnly) // Hide the plain-HTTP link from the Resources grid - .WithUrlForEndpoint("https", url => - { - url.DisplayText = "Event API Scalar OpenAPI"; - url.Url += "/scalar/v1"; - }); + .WithScalarApiDocumentation("Visage Event API"); @@ -46,7 +40,8 @@ var registrationAPI = builder.AddProject("registrations-api") .WithEnvironment("Auth0__Domain", iamDomain) .WithEnvironment("Auth0__Audience", iamAudience) - .WithReference(VisageSQL); + .WithReference(VisageSQL) + .WithScalarApiDocumentation("Visage Registration API"); #endregion diff --git a/Visage.AppHost/ScalarExtensions.cs b/Visage.AppHost/ScalarExtensions.cs new file mode 100644 index 0000000..4c4eb21 --- /dev/null +++ b/Visage.AppHost/ScalarExtensions.cs @@ -0,0 +1,51 @@ +using Aspire.Hosting; +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting; + +/// +/// Provides extension methods for adding Scalar API documentation to Aspire applications. +/// +public static class ScalarResourceExtensions +{ + /// + /// Configures a project resource to expose Scalar API documentation. + /// This adds the necessary URL endpoint configuration for Scalar UI. + /// + /// The project resource to configure. + /// The title for the API documentation. If null, uses the resource name. + /// The path where Scalar UI will be served. Defaults to "/scalar/v1". + /// The project resource for chaining. + public static IResourceBuilder WithScalarApiDocumentation( + this IResourceBuilder projectResource, + string? title = null, + string scalarPath = "/scalar/v1") + { + var apiTitle = title ?? $"{projectResource.Resource.Name} API"; + + return projectResource + .WithUrlForEndpoint("http", url => + url.DisplayLocation = UrlDisplayLocation.DetailsOnly) // Hide the plain-HTTP link from the Resources grid + .WithUrlForEndpoint("https", url => + { + url.DisplayText = $"{apiTitle} Scalar OpenAPI"; + url.Url += scalarPath; + }); + } + + /// + /// Configures a project resource to expose Scalar API documentation with custom endpoint configuration. + /// + /// The project resource to configure. + /// Action to configure the Scalar endpoint. + /// The project resource for chaining. + public static IResourceBuilder WithScalarApiDocumentation( + this IResourceBuilder projectResource, + Action configure) + { + return projectResource + .WithUrlForEndpoint("http", url => + url.DisplayLocation = UrlDisplayLocation.DetailsOnly) // Hide the plain-HTTP link from the Resources grid + .WithUrlForEndpoint("https", configure); + } +} \ No newline at end of file diff --git a/Visage.ServiceDefaults/Extensions.cs b/Visage.ServiceDefaults/Extensions.cs index c404e89..2eaa4b5 100644 --- a/Visage.ServiceDefaults/Extensions.cs +++ b/Visage.ServiceDefaults/Extensions.cs @@ -20,6 +20,8 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu builder.ConfigureOpenTelemetry(); builder.AddDefaultHealthChecks(); + + builder.AddScalarDefaults(); builder.Services.AddServiceDiscovery(); diff --git a/Visage.ServiceDefaults/ScalarExtensions.cs b/Visage.ServiceDefaults/ScalarExtensions.cs new file mode 100644 index 0000000..c1df88d --- /dev/null +++ b/Visage.ServiceDefaults/ScalarExtensions.cs @@ -0,0 +1,72 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Scalar.AspNetCore; + +namespace Microsoft.Extensions.Hosting; + +/// +/// Provides extension methods for configuring Scalar API documentation in Aspire applications. +/// +public static class ScalarExtensions +{ + /// + /// Adds Scalar API documentation support to the service defaults. + /// This configures OpenAPI services and provides default Scalar configuration. + /// + /// The host application builder. + /// The host application builder for chaining. + public static IHostApplicationBuilder AddScalarDefaults(this IHostApplicationBuilder builder) + { + // Add OpenAPI services that Scalar depends on + builder.Services.AddOpenApi(); + + return builder; + } + + /// + /// Maps Scalar API reference endpoints with default configuration for Aspire applications. + /// + /// The web application. + /// The title for the API documentation. If null, uses the application name. + /// The path where Scalar UI will be served. Defaults to "/scalar/v1". + /// The web application for chaining. + public static WebApplication MapScalarDefaults(this WebApplication app, string? title = null, string? path = null) + { + if (app.Environment.IsDevelopment()) + { + // Map OpenAPI endpoint + app.MapOpenApi(); + + // Configure and map Scalar API reference + app.MapScalarApiReference(options => + { + var apiTitle = title ?? app.Environment.ApplicationName ?? "API"; + options.WithTitle(apiTitle) + .WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient); + }); + } + + return app; + } + + /// + /// Maps Scalar API reference endpoints with custom configuration. + /// + /// The web application. + /// Action to configure Scalar options. + /// The web application for chaining. + public static WebApplication MapScalarDefaults(this WebApplication app, Action configure) + { + if (app.Environment.IsDevelopment()) + { + // Map OpenAPI endpoint + app.MapOpenApi(); + + // Configure and map Scalar API reference with custom options + app.MapScalarApiReference(configure); + } + + return app; + } +} \ No newline at end of file diff --git a/Visage.ServiceDefaults/Visage.ServiceDefaults.csproj b/Visage.ServiceDefaults/Visage.ServiceDefaults.csproj index 56023f7..1dd804b 100644 --- a/Visage.ServiceDefaults/Visage.ServiceDefaults.csproj +++ b/Visage.ServiceDefaults/Visage.ServiceDefaults.csproj @@ -10,6 +10,8 @@ + + @@ -17,6 +19,7 @@ + diff --git a/Visage.Services.Registrations/Program.cs b/Visage.Services.Registrations/Program.cs index 36ef014..030c99a 100644 --- a/Visage.Services.Registrations/Program.cs +++ b/Visage.Services.Registrations/Program.cs @@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.SqlServer; -using Scalar.AspNetCore; using Visage.Services.Registration; using Visage.Shared.Models; @@ -54,21 +53,10 @@ logging.RequestHeaders.Add("Authorization"); }); -// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi -builder.Services.AddOpenApi(); - var app = builder.Build(); // Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.MapOpenApi(); - app.MapScalarApiReference(options => - { - options.WithTitle("Visage Registration API") - .WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient); - }); -} +app.MapScalarDefaults("Visage Registration API"); app.UseHttpsRedirection(); app.UseAuthentication(); diff --git a/services/Visage.Services.Eventing/Program.cs b/services/Visage.Services.Eventing/Program.cs index 141ab82..798a86c 100644 --- a/services/Visage.Services.Eventing/Program.cs +++ b/services/Visage.Services.Eventing/Program.cs @@ -1,5 +1,4 @@ using Microsoft.EntityFrameworkCore; -using Scalar.AspNetCore; using Microsoft.AspNetCore.Http.HttpResults; using Visage.Shared.Models; using Microsoft.AspNetCore.Mvc; @@ -16,25 +15,10 @@ builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("EventList")); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); - - -// Add services to the container. -// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi -builder.Services.AddOpenApi(); - var app = builder.Build(); // Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.MapOpenApi(); - app.MapScalarApiReference(options => - { - options.WithTitle("Visage Event API") - .WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient); - } - ); -} +app.MapScalarDefaults("Visage Event API"); app.UseHttpsRedirection(); diff --git a/tests/Visage.Test.Aspire/ScalarIntegrationTest.cs b/tests/Visage.Test.Aspire/ScalarIntegrationTest.cs new file mode 100644 index 0000000..c1cf67b --- /dev/null +++ b/tests/Visage.Test.Aspire/ScalarIntegrationTest.cs @@ -0,0 +1,53 @@ +using Aspire.Hosting; +using FluentAssertions; +using TUnit.Assertions; + +namespace Visage.Test.Aspire.Tests; + +public class ScalarIntegrationTest +{ + [Test] + public void WithScalarApiDocumentation_ShouldConfigureEndpoints() + { + // Arrange + var appBuilder = DistributedApplication.CreateBuilder(); + var projectResource = appBuilder.AddProject("test-api"); + + // Act + var configuredResource = projectResource.WithScalarApiDocumentation("Test API"); + + // Assert + configuredResource.Should().NotBeNull(); + configuredResource.Should().BeSameAs(projectResource); + } + + [Test] + public void WithScalarApiDocumentation_WithDefaultTitle_ShouldUseResourceName() + { + // Arrange + var appBuilder = DistributedApplication.CreateBuilder(); + var projectResource = appBuilder.AddProject("test-api"); + + // Act + var configuredResource = projectResource.WithScalarApiDocumentation(); + + // Assert + configuredResource.Should().NotBeNull(); + configuredResource.Should().BeSameAs(projectResource); + } + + [Test] + public void WithScalarApiDocumentation_WithCustomPath_ShouldConfigureCorrectly() + { + // Arrange + var appBuilder = DistributedApplication.CreateBuilder(); + var projectResource = appBuilder.AddProject("test-api"); + + // Act + var configuredResource = projectResource.WithScalarApiDocumentation("Test API", "/docs/v1"); + + // Assert + configuredResource.Should().NotBeNull(); + configuredResource.Should().BeSameAs(projectResource); + } +} \ No newline at end of file From 4d8789d1ea104ea15ad8f1c46016040d67d8aa49 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Jun 2025 10:41:40 +0000 Subject: [PATCH 3/8] Add documentation and examples for Scalar Aspire integration Co-authored-by: indcoder <2099300+indcoder@users.noreply.github.com> --- docs/examples/sample-apphost-program.cs | 21 +++ docs/examples/sample-service-program.cs | 55 ++++++++ docs/scalar-aspire-integration.md | 180 ++++++++++++++++++++++++ 3 files changed, 256 insertions(+) create mode 100644 docs/examples/sample-apphost-program.cs create mode 100644 docs/examples/sample-service-program.cs create mode 100644 docs/scalar-aspire-integration.md diff --git a/docs/examples/sample-apphost-program.cs b/docs/examples/sample-apphost-program.cs new file mode 100644 index 0000000..86ad16a --- /dev/null +++ b/docs/examples/sample-apphost-program.cs @@ -0,0 +1,21 @@ +using Aspire.Hosting; + +var builder = DistributedApplication.CreateBuilder(args); + +// Add services with Scalar API documentation +var sampleAPI = builder.AddProject("sample-api") + .WithScalarApiDocumentation("Sample API"); + +var userAPI = builder.AddProject("user-api") + .WithScalarApiDocumentation("User Management API", "/docs"); + +var orderAPI = builder.AddProject("order-api") + .WithScalarApiDocumentation("Order Processing API"); + +// Add frontend application +var webapp = builder.AddProject("webapp") + .WithReference(sampleAPI) + .WithReference(userAPI) + .WithReference(orderAPI); + +builder.Build().Run(); \ No newline at end of file diff --git a/docs/examples/sample-service-program.cs b/docs/examples/sample-service-program.cs new file mode 100644 index 0000000..4062f2b --- /dev/null +++ b/docs/examples/sample-service-program.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +// Add service defaults & Aspire components (includes Scalar) +builder.AddServiceDefaults(); + +// Add your services +builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("SampleData")); + +var app = builder.Build(); + +// Configure Scalar API documentation +app.MapScalarDefaults("Sample API"); + +app.UseHttpsRedirection(); + +// Define API endpoints +var api = app.MapGroup("/api/items"); + +api.MapGet("/", async (SampleDb db) => await db.Items.ToListAsync()) + .WithName("GetAllItems") + .WithOpenApi(); + +api.MapGet("/{id}", async (int id, SampleDb db) => + await db.Items.FindAsync(id) is Item item + ? Results.Ok(item) + : Results.NotFound()) + .WithName("GetItem") + .WithOpenApi(); + +api.MapPost("/", async (Item item, SampleDb db) => +{ + db.Items.Add(item); + await db.SaveChangesAsync(); + return Results.Created($"/api/items/{item.Id}", item); +}) + .WithName("CreateItem") + .WithOpenApi(); + +app.Run(); + +// Sample data models +public class Item +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; +} + +public class SampleDb : DbContext +{ + public SampleDb(DbContextOptions options) : base(options) { } + public DbSet Items { get; set; } +} \ No newline at end of file diff --git a/docs/scalar-aspire-integration.md b/docs/scalar-aspire-integration.md new file mode 100644 index 0000000..ed30645 --- /dev/null +++ b/docs/scalar-aspire-integration.md @@ -0,0 +1,180 @@ +# Scalar Aspire Integration + +This document describes how to use Scalar API documentation with .NET Aspire in the Visage project. + +## Overview + +Scalar has been integrated as an Aspire primitive, providing a simplified way to add interactive API documentation to your Aspire applications. The integration includes both service-level configuration and AppHost orchestration support. + +## Features + +- **Centralized Configuration**: Scalar setup is included in the default service configuration +- **Aspire Integration**: Easy configuration through the AppHost with the `WithScalarApiDocumentation()` extension method +- **Development Mode**: Automatically enabled only in development environment +- **Customizable**: Support for custom titles, paths, and configurations + +## Usage + +### In Services (Automatic) + +Services that use `builder.AddServiceDefaults()` automatically get Scalar support. Simply call: + +```csharp +var builder = WebApplication.CreateBuilder(args); + +// Add service defaults (includes Scalar configuration) +builder.AddServiceDefaults(); + +var app = builder.Build(); + +// Map Scalar with default configuration +app.MapScalarDefaults("My API"); + +// Or with custom configuration +app.MapScalarDefaults(options => +{ + options.WithTitle("My Custom API") + .WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient); +}); + +app.Run(); +``` + +### In AppHost + +Configure Scalar endpoints for your services in the AppHost: + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +// Add project with Scalar documentation +var eventAPI = builder.AddProject("event-api") + .WithScalarApiDocumentation("Event API"); + +// With custom path +var registrationAPI = builder.AddProject("registration-api") + .WithScalarApiDocumentation("Registration API", "/docs/v1"); + +builder.Build().Run(); +``` + +## API Reference + +### ServiceDefaults Extensions + +#### `AddScalarDefaults()` +Adds Scalar support to service defaults, including OpenAPI services. + +#### `MapScalarDefaults(title, path)` +Maps Scalar API reference endpoints with default configuration. + +**Parameters:** +- `title` (optional): The title for the API documentation +- `path` (optional): The path where Scalar UI will be served (defaults to "/scalar/v1") + +### AppHost Extensions + +#### `WithScalarApiDocumentation(title, scalarPath)` +Configures a project resource to expose Scalar API documentation. + +**Parameters:** +- `title` (optional): The title for the API documentation +- `scalarPath` (optional): The path where Scalar UI will be served (defaults to "/scalar/v1") + +## Examples + +### Basic Service Setup + +```csharp +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +// Add service defaults & Aspire components (includes Scalar) +builder.AddServiceDefaults(); + +builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("MyList")); + +var app = builder.Build(); + +// Configure Scalar with service name +app.MapScalarDefaults("My Service API"); + +app.UseHttpsRedirection(); + +// Your API endpoints +var api = app.MapGroup("/api"); +api.MapGet("/items", GetItems).WithOpenApi(); + +app.Run(); +``` + +### AppHost Configuration + +```csharp +using Aspire.Hosting; + +var builder = DistributedApplication.CreateBuilder(args); + +// Services with Scalar documentation +var eventAPI = builder.AddProject("event-api") + .WithScalarApiDocumentation("Event Management API"); + +var userAPI = builder.AddProject("user-api") + .WithScalarApiDocumentation("User Management API", "/api-docs"); + +// Frontend references the APIs +var webapp = builder.AddProject("webapp") + .WithReference(eventAPI) + .WithReference(userAPI); + +builder.Build().Run(); +``` + +## Migration Guide + +If you're migrating from manual Scalar configuration: + +### Before +```csharp +// Manual configuration +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); + app.MapScalarApiReference(options => + { + options.WithTitle("My API") + .WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient); + }); +} +``` + +### After +```csharp +// Using Scalar Aspire primitive +builder.AddServiceDefaults(); // Includes OpenAPI and Scalar setup + +var app = builder.Build(); + +app.MapScalarDefaults("My API"); // Handles development check automatically +``` + +## Troubleshooting + +### Scalar UI not appearing +- Ensure you're running in Development environment +- Check that `AddServiceDefaults()` is called before building the app +- Verify that `MapScalarDefaults()` is called after building the app + +### OpenAPI not working +- Make sure your endpoints use `.WithOpenApi()` extension +- Check that the Microsoft.AspNetCore.OpenApi package is referenced + +### AppHost configuration not working +- Ensure the service uses `AddServiceDefaults()` and `MapScalarDefaults()` +- Verify that the project reference in AppHost is correct +- Check that the `WithScalarApiDocumentation()` extension is applied to the correct resource \ No newline at end of file From fe661a669a2c70cffee65295fabd870540906e98 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Jun 2025 10:57:06 +0000 Subject: [PATCH 4/8] Add Scalar.AspNetCore package reference to AppHost Co-authored-by: indcoder <2099300+indcoder@users.noreply.github.com> --- Visage.AppHost/Visage.AppHost.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Visage.AppHost/Visage.AppHost.csproj b/Visage.AppHost/Visage.AppHost.csproj index 72ccef6..7c16688 100644 --- a/Visage.AppHost/Visage.AppHost.csproj +++ b/Visage.AppHost/Visage.AppHost.csproj @@ -12,6 +12,7 @@ + From bccd4b0c73aa4a7151ae5da86a614a4f68f5b890 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Jun 2025 10:58:09 +0000 Subject: [PATCH 5/8] Fix Scalar extensions to properly configure HTTP endpoints Co-authored-by: indcoder <2099300+indcoder@users.noreply.github.com> --- Visage.AppHost/ScalarExtensions.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Visage.AppHost/ScalarExtensions.cs b/Visage.AppHost/ScalarExtensions.cs index 4c4eb21..740c6cb 100644 --- a/Visage.AppHost/ScalarExtensions.cs +++ b/Visage.AppHost/ScalarExtensions.cs @@ -24,6 +24,8 @@ public static IResourceBuilder WithScalarApiDocumentation( var apiTitle = title ?? $"{projectResource.Resource.Name} API"; return projectResource + .WithHttpEndpoint(port: null, name: "http") + .WithHttpsEndpoint(port: null, name: "https") .WithUrlForEndpoint("http", url => url.DisplayLocation = UrlDisplayLocation.DetailsOnly) // Hide the plain-HTTP link from the Resources grid .WithUrlForEndpoint("https", url => @@ -44,6 +46,8 @@ public static IResourceBuilder WithScalarApiDocumentation( Action configure) { return projectResource + .WithHttpEndpoint(port: null, name: "http") + .WithHttpsEndpoint(port: null, name: "https") .WithUrlForEndpoint("http", url => url.DisplayLocation = UrlDisplayLocation.DetailsOnly) // Hide the plain-HTTP link from the Resources grid .WithUrlForEndpoint("https", configure); From 7f7a46ec91d5a1163460e31fef1ab94817e82a51 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Jun 2025 10:58:47 +0000 Subject: [PATCH 6/8] Update Scalar documentation with endpoint configuration details Co-authored-by: indcoder <2099300+indcoder@users.noreply.github.com> --- docs/scalar-aspire-integration.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/scalar-aspire-integration.md b/docs/scalar-aspire-integration.md index ed30645..c0824a1 100644 --- a/docs/scalar-aspire-integration.md +++ b/docs/scalar-aspire-integration.md @@ -75,12 +75,14 @@ Maps Scalar API reference endpoints with default configuration. ### AppHost Extensions #### `WithScalarApiDocumentation(title, scalarPath)` -Configures a project resource to expose Scalar API documentation. +Configures a project resource to expose Scalar API documentation. This method automatically configures HTTP and HTTPS endpoints for the service and sets up the Scalar UI endpoint. **Parameters:** - `title` (optional): The title for the API documentation - `scalarPath` (optional): The path where Scalar UI will be served (defaults to "/scalar/v1") +**Note:** This method automatically adds HTTP and HTTPS endpoints to the project resource if they don't exist. + ## Examples ### Basic Service Setup @@ -177,4 +179,5 @@ app.MapScalarDefaults("My API"); // Handles development check automatically ### AppHost configuration not working - Ensure the service uses `AddServiceDefaults()` and `MapScalarDefaults()` - Verify that the project reference in AppHost is correct -- Check that the `WithScalarApiDocumentation()` extension is applied to the correct resource \ No newline at end of file +- Check that the `WithScalarApiDocumentation()` extension is applied to the correct resource +- Ensure the AppHost project includes the `Scalar.AspNetCore` package reference \ No newline at end of file From a298ff1a8304b132d6c50258c3853407d9ebc958 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Jun 2025 09:49:29 +0000 Subject: [PATCH 7/8] Remove unnecessary Scalar.AspNetCore package reference from AppHost Co-authored-by: indcoder <2099300+indcoder@users.noreply.github.com> --- Visage.AppHost/Visage.AppHost.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/Visage.AppHost/Visage.AppHost.csproj b/Visage.AppHost/Visage.AppHost.csproj index 7c16688..72ccef6 100644 --- a/Visage.AppHost/Visage.AppHost.csproj +++ b/Visage.AppHost/Visage.AppHost.csproj @@ -12,7 +12,6 @@ - From f1705358f8889d212dcf540789542a12e41b4a22 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:13:04 +0000 Subject: [PATCH 8/8] Implement official Scalar Aspire integration pattern - Follow official Scalar guide for .NET Aspire integration - Remove custom extension methods in favor of direct service configuration - Add Scalar URL configuration for Registration API in AppHost - Remove unnecessary Scalar package reference from ServiceDefaults - Update documentation to reflect official approach Co-authored-by: indcoder <2099300+indcoder@users.noreply.github.com> --- Visage.AppHost/Program.cs | 16 +- Visage.AppHost/ScalarExtensions.cs | 55 ------ Visage.ServiceDefaults/Extensions.cs | 2 - Visage.ServiceDefaults/ScalarExtensions.cs | 72 ------- .../Visage.ServiceDefaults.csproj | 2 +- Visage.Services.Registrations/Program.cs | 14 +- docs/examples/sample-apphost-program.cs | 21 -- docs/examples/sample-service-program.cs | 55 ------ docs/scalar-aspire-integration.md | 186 ++++-------------- services/Visage.Services.Eventing/Program.cs | 18 +- .../ScalarIntegrationTest.cs | 53 ----- 11 files changed, 84 insertions(+), 410 deletions(-) delete mode 100644 Visage.AppHost/ScalarExtensions.cs delete mode 100644 Visage.ServiceDefaults/ScalarExtensions.cs delete mode 100644 docs/examples/sample-apphost-program.cs delete mode 100644 docs/examples/sample-service-program.cs delete mode 100644 tests/Visage.Test.Aspire/ScalarIntegrationTest.cs diff --git a/Visage.AppHost/Program.cs b/Visage.AppHost/Program.cs index 6640782..09bd8ff 100644 --- a/Visage.AppHost/Program.cs +++ b/Visage.AppHost/Program.cs @@ -29,7 +29,13 @@ #region EventAPI var EventAPI = builder.AddProject("event-api") - .WithScalarApiDocumentation("Visage Event API"); + .WithUrlForEndpoint("http", + url => url.DisplayLocation = UrlDisplayLocation.DetailsOnly) // Hide the plain-HTTP link from the Resources grid + .WithUrlForEndpoint("https", url => + { + url.DisplayText = "Event API Scalar OpenAPI"; + url.Url += "/scalar/v1"; + }); @@ -41,7 +47,13 @@ .WithEnvironment("Auth0__Domain", iamDomain) .WithEnvironment("Auth0__Audience", iamAudience) .WithReference(VisageSQL) - .WithScalarApiDocumentation("Visage Registration API"); + .WithUrlForEndpoint("http", + url => url.DisplayLocation = UrlDisplayLocation.DetailsOnly) // Hide the plain-HTTP link from the Resources grid + .WithUrlForEndpoint("https", url => + { + url.DisplayText = "Registration API Scalar OpenAPI"; + url.Url += "/scalar/v1"; + }); #endregion diff --git a/Visage.AppHost/ScalarExtensions.cs b/Visage.AppHost/ScalarExtensions.cs deleted file mode 100644 index 740c6cb..0000000 --- a/Visage.AppHost/ScalarExtensions.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Aspire.Hosting; -using Aspire.Hosting.ApplicationModel; - -namespace Aspire.Hosting; - -/// -/// Provides extension methods for adding Scalar API documentation to Aspire applications. -/// -public static class ScalarResourceExtensions -{ - /// - /// Configures a project resource to expose Scalar API documentation. - /// This adds the necessary URL endpoint configuration for Scalar UI. - /// - /// The project resource to configure. - /// The title for the API documentation. If null, uses the resource name. - /// The path where Scalar UI will be served. Defaults to "/scalar/v1". - /// The project resource for chaining. - public static IResourceBuilder WithScalarApiDocumentation( - this IResourceBuilder projectResource, - string? title = null, - string scalarPath = "/scalar/v1") - { - var apiTitle = title ?? $"{projectResource.Resource.Name} API"; - - return projectResource - .WithHttpEndpoint(port: null, name: "http") - .WithHttpsEndpoint(port: null, name: "https") - .WithUrlForEndpoint("http", url => - url.DisplayLocation = UrlDisplayLocation.DetailsOnly) // Hide the plain-HTTP link from the Resources grid - .WithUrlForEndpoint("https", url => - { - url.DisplayText = $"{apiTitle} Scalar OpenAPI"; - url.Url += scalarPath; - }); - } - - /// - /// Configures a project resource to expose Scalar API documentation with custom endpoint configuration. - /// - /// The project resource to configure. - /// Action to configure the Scalar endpoint. - /// The project resource for chaining. - public static IResourceBuilder WithScalarApiDocumentation( - this IResourceBuilder projectResource, - Action configure) - { - return projectResource - .WithHttpEndpoint(port: null, name: "http") - .WithHttpsEndpoint(port: null, name: "https") - .WithUrlForEndpoint("http", url => - url.DisplayLocation = UrlDisplayLocation.DetailsOnly) // Hide the plain-HTTP link from the Resources grid - .WithUrlForEndpoint("https", configure); - } -} \ No newline at end of file diff --git a/Visage.ServiceDefaults/Extensions.cs b/Visage.ServiceDefaults/Extensions.cs index 2eaa4b5..c404e89 100644 --- a/Visage.ServiceDefaults/Extensions.cs +++ b/Visage.ServiceDefaults/Extensions.cs @@ -20,8 +20,6 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu builder.ConfigureOpenTelemetry(); builder.AddDefaultHealthChecks(); - - builder.AddScalarDefaults(); builder.Services.AddServiceDiscovery(); diff --git a/Visage.ServiceDefaults/ScalarExtensions.cs b/Visage.ServiceDefaults/ScalarExtensions.cs deleted file mode 100644 index c1df88d..0000000 --- a/Visage.ServiceDefaults/ScalarExtensions.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Scalar.AspNetCore; - -namespace Microsoft.Extensions.Hosting; - -/// -/// Provides extension methods for configuring Scalar API documentation in Aspire applications. -/// -public static class ScalarExtensions -{ - /// - /// Adds Scalar API documentation support to the service defaults. - /// This configures OpenAPI services and provides default Scalar configuration. - /// - /// The host application builder. - /// The host application builder for chaining. - public static IHostApplicationBuilder AddScalarDefaults(this IHostApplicationBuilder builder) - { - // Add OpenAPI services that Scalar depends on - builder.Services.AddOpenApi(); - - return builder; - } - - /// - /// Maps Scalar API reference endpoints with default configuration for Aspire applications. - /// - /// The web application. - /// The title for the API documentation. If null, uses the application name. - /// The path where Scalar UI will be served. Defaults to "/scalar/v1". - /// The web application for chaining. - public static WebApplication MapScalarDefaults(this WebApplication app, string? title = null, string? path = null) - { - if (app.Environment.IsDevelopment()) - { - // Map OpenAPI endpoint - app.MapOpenApi(); - - // Configure and map Scalar API reference - app.MapScalarApiReference(options => - { - var apiTitle = title ?? app.Environment.ApplicationName ?? "API"; - options.WithTitle(apiTitle) - .WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient); - }); - } - - return app; - } - - /// - /// Maps Scalar API reference endpoints with custom configuration. - /// - /// The web application. - /// Action to configure Scalar options. - /// The web application for chaining. - public static WebApplication MapScalarDefaults(this WebApplication app, Action configure) - { - if (app.Environment.IsDevelopment()) - { - // Map OpenAPI endpoint - app.MapOpenApi(); - - // Configure and map Scalar API reference with custom options - app.MapScalarApiReference(configure); - } - - return app; - } -} \ No newline at end of file diff --git a/Visage.ServiceDefaults/Visage.ServiceDefaults.csproj b/Visage.ServiceDefaults/Visage.ServiceDefaults.csproj index 1dd804b..90329be 100644 --- a/Visage.ServiceDefaults/Visage.ServiceDefaults.csproj +++ b/Visage.ServiceDefaults/Visage.ServiceDefaults.csproj @@ -19,7 +19,7 @@ - + diff --git a/Visage.Services.Registrations/Program.cs b/Visage.Services.Registrations/Program.cs index 030c99a..36ef014 100644 --- a/Visage.Services.Registrations/Program.cs +++ b/Visage.Services.Registrations/Program.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.SqlServer; +using Scalar.AspNetCore; using Visage.Services.Registration; using Visage.Shared.Models; @@ -53,10 +54,21 @@ logging.RequestHeaders.Add("Authorization"); }); +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); + var app = builder.Build(); // Configure the HTTP request pipeline. -app.MapScalarDefaults("Visage Registration API"); +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); + app.MapScalarApiReference(options => + { + options.WithTitle("Visage Registration API") + .WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient); + }); +} app.UseHttpsRedirection(); app.UseAuthentication(); diff --git a/docs/examples/sample-apphost-program.cs b/docs/examples/sample-apphost-program.cs deleted file mode 100644 index 86ad16a..0000000 --- a/docs/examples/sample-apphost-program.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Aspire.Hosting; - -var builder = DistributedApplication.CreateBuilder(args); - -// Add services with Scalar API documentation -var sampleAPI = builder.AddProject("sample-api") - .WithScalarApiDocumentation("Sample API"); - -var userAPI = builder.AddProject("user-api") - .WithScalarApiDocumentation("User Management API", "/docs"); - -var orderAPI = builder.AddProject("order-api") - .WithScalarApiDocumentation("Order Processing API"); - -// Add frontend application -var webapp = builder.AddProject("webapp") - .WithReference(sampleAPI) - .WithReference(userAPI) - .WithReference(orderAPI); - -builder.Build().Run(); \ No newline at end of file diff --git a/docs/examples/sample-service-program.cs b/docs/examples/sample-service-program.cs deleted file mode 100644 index 4062f2b..0000000 --- a/docs/examples/sample-service-program.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -var builder = WebApplication.CreateBuilder(args); - -// Add service defaults & Aspire components (includes Scalar) -builder.AddServiceDefaults(); - -// Add your services -builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("SampleData")); - -var app = builder.Build(); - -// Configure Scalar API documentation -app.MapScalarDefaults("Sample API"); - -app.UseHttpsRedirection(); - -// Define API endpoints -var api = app.MapGroup("/api/items"); - -api.MapGet("/", async (SampleDb db) => await db.Items.ToListAsync()) - .WithName("GetAllItems") - .WithOpenApi(); - -api.MapGet("/{id}", async (int id, SampleDb db) => - await db.Items.FindAsync(id) is Item item - ? Results.Ok(item) - : Results.NotFound()) - .WithName("GetItem") - .WithOpenApi(); - -api.MapPost("/", async (Item item, SampleDb db) => -{ - db.Items.Add(item); - await db.SaveChangesAsync(); - return Results.Created($"/api/items/{item.Id}", item); -}) - .WithName("CreateItem") - .WithOpenApi(); - -app.Run(); - -// Sample data models -public class Item -{ - public int Id { get; set; } - public string Name { get; set; } = string.Empty; - public string Description { get; set; } = string.Empty; -} - -public class SampleDb : DbContext -{ - public SampleDb(DbContextOptions options) : base(options) { } - public DbSet Items { get; set; } -} \ No newline at end of file diff --git a/docs/scalar-aspire-integration.md b/docs/scalar-aspire-integration.md index c0824a1..3beefb9 100644 --- a/docs/scalar-aspire-integration.md +++ b/docs/scalar-aspire-integration.md @@ -1,149 +1,29 @@ # Scalar Aspire Integration -This document describes how to use Scalar API documentation with .NET Aspire in the Visage project. +This document describes how Scalar API documentation is integrated with .NET Aspire in the Visage project, following the official Scalar guide. ## Overview -Scalar has been integrated as an Aspire primitive, providing a simplified way to add interactive API documentation to your Aspire applications. The integration includes both service-level configuration and AppHost orchestration support. +Scalar provides interactive API documentation for .NET APIs. This integration follows the official approach recommended in the [Scalar .NET Aspire Integration Guide](https://guides.scalar.com/scalar/scalar-api-references/integrations/net-aspire). -## Features +## Implementation -- **Centralized Configuration**: Scalar setup is included in the default service configuration -- **Aspire Integration**: Easy configuration through the AppHost with the `WithScalarApiDocumentation()` extension method -- **Development Mode**: Automatically enabled only in development environment -- **Customizable**: Support for custom titles, paths, and configurations +### Services Configuration -## Usage - -### In Services (Automatic) - -Services that use `builder.AddServiceDefaults()` automatically get Scalar support. Simply call: - -```csharp -var builder = WebApplication.CreateBuilder(args); - -// Add service defaults (includes Scalar configuration) -builder.AddServiceDefaults(); - -var app = builder.Build(); - -// Map Scalar with default configuration -app.MapScalarDefaults("My API"); - -// Or with custom configuration -app.MapScalarDefaults(options => -{ - options.WithTitle("My Custom API") - .WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient); -}); - -app.Run(); -``` - -### In AppHost - -Configure Scalar endpoints for your services in the AppHost: - -```csharp -var builder = DistributedApplication.CreateBuilder(args); - -// Add project with Scalar documentation -var eventAPI = builder.AddProject("event-api") - .WithScalarApiDocumentation("Event API"); - -// With custom path -var registrationAPI = builder.AddProject("registration-api") - .WithScalarApiDocumentation("Registration API", "/docs/v1"); - -builder.Build().Run(); -``` - -## API Reference - -### ServiceDefaults Extensions - -#### `AddScalarDefaults()` -Adds Scalar support to service defaults, including OpenAPI services. - -#### `MapScalarDefaults(title, path)` -Maps Scalar API reference endpoints with default configuration. - -**Parameters:** -- `title` (optional): The title for the API documentation -- `path` (optional): The path where Scalar UI will be served (defaults to "/scalar/v1") - -### AppHost Extensions - -#### `WithScalarApiDocumentation(title, scalarPath)` -Configures a project resource to expose Scalar API documentation. This method automatically configures HTTP and HTTPS endpoints for the service and sets up the Scalar UI endpoint. - -**Parameters:** -- `title` (optional): The title for the API documentation -- `scalarPath` (optional): The path where Scalar UI will be served (defaults to "/scalar/v1") - -**Note:** This method automatically adds HTTP and HTTPS endpoints to the project resource if they don't exist. - -## Examples - -### Basic Service Setup +Each API service configures Scalar directly in their `Program.cs`: ```csharp -using Microsoft.EntityFrameworkCore; - var builder = WebApplication.CreateBuilder(args); -// Add service defaults & Aspire components (includes Scalar) +// Add service defaults & Aspire components builder.AddServiceDefaults(); -builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("MyList")); - -var app = builder.Build(); - -// Configure Scalar with service name -app.MapScalarDefaults("My Service API"); - -app.UseHttpsRedirection(); - -// Your API endpoints -var api = app.MapGroup("/api"); -api.MapGet("/items", GetItems).WithOpenApi(); - -app.Run(); -``` - -### AppHost Configuration - -```csharp -using Aspire.Hosting; - -var builder = DistributedApplication.CreateBuilder(args); - -// Services with Scalar documentation -var eventAPI = builder.AddProject("event-api") - .WithScalarApiDocumentation("Event Management API"); - -var userAPI = builder.AddProject("user-api") - .WithScalarApiDocumentation("User Management API", "/api-docs"); - -// Frontend references the APIs -var webapp = builder.AddProject("webapp") - .WithReference(eventAPI) - .WithReference(userAPI); - -builder.Build().Run(); -``` - -## Migration Guide - -If you're migrating from manual Scalar configuration: - -### Before -```csharp -// Manual configuration +// Add OpenAPI services builder.Services.AddOpenApi(); var app = builder.Build(); +// Configure Scalar in development if (app.Environment.IsDevelopment()) { app.MapOpenApi(); @@ -155,29 +35,41 @@ if (app.Environment.IsDevelopment()) } ``` -### After -```csharp -// Using Scalar Aspire primitive -builder.AddServiceDefaults(); // Includes OpenAPI and Scalar setup +**Required Package Reference:** +```xml + +``` -var app = builder.Build(); +### AppHost Configuration -app.MapScalarDefaults("My API"); // Handles development check automatically +The AppHost configures dashboard URLs to link directly to Scalar endpoints: + +```csharp +var eventAPI = builder.AddProject("event-api") + .WithUrlForEndpoint("http", url => + url.DisplayLocation = UrlDisplayLocation.DetailsOnly) // Hide HTTP link + .WithUrlForEndpoint("https", url => + { + url.DisplayText = "Event API Scalar OpenAPI"; + url.Url += "/scalar/v1"; // Default Scalar endpoint + }); ``` -## Troubleshooting +## Current Services + +### Event API +- **Service**: Uses `MapScalarApiReference` with title "Visage Event API" +- **AppHost**: Configured with "Event API Scalar OpenAPI" dashboard link +- **URL**: `https://localhost:{port}/scalar/v1` -### Scalar UI not appearing -- Ensure you're running in Development environment -- Check that `AddServiceDefaults()` is called before building the app -- Verify that `MapScalarDefaults()` is called after building the app +### Registration API +- **Service**: Uses `MapScalarApiReference` with title "Visage Registration API" +- **AppHost**: Configured with "Registration API Scalar OpenAPI" dashboard link +- **URL**: `https://localhost:{port}/scalar/v1` -### OpenAPI not working -- Make sure your endpoints use `.WithOpenApi()` extension -- Check that the Microsoft.AspNetCore.OpenApi package is referenced +## Benefits -### AppHost configuration not working -- Ensure the service uses `AddServiceDefaults()` and `MapScalarDefaults()` -- Verify that the project reference in AppHost is correct -- Check that the `WithScalarApiDocumentation()` extension is applied to the correct resource -- Ensure the AppHost project includes the `Scalar.AspNetCore` package reference \ No newline at end of file +- **Development-only**: Scalar UI only appears in development environment +- **Direct Integration**: Services configure Scalar directly following official patterns +- **Dashboard Links**: Aspire dashboard provides direct access to API documentation +- **Standard Endpoints**: Uses default `/scalar/v1` path for consistency \ No newline at end of file diff --git a/services/Visage.Services.Eventing/Program.cs b/services/Visage.Services.Eventing/Program.cs index 798a86c..141ab82 100644 --- a/services/Visage.Services.Eventing/Program.cs +++ b/services/Visage.Services.Eventing/Program.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Scalar.AspNetCore; using Microsoft.AspNetCore.Http.HttpResults; using Visage.Shared.Models; using Microsoft.AspNetCore.Mvc; @@ -15,10 +16,25 @@ builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("EventList")); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); + + +// Add services to the container. +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); + var app = builder.Build(); // Configure the HTTP request pipeline. -app.MapScalarDefaults("Visage Event API"); +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); + app.MapScalarApiReference(options => + { + options.WithTitle("Visage Event API") + .WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient); + } + ); +} app.UseHttpsRedirection(); diff --git a/tests/Visage.Test.Aspire/ScalarIntegrationTest.cs b/tests/Visage.Test.Aspire/ScalarIntegrationTest.cs deleted file mode 100644 index c1cf67b..0000000 --- a/tests/Visage.Test.Aspire/ScalarIntegrationTest.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Aspire.Hosting; -using FluentAssertions; -using TUnit.Assertions; - -namespace Visage.Test.Aspire.Tests; - -public class ScalarIntegrationTest -{ - [Test] - public void WithScalarApiDocumentation_ShouldConfigureEndpoints() - { - // Arrange - var appBuilder = DistributedApplication.CreateBuilder(); - var projectResource = appBuilder.AddProject("test-api"); - - // Act - var configuredResource = projectResource.WithScalarApiDocumentation("Test API"); - - // Assert - configuredResource.Should().NotBeNull(); - configuredResource.Should().BeSameAs(projectResource); - } - - [Test] - public void WithScalarApiDocumentation_WithDefaultTitle_ShouldUseResourceName() - { - // Arrange - var appBuilder = DistributedApplication.CreateBuilder(); - var projectResource = appBuilder.AddProject("test-api"); - - // Act - var configuredResource = projectResource.WithScalarApiDocumentation(); - - // Assert - configuredResource.Should().NotBeNull(); - configuredResource.Should().BeSameAs(projectResource); - } - - [Test] - public void WithScalarApiDocumentation_WithCustomPath_ShouldConfigureCorrectly() - { - // Arrange - var appBuilder = DistributedApplication.CreateBuilder(); - var projectResource = appBuilder.AddProject("test-api"); - - // Act - var configuredResource = projectResource.WithScalarApiDocumentation("Test API", "/docs/v1"); - - // Assert - configuredResource.Should().NotBeNull(); - configuredResource.Should().BeSameAs(projectResource); - } -} \ No newline at end of file