Search
K
  1. API Versioning

API Versioning

The versioning strategy in FastEndpoints is simplified to require way less effort by the developer.

Basically, you evolve/version each endpoint in your project independently and group them into a release number/name using Swagger.

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(x) method in the configuration to indicate that this is the latest incarnation of the endpoint.

For example, let's assume the following:

Initial State

Your app has the following endpoints:

/admin/login
/inventory/order/{OrderID}

After Evolving an Endpoint

/admin/login
/admin/login/v1
/inventory/order/{OrderID}

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

 - initial release
 |- /admin/login
 |- /inventory/order/{OrderID}

 - release 1.0
 |- /admin/login/v1
 |- /inventory/order/{OrderID}

After Another Change

- /admin/login
- /admin/login/v1
- /admin/login/v2
- /inventory/order/{OrderID}
- /inventory/order/{OrderID}/v1

Your releases can now look like this:

 - initial release
 |- /admin/login
 |- /inventory/order/{OrderID}

 - release 1.0
 |- /admin/login/v1
 |- /inventory/order/{OrderID}

 - release 2.0
 |- /admin/login/v2
 |- /inventory/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 VersioningOptions settings in startup config in order to activate versioning.

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

Define Swagger 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.0";
           s.Title = "my api";
           s.Version = "v1.0";
       };
   })
   .SwaggerDocument(o =>
   {
       o.MaxEndpointVersion = 2;
       o.DocumentSettings = s =>
       {
           s.DocumentName = "Release 2.0";
           s.Title = "my api";
           s.Version = "v2.0";
       };
   });

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<Request>
{
    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 two endpoints:

Initial release

/user/delete
/user/profile

Release Group v1.0

/user/delete/v1
/user/profile/v1

Release Group v2.0

/user/delete/v1
/user/profile/v2

If you mark the /user/delete/v1 endpoint with Version(1, deprecateAt: 2) then release groups v2.0 and newer will not have any /user/delete endpoints listed.

And the release will look like this:

release group v2.0

/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.

Versioning Options

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

  • 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 it's 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.

Ad Hoc Grouping Of Endpoints

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 (Experimental)

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