Search
K
  1. API Versioning

API Versioning

Release Group Strategy

This is a simplified route based versioning strategy that requires less effort by the developer compared to traditional versioning. Each endpoint is evolved/versioned independently and ultimately grouped into a Swagger document which we call a "release group". When it's time for an endpoint contract to change, simply leave the existing endpoint alone and create (either by inheriting the old one) or creating a brand-new endpoint class and call the Version(n) method of the endpoint to indicate its version.

For example, let's assume the following:

Initial State

Your app has the following endpoints initially:

/admin/login
/order/{OrderID}

After evolving the login endpoint:

/admin/login
/admin/login/v1 <-- newly added version
/order/{OrderID}

At this point you can have 2 Swagger documents (release groups) that look like the following:

 Initial Release
 |-- /admin/login
 |-- /order/{OrderID}

 Release 1
 |-- /admin/login/v1
 |-- /order/{OrderID}

After another change:

/admin/login
/admin/login/v1
/admin/login/v2     <-- newly added
/order/{OrderID}
/order/{OrderID}/v1 <-- newly added

Your releases can now look like this:

 Initial Release
 |-- /admin/login
 |-- /order/{OrderID}

 Release 1
 |-- /admin/login/v1
 |-- /order/{OrderID}

 Release 2
 |-- /admin/login/v2
 |-- /order/{OrderID}/v1

A release group contains only the latest iteration of each endpoint in your project. All older/previous iterations will not show up. How to define release groups is described below.

Enable Versioning

Simply specify one of the versioning options during startup to activate versioning.

app.UseFastEndpoints(c =>
{
    c.Versioning.Prefix = "v";
});

Versioning Options

At least one of the following settings should be set in order to enable versioning.

  • Prefix : A string to be used in front of the version (for example 'v' produces 'v1')

  • DefaultVersion : This value will be used for endpoints that do not specify a version in its configuration. The default value is 0. When the version of an endpoint is 0 it does not get added to the route making that version the initial version of that endpoint.

  • PrependToRoute : By default, the version string is appended to the endpoint route. By setting this to true, you can have it prepended to the route.

Define Swagger Documents (Release Groups)

Program.cs
bld.Services
   .SwaggerDocument(o =>
   {
       o.DocumentSettings = s =>
       {
           s.DocumentName = "Initial Release";
           s.Title = "My API";
           s.Version = "v0";
       };
   })
   .SwaggerDocument(o =>
   {
       o.MaxEndpointVersion = 1;
       o.DocumentSettings = s =>
       {
           s.DocumentName = "Release 1";
           s.Title = "My API";
           s.Version = "v1";
       };
   })
   .SwaggerDocument(o =>
   {
       o.MaxEndpointVersion = 2;
       o.DocumentSettings = s =>
       {
           s.DocumentName = "Release 2";
           s.Title = "My API";
           s.Version = "v2";
       };
   });

The thing to note here is the MaxEndpointVersion property. This is where you specify the max version of an endpoint which a release group should include. Any endpoint versions that are greater than this number will not be included in that release group/swagger doc. If you don't specify this, only the initial version of each endpoint will be listed in the group.

Mark Endpoint With a Version

public class AdminLoginEndpoint_V2 : Endpoint<LoginRequest, LoginResponse>
{
    public override void Configure()
    {
        Get("admin/login");
        Version(2);
    }
}

Deprecate an Endpoint

You can specify that an endpoint should not be visible after (and including) a given version group like so:

Version(1, deprecateAt: 4);

An endpoint marked as above will be visible in all swagger docs up until MaxEndpointVersion = 4. It will be excluded from docs starting from 4 and above. As an example, take the following release groups with just two endpoints:

Initial Release
|-- /user/delete
|-- /user/profile

Release 1
|-- /user/delete/v1
|-- /user/profile/v1

Release 2
|-- /user/delete/v1
|-- /user/profile/v2

If you mark the /user/delete/v1 endpoint with Version(1, deprecateAt: 2) then Release 2 and newer will not have any /user/delete endpoints listed. The release will look like this:

Release 2
|-- /user/profile/v2

It is only necessary to mark the last endpoint version as deprecated. You can leave all previous iterations alone, if there's any.

Release Version Strategy

The "release group" strategy above automatically includes endpoints in the system that falls under the Swagger doc's defined MaxEndpointVersion. I.e. the doc dictates what will be included. The individual endpoints have no say in which Swagger document they want to be listed under. With the "release version" strategy, the inverse is possible where the Swagger doc simply specifies what "release version" it is like so:

bld.Services.SwaggerDocument(o =>
{
   o.DocumentSettings = d => d.DocumentName = "Release 2";
   o.ReleaseVersion = 2;
})

And, the endpoints decide which "release version" and newer they want to be included in like so:

public override void Configure()
{
    Version(1).StartingRelease(2);
}

An endpoint marked as above only starts showing up in Swagger docs marked ReleaseVersion = 2 and higher. This strategy may be preferred by most as it's closer to the traditional versioning approach in ASP.NET with packages such as Asp.Versioning.Http, but with less verbosity & effort.

Note: If you don't specify StartingRelease(n), it is assumed the starting release is the same version as the endpoint version. For example, if you just specify Version(3), that endpoint will only start showing up in ReleaseVersion = 3 and higher Swagger docs (until being marked as deprecated).

Deprecating endpoints can be done like so:

public override void Configure()
{
    Version(1)
      .StartingRelease(2)
      .DeprecateAt(4);
}

Ad Hoc Grouping Strategy

It's possible to group a bunch of endpoints together into a swagger document by employing an endpoint filter. I.e. only endpoints in your application that matches a condition/predicate will be included in that particular swagger doc.

bld.Services.SwaggerDocument(o =>
{
    o.EndpointFilter = ep => ep.EndpointTags?.Contains("GroupA") is true;
    o.DocumentSettings = s =>
    {
        s.DocumentName = "Group A (v1)";
        s.Title = "My App";
        s.Version = "v1.0";
    };
});

bld.Services.SwaggerDocument(o =>
{
    o.EndpointFilter = ep => ep.EndpointTags?.Contains("GroupB") is true;
    o.DocumentSettings = s =>
    {
        s.DocumentName = "Group B (v1)";
        s.Title = "My App";
        s.Version = "v1.0";
    };
});

If the predicate returns true for a particular endpoint definition, it will be included in the swagger doc. In the above example, only endpoints that has the tag "GroupA" associated with it would be included in the "Group A" swagger doc. It can be any criteria that can be matched against the supplied endpoint definition. The example uses endpoint tags which are specified like so:

public override void Configure()
{
    ...
    Tags("GroupA");
    Version(1);
}

public override void Configure()
{
    ...
    Tags("GroupB");
    Version(1);
}

Asp.Versioning.Http Package Support

The built-in versioning only supports route based versioning. If you'd like to use header based versioning and a more traditional approach to versioning, it's possible to use the Asp.Versioning.Http package with FastEndpoints as shown below.

Install the wrapper library from nuget:

package manager
Install-Package FastEndpoints.AspVersioning

Create a couple of Version Sets where you specify a name for the API as well as the different versions each API has like so:

VersionSets.CreateApi(">>Orders<<", v => v
    .HasApiVersion(1.0)
    .HasApiVersion(2.0));

VersionSets.CreateApi(">>Inventory<<", v =>
{
    v.HasApiVersion(1.0);
    v.HasApiVersion(1.1);
});

Enable the versioning middleware by specifying the default configuration:


bld.Services
   .AddFastEndpoints()
   .AddVersioning(o =>
   {
       o.DefaultApiVersion = new(1.0);
       o.AssumeDefaultVersionWhenUnspecified = true;
       o.ApiVersionReader = new HeaderApiVersionReader("X-Api-Version");
   })

Define swagger documents for each version your application has:

bld.Services
   .SwaggerDocument(o =>
   {
       o.DocumentSettings = x =>
       {
           x.DocumentName = "version one";
           x.ApiVersion(new(1.0));
       };
       o.AutoTagPathSegmentIndex = 0; //need to disable path segment based auto tagging
   })
   .SwaggerDocument(o =>
   {
       o.DocumentSettings = x =>
       {
           x.DocumentName = "version one point one";
           x.ApiVersion(new(1.1));
       };
       o.AutoTagPathSegmentIndex = 0;
   })
   .SwaggerDocument(o =>
   {
       o.DocumentSettings = x =>
       {
           x.DocumentName = "version two";
           x.ApiVersion(new(2.0));
       };
       o.AutoTagPathSegmentIndex = 0;
   });

Now all that's left to do is to associate the endpoints with the relevant Version Set (API) by name like so:

public class GetInvoices_v1 : EndpointWithoutRequest
{
    public override void Configure()
    {
        Get("/orders/invoices");
        Options(x => x
            .WithVersionSet(">>Orders<<")
            .MapToApiVersion(1.0));
    }

    public override async Task HandleAsync(CancellationToken c)
    {
        await SendAsync("v1 - orders");
    }
}

public class GetInvoices_v2 : EndpointWithoutRequest
{
    public override void Configure()
    {
        Get("/order/invoices");
        Options(x => x
            .WithVersionSet(">>Orders<<")
            .MapToApiVersion(2.0));
    }

    public override async Task HandleAsync(CancellationToken c)
    {
        await SendAsync("v2 - orders");
    }
}

See this gist for the full sample application.


© FastEndpoints 2024