Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Proposal: Centralized Swagger UI for OpenAPI Specification Files #3608

Open
AClerbois opened this issue Apr 11, 2024 · 17 comments
Open
Labels
area-integrations Issues pertaining to Aspire Integrations packages
Milestone

Comments

@AClerbois
Copy link

Feature Proposal

Summary

In the context of the .NET Aspire project, I propose to add a new feature aimed at consolidating all OpenAPI specification files across the project into a unified Swagger UI interface. This would greatly enhance the developer experience by providing a single, centralized location to view and interact with the API documentation generated from OpenAPI specifications.

Problem Statement

Currently, OpenAPI specification files are scattered throughout various parts of our project, making it challenging to locate, view, and test different APIs efficiently. Developers and API consumers have to navigate through multiple Swagger UI instances or look into different directories to find the relevant API documentation. This fragmentation hinders productivity and can lead to inconsistencies in how APIs are understood and used.

Proposed Solution

Implement a mechanism within the .NET Aspire project that automatically scans the project directories for OpenAPI specification files (*.json or *.yaml). Once identified, these files will be aggregated into a single Swagger UI instance. This unified Swagger UI will be automatically updated to reflect changes in the OpenAPI specifications, ensuring that the documentation is always current.

Benefits

  • Centralization: Provides a one-stop-shop for all API documentation, making it easier for developers to find and use the APIs they need.
  • Improved Developer Experience: Facilitates a smoother development process by reducing the time spent searching for API documentation.
  • Consistency: Ensures that all API documentation is presented in a consistent manner, aiding in understanding and integration efforts.
  • Automation: Reduces manual effort required to maintain API documentation visibility as the project evolves.

Implementation Considerations

  • The scanning mechanism should be configurable to allow for inclusion or exclusion of specific directories or files.
  • Consideration should be given to how the unified Swagger UI is hosted within the project (e.g., as part of the build process, within a container, etc.).
  • Security implications of aggregating and exposing API documentation in a single location should be assessed and mitigated.

Request for Comments

I invite the community to provide feedback on this proposal. Any insights on potential challenges, additional benefits, or alternative approaches to achieving this goal would be greatly appreciated.

@davidfowl davidfowl added this to the Backlog milestone Apr 12, 2024
@davidfowl
Copy link
Member

Cool idea, related to #2980

@sayedihashimi
Copy link
Member

If we do this we should also support it in Visual Studio.

@davidfowl davidfowl added feature area-integrations Issues pertaining to Aspire Integrations packages and removed area-dashboard labels Sep 16, 2024
@Foxtrek64
Copy link

Just some (half-baked) general thoughts to help spur discussion

  • Dedicated Swagger service container
var builder = DistributedApplication.CreateBuilder(args);
var swagger = builder.AddSwaggerHost("swagger");

builder.AddProject<Projects.MyCoolProject>("web")
       .WithReference(swagger);
  • A few discovery mechanism ideas
var swagger = builder.AddSwaggerHost("swagger")
    .WithSwaggerFile("**/*.swagger.json"); // With glob to point to swagger files

var someService = builder.AddNonDotNetService("foo")
    .WithSwagger(); // Search output path for swagger file. Possibly accept glob pattern again. Intended scenario is including a swagger file from a docker image or something similar.

var web = builder.AddProject<Projects.MyCoolProject>("web")
    .WithReference(swagger);

// In MyCoolProject.Project.cs
var builder = WebApplication.CreateBuilder(args);
builder.AddSwagger("swagger"); // Wraps standard swagger packages to produce the swagger json file, then provides that back to the swagger host.

My Questions

  • What kind of ergonomics do we want here?
  • Should supporting DotNet projects (a la AddProject<TProject>(string)) feel different than supporting non-DotNet projects (e.g. a PHP web service)?
    • If so, how? Why?
  • How should we handle IDP support, e.g. if Keycloak is used as an auth source for the application overall, how do we secure Swagger with it?
    • This likely falls under the umbrella of IDP integration with Aspire overall and might be a little out of scope.

@davidfowl davidfowl removed the feature label Oct 16, 2024
@captainsafia
Copy link
Member

@Foxtrek64 I like the way that you've modeled this implementation as an integration in your proposal, with both a client and hosting component.

In .NET 9, we added built-in support for OpenAPI document generation via the Microsoft.AspNetCore.OpenApi package. We could consider what it would look like to add a client integration for this, that could help solve issues like this one.

With regard to the hosting side of this, I saw it being inverted a bit:

var apiService1 = builder.AddProject<>();
var apiService2 = builder.AddProject<>();

var swagger = builder.AddSwaggerUi()
  .WithReference(apiService1)
  .WithReference(apiService2);

This way a user could navigate to a certain gateway that incorporated the OpenAPI documentation for all the API services in their application. You could potentially use the document switcher UI to keep things truly centralized.

Side note: although Swagger UI is popular, I've become quite partial to Scalar over the past few months. It would be interesting to explore adding an Aspire hosting integration for Scalar that provided this functionality. @xC0dex -- what are your thoughts on prototyping an Aspire hosting integration around Scalar and seeing what it would look like?

Should supporting DotNet projects (a la AddProject(string)) feel different than supporting non-DotNet projects (e.g. a PHP web service)?
If so, how? Why?

I'm inclined to say the ergonomics should be the same across all project types, especially if the API reference document is an independent resource in-and-off itself instead of something that gets added via a WithReference on the API service....or do we support both models?

One more thing, I was originally gonna close this issue in favor of centralizing all the discussion in #2980 but I think we should keep these two as independent and separate ideas.

  • This issue is about adding hosting/client integrations for OpenAPI document + Swagger UI/Scalar UI/etc.
  • The other issue is about providing an in-dashboard experience for ad-hoc testing and would require spinning up a new UI altogether.

I think the first issue (this one) is a lot more tractable given the APIs that we currently support and the prototyping that has already been done in this project.

@Foxtrek64
Copy link

Foxtrek64 commented Oct 25, 2024

@captainsafia I like the inverted model. By providing the service reference to the service with the open api spec, it makes it really easy to keep the ergonomics consistent regardless of whether the service generating the open api doc is .NET or not.

Also, I think the best way to handle the question of swagger vs scalar or other potential implementations could be something like

var openApi = builder.AddOpenApi();
var swagger = openApi.WithSwaggerUi();
var scalar = openApi.WithScalarUi();
// others

Essentially, the AddOpenApi() piece would produce an OpenApiBuilder which is responsible for aggregating all of the input documents. Vendors, be it Swagger or Scalar or someone else, can then come in and extend OpenApiBuilder to provide output to their web interface.

This also means a really nice tie-in to #2980 - the in-dashboard experience would simply be another output source, just like Swagger and Scalar.

builder.AddOpenApi().WithDashboardUi();

Edit: component registration example
I imagine that the order in which things are added to the builder do not matter, and you should in theory be able to add multiple front ends here - each would just register a container hosting Swagger or Scalar or whatever at its own endpoint. I can't think of a scenario for when you'd want that, but I don't see why it wouldn't be possible.

var openApi = builder.AddOpenApi()
    .WithScalarUi()
    .WithReference(apiService1)
    .WithReference(apiService2);

@captainsafia
Copy link
Member

Also, I think the best way to handle the question of swagger vs scalar or other potential implementations could be something like

var openApi = builder.AddOpenApi();
var swagger = openApi.WithSwaggerUi();
var scalar = openApi.WithScalarUi();
// others

I assume this is code that someone would write in their app host. If so, what purpose does the AddOpenApi call and OpenApiBuilder serve? Is it mostly so you can chain different front-end to the same resource instance?

I think it is a comparison between:

var openApi = builder.AddOpenApi()
    .WithScalarUi()
    .WithSwaggerUi()
    .WithReference(apiService1)
    .WithReference(apiService2);

OR

var scalar = builder.AddScalarUi();
var swagger = builder.AddSwaggerUi();

scalar
  .WithReference(apiService1)
  .WithReference(apiService2);

swagger
  .WithReference(apiService1)
  .WithReference(apiService2);

With the first option being DRYer but requiring more new API. I don't think there's a technical need for an OpenApiBuilder (for example, some requirement to share state between the two that isn't available on IResourceBuilder) but not sure...

@xC0dex
Copy link

xC0dex commented Oct 25, 2024

Hey @captainsafia,
this sounds like a great idea! I'm a fan of Scalar too, so I would be happy to work on an integration for Aspire. I haven’t worked with Aspire before, so I might need a little support 👀.

@captainsafia
Copy link
Member

@xC0dex Nice! I'd recommend checking out https://github.com/davidfowl/AspireSwaggerUI as an example to start with. You'll notice if you look at this file that it uses an approach for serving the Swagger UI that is very familiar to what the Scalar.AspNetCore package currently does so you can use that as a building block for this.

Most of the Aspire-specific logic is in this file and deals with wiring up the endpoints directly to what the API service is actually listening on.

Note: the repo above doesn't use the inverted model that I proposed in #3608 (comment) but it's a good start nonetheless.

@Foxtrek64
Copy link

I assume this is code that someone would write in their app host. If so, what purpose does the AddOpenApi call and OpenApiBuilder serve? Is it mostly so you can chain different front-end to the same resource instance?

Essentially, yes. I think the primary concern here is the input logic - how we aggregate the various sources into a single resource instance that can be given to the chosen front-end. In my opinion, as someone implementing Swagger or Scalar or whatever else, I don't necessarily care where my input comes from or how it's created, I just care that I get input that is in the correct format. I also don't feel it's necessarily my responsibility to need to implement that logic myself. This would lead to a situation where there is inconsistency between builders.

I don't think there's a technical need for an OpenApiBuilder (for example, some requirement to share state between the two that isn't available on IResourceBuilder) but not sure...

I agree. Having looked at some other examples already in the ecosystem, it seems implementing our own builder type would be somewhat unusual. Take this example with Postgres, for example:

var database = builder.AddPostgres("database") // IResourceBuilder<PostgresServerResource>
    .WithPgAdmin();

Here, they're doing what we're trying to accomplish - some back-end service with a bolt-on GUI. I think it may be good to follow the example here - forego the OpenApiBuilder type for IResourceBuilder<OpenApiResource> or something of the like.

With this in mind, I think the first option makes the most sense, both from a consumer standpoint and from a vendor standpoint.

var openApi = builder.AddOpenApi() // IResourceBuilder<OpenApiResource>
    .WithDashboardUi()
    .WithResource(apiResource1)
    .WithResource(apiResource2);

@MermaidIsla
Copy link

I was bored over the weekend so I decided to throw together a quick partial prototype of this feature, in the current context that would be the builder.AddOpenApi().WithDashboardUi();.

I haven't worked with the aspire repo before so this is my first attempt at making something functional.

Forked repo here

Prototype showcase:
https://github.com/user-attachments/assets/ccfeccdb-0d41-4121-a478-b8c1c977441f

@samsp-msft
Copy link
Member

@mikekistler FYI

@samsp-msft
Copy link
Member

If we do this we should also support it in Visual Studio.

+1 - personally, I think that the .http file support is a much better experience than the web based explorers. What is (or was the last time I looked) a good experience to go from a list of APIs on an endpoint to generating a block in the http file for calling that API.

For scenarios like this, I would like to improve the integration between VS and the Aspire dashboard. If an Aspire AppHost has the swagger annotations, what if that resulted in a "API Test" command in the resource view in the dashboard. Clicking it would take you to the .http file for that service, and enumerate the endpoints/APIs available.

@Foxtrek64
Copy link

Foxtrek64 commented Oct 29, 2024

For scenarios like this, I would like to improve the integration between VS and the Aspire dashboard. If an Aspire AppHost has the swagger annotations, what if that resulted in a "API Test" command in the resource view in the dashboard. Clicking it would take you to the .http file for that service, and enumerate the endpoints/APIs available.

To make sure I'm understanding your idea - if we take my example above, you're proposing that we take the AddOpenApi() and bake it in to the Aspire AppHost. And if it detects an OpenAPI file for any members, it automatically renders it on a built-in page in the dashboard - no WithDashboardUi() required.

I'd be on board with that approach, provided that we get a way to extract that OpenAPI information in case someone wants to expose Swagger or Scalar or some alternative UI, which could be good for production purposes where the Aspire Dashboard may be less appropriate.

@MermaidIsla
Copy link

With this in mind, I think the first option makes the most sense, both from a consumer standpoint and from a vendor standpoint.

var openApi = builder.AddOpenApi() // IResourceBuilder<OpenApiResource>
    .WithDashboardUi()
    .WithResource(apiResource1)
    .WithResource(apiResource2);

I've been thinking about this and I probably like this design the most because it leaves a room for easy configuration.

Let's say I would want to configure the path to the OpenApi document. I think something like builder.AddOpenApi(["/path/to/document.json"]) would be nice. It being an array, it would also allow to specify multiple paths.

Further more I think it would be nice to also have the ability to define specific paths for selected resources, so something like .WithResource(apiResource, ["/path/to/extra/document.json"]).

So putting it all together it would look something like this:

var openApi = builder.AddOpenApi(["/path/to/document.json"]) // IResourceBuilder<OpenApiResource>
    .WithDashboardUi()
    .WithResource(apiResource1)                                    // This has one document path.
    .WithResource(apiResource2, ["/path/to/extra/document.json"]); // This has two document paths.

@xC0dex
Copy link

xC0dex commented Nov 1, 2024

Hey @captainsafia,

I’ve put together an MVP for the Scalar integration, based on the AspireSwaggerUI project. Currently, it uses the CDN version of the Scalar API Reference and isn’t configurable yet. From my understanding, the plan for how the UI will be added isn’t fully settled yet, so I didn't spend much time there.

Even though it’s still in a very early stage, I was wondering: should the UI/integration be fully configurable? And would it make sense to use the Scalar.AspNetCore package here, or should everything be shipped through Aspire or an Aspire integration package? I think it would also be possible to update the package or introduce a new one.

During the development, I thought it would enhance the developer experience if the OpenAPI documents from all services could be fetched automagically. I’m considering an endpoint that could provide all the necessary information needed for the integration.

@Foxtrek64
Copy link

I've been thinking about this and I probably like this design the most because it leaves a room for easy configuration.

Let's say I would want to configure the path to the OpenApi document. I think something like builder.AddOpenApi(["/path/to/document.json"]) would be nice. It being an array, it would also allow to specify multiple paths.

Further more I think it would be nice to also have the ability to define specific paths for selected resources, so something like .WithResource(apiResource, ["/path/to/extra/document.json"]).

I really like this configuration approach. A params string[] would work very nicely here. That said, I would really like to push for globbing via Microsoft.Extensions.FileSystemGlobbing. I think it'd be nice to have something like this:

// Just include paths
builder.AddOpenApi(["**/*.document.json", "**/*.swagger.json"]);

// More complicated example. This is NOT intended to be equivalent to the
// preceding example.

Matcher matcher = new();
matcher.AddInclude("**/*.document.json");
matcher.AddExclude("**/*.swagger.json");
builder.AddOpenApi(matcher);

This would of course also apply to IResourceBuilder<OpenApiResource>#WithResource() as well, accepting either params string or a matcher instance.

@MermaidIsla
Copy link

Ooh, I wasn't aware of that class. I like it. This then gives me an idea of having two string[] in builder.AddOpenApi() method allowing to specify both include paths and exclude paths:

// First parameter are include paths, second parameter are exclude paths.
builder.AddOpenApi(["**/*.document.json"], ["**/*.swagger.json"]);

// This complicated example below now matches the preceding example.
Matcher matcher = new();
matcher.AddInclude("**/*.document.json");
matcher.AddExclude("**/*.swagger.json");
builder.AddOpenApi(matcher);

// Writing just exclude paths would be like this
builder.AddOpenApi([], ["**/*.swagger.json"]);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-integrations Issues pertaining to Aspire Integrations packages
Projects
None yet
Development

No branches or pull requests

8 participants