Skip to content

Route-based API versioning for ASP.NET Core minimal APIs.

License

Notifications You must be signed in to change notification settings

ravindUwU/route-versioning

Repository files navigation

Route Versioning

Route-based API versioning for ASP.NET Core. This library works by exhaustively mapping all corresponding API versions, on startup. Only minimal APIs are currently supported.

The following packages are available,

  • RouteVersioning
  • RouteVersioning.OpenApi

Warning

This project is a work in progress!! The examples below will likely change as I finalise its APIs. My to-do list is:

  • Minimal APIs
  • OpenAPI
  • Sunset header
  • Tests
  • NuGet package
  • Controllers, if possible?

Usage

Mapping a Versioned API

  1. Define all available API versions with a RouteVersionSetBuilder.

    // In this case, the API has 3 versions.
    var versions = new RouteVersionSetBuilder<int>()
    	.Version(1)
    	.Version(2)
    	.Version(3)
    	.Build();
  2. Use WithVersions to map versioned endpoints, specifying the range of versions to which they apply.

    var api = app.MapGroup("api").WithVersions(versions);
    • Use From to map an endpoint that is available from a specific version onward.

      // api/v1/a (introduced)
      // api/v2/a (unchanged)
      // api/v3/a (unchanged)
      api.From(1).MapGet("a", () => ...);
      
      // api/v2/b (introduced)
      // api/v3/b (unchanged)
      api.From(2).MapGet("b", () => ...);
      
      // api/v3/c (introduced)
      api.From(3).MapGet("c", () => ...);
    • Use Between to map an endpoint that is available in all versions within an inclusive range of versions.

      // api/v1/d (introduced)
      // api/v2/d (unchanged; retired)
      api.Between(1, 2).MapGet("d", () => ...);
    • Combine the two to revise an endpoint at a specific version.

      // api/v1/e (introduced)
      // api/v2/e (unchanged)
      api.Between(1, 2).MapGet("e", () => ...);
      
      // api/v3/e (revised)
      api.From(3).MapGet("e", () => ...);

Retiring an API Version

  • Use Sunset to indicate an API version being retired, which adds the specified details as Sunset and Link headers as described in RFC 8594, to its responses.

    var versions = new RouteVersionSetBuilder<int>()
    	.Version(1, (v) => v
    		.Sunset(
    			at: someDateTime,
    			link: "https://example.com/changelog/v2-migration",
    			linkMediaType: "text/html"
    		)
    	)
    	.Build();
    HTTP/1.1 200 OK
    Sunset: Tue, 24 Dec 2024 12:41:24 GMT
    Link: <https://example.com/changelog/v2-migration>; rel="sunset"; type="text/html"

Adding Endpoint Conventions

  • To add a convention that applies to a specific endpoint across a range of API versions (v*/a), add a convention as you normally would, after the Map* call.

    api.Between(1, 2).MapGet("a", () => ...).AddEndpointFilter<UwuifyFilter>();
    api.From(3).MapGet("a", () => ...).AddEndpointFilter<UwuifyFilter>();
  • To add a convention that applies to all endpoints of a specific API version (v1/*), use the configuration delegate of RouteVersionSetBuilder.Version.

    var versions = new RouteVersionSetBuilder<int>()
     	.Version(1, (v) => v
    		.AddEndpointFilter<IEndpointConventionBuilder, UwuifyFilter>()
    	)
     	.Build();

Generating OpenAPI Documents

  • The default Microsoft.AspNetCore.OpenApi document includes all versioned APIs.

    services.AddOpenApi("current");
    app.MapOpenApi();
    
    // openapi/current.json includes:
    //     api/v1/{a,d,e}
    //     api/v2/{a,b,d,e}
    //     api/v3/{a,b,c,e}

    To exclude versioned operations from this document, use ExcludeVersionedOperations().

    services.AddOpenApi("current", (options) => options
    	.ExcludeVersionedOperations()
    );
  • Use AddVersionedOpenApi to add version-specific OpenAPI documents. To exclude unversioned endpoints from these documents, specify includeUnversionedEndpoints: false.

    services.AddVersionedOpenApi(versions, includeUnversionedEndpoints: false);
    
    // openapi/v1.json includes api/v1/{a,d,e}
    // openapi/v2.json includes api/v2/{a,b,d,e}
    // openapi/v3.json includes api/v3/{a,b,c,e}

    Use the configuration delegate of RouteVersionSetBuilder.Version to configure version-specific OpenAPI options.

    new RouteVersionSetBuilder<int>().Version(1, (v) => v
    	.ConfigureOpenApiInfo((i) => i.Description = "v1 description")
    	.ConfigureOpenApiOptions(...)
    );
  • Operations of retired API versions will be marked deprecated in version-specific OpenAPI documents. To do the same in other OpenAPI documents, use MarkSunsettedOperations().

    services.AddOpenApi("current", (options) => options
    	.MarkSunsettedOperations()
    );

Configuring OpenAPI UIs

  • Scalar.AspNetCore uses the OpenAPI document specified in the path.

    // scalar/current
    // scalar/v1
    // scalar/v2
    // scalar/v3
    app.MapScalarApiReference();
  • Swagger UI can be configured with URLs of all generated OpenAPI documents.

    app.MapGet("swagger", () =>
    {
    	var urls = new[] { "current" }
    		.Concat(versions.Select(versions.GetSlug))
    		.Select((name) => new { name, url = $"/openapi/{name}.json" });
    
    	return Results.Content(contentType: "text/html", content: $$"""
    		<!DOCTYPE html>
    		<html>
    		<head>
    			<meta name="viewport" content="width=device-width, initial-scale=1" />
    			<style>body { margin: 0 }</style>
    			<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/swagger-ui.css" />
    		</head>
    		<body>
    			<div id="swagger-ui"></div>
    			<script src="https://cdn.jsdelivr.net/npm/[email protected]/swagger-ui-bundle.js"></script>
    			<script src="https://cdn.jsdelivr.net/npm/[email protected]/swagger-ui-standalone-preset.js"></script>
    			<script>
    				SwaggerUIBundle({
    					urls: {{JsonSerializer.Serialize(urls)}},
    					dom_id: '#swagger-ui',
    					presets: [
    						SwaggerUIBundle.presets.apis,
    						SwaggerUIStandalonePreset,
    					],
    					layout: 'StandaloneLayout',
    				});
    			</script>
    		</body>
    		</html>
    		"""
    	);
    })

Alternatives

If you're after a library that supports versioning using query parameters, request headers, content negotiation, etc., consider Asp.Versioning which should be capable of everything that this library offers, with some configuration. Comparatively, this one,

  • Offers just one API versioning scheme (route-based).
  • Maps all versions at startup, instead of resolving API versions as requests are made (as I understand).
  • Uses API version ranges (i.e., vX-onward/between vX & Y inclusive) by default.

About

Route-based API versioning for ASP.NET Core minimal APIs.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published