Skip to content

14 - Public Api

Public REST API

Microsoft REST guidelines

  • https://github.com/Microsoft/api-guidelines
  • https://github.com/Microsoft/api-guidelines/blob/vNext/Guidelines.md

All APIs compliant with the Microsoft REST API Guidelines MUST support explicit versioning. 

It's critical that clients can count on services to be stable over time, and it's critical that services can add features and make changes.

Versioning

Embedded in the path of the request URL, at the end of the service root:

https://api.contoso.com/v1.0/products/users

As a query string parameter of the URL: 

https://api.contoso.com/products/users?api-version=1.0

Or some other way (header?)...

When to version?
Services MUST increment their version number in response to any breaking API change.

Breaking changes

Examples of breaking changes:

  • Removing or renaming APIs or API parameters
  • Changes in behavior for an existing API
  • Changes in Error Codes and Fault Contracts
  • Anything that would violate the Principle of Least Astonishment


Principle of Least Astonishment

The principle means that a component of a system should behave in a way that most users will expect it to behave; the behavior should not astonish or surprise users.

Versioning in ASP.NET

Versioning in ASP.NET Core MVC
https://github.com/dotnet/aspnet-api-versioning

Add via NuGet: Microsoft.AspNetCore.Mvc.Versioning

Configure Program.cs

1
2
3
4
5
6
7
builder.Services.AddApiVersioning(options =>
    {
        options.ReportApiVersions = true;
        // in case of no explicit version
        options.DefaultApiVersion = new ApiVersion(1, 0);
    }
);

Controller configuration

1
2
3
4
5
6
7
8
9
[ApiVersion( "1.0" )]
[ApiController]
[Route("[api/v{version:apiVersion}/controller]")]
public class PersonsController : ControllerBase {

    [HttpGet]
    public IActionResult Get() => Ok( new[] { new Person() } );

}

Update your CreatedAtAction (in post methods) returns to include version

1
2
3
4
5
6
return CreatedAtAction(nameof(GetContactType), new
    {
        version = HttpContext.GetRequestedApiVersion().ToString(),
        id = contactType.Id
    }, contactType
);

Deprecated API versions

Add deprecated flag

1
2
3
4
[ApiVersion("1.0", Deprecated = true)]
public class PersonsController : ControllerBase
{
    ...

Api documentation

Versioning is good but what about documenting your api (and versions)?

OpenAPI Specification

https://swagger.io/resources/open-api/

Add web based Api explorer and testing functionality to your API project.

  • Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
  • Swashbuckle.AspNetCore

Swagger minimal config

1
2
3
4
5
6
7
8
builder.Services.AddControllersWithViews();

builder.Services.AddApiVersioning();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

app.UseSwagger();
app.UseSwaggerUI();

DI

Swagger advanced config

Support multiple versions, add descriptions, etc

Swagger generation conf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
    private readonly IApiVersionDescriptionProvider _provider;

    public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) =>
        _provider = provider;

    public void Configure(SwaggerGenOptions options)
    {
        // add all possible api versions found
        foreach (var description in _provider.ApiVersionDescriptions)
        {
            options.SwaggerDoc(
                description.GroupName,
                new OpenApiInfo()
                {
                    Title = $"API {description.ApiVersion}",
                    Version = description.ApiVersion.ToString()
                    // Description, TermsOfServce, Contact, License, ...
                });
        }
    }
}

Use multiple versions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddApiVersioning(options =>
    {
        options.ReportApiVersions = true;
        // in case of no explicit version
        options.DefaultApiVersion = new ApiVersion(1, 0);
    }
);
builder.Services.AddVersionedApiExplorer( options => options.GroupNameFormat = "'v'VVV" );
builder.Services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
builder.Services.AddSwaggerGen();
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
app.UseSwagger();
app.UseSwaggerUI(options =>
{
    var provider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>(); 
    foreach ( var description in provider.ApiVersionDescriptions )
    {
        options.SwaggerEndpoint(
            $"/swagger/{description.GroupName}/swagger.json",
            description.GroupName.ToUpperInvariant() 
        );
    }
    // serve from root
    // options.RoutePrefix = string.Empty;
});

Decorate controllers

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/// <summary>       
/// Represents a RESTful people service.    
/// </summary>  
[ApiController] 
[ApiVersion( "1.0" )]   
[ApiVersion( "0.9", Deprecated = true )]    
[Route( "api/v{version:apiVersion}/[controller]" )] 
public class PeopleController : ControllerBase  
{   
    /// <summary>   
    /// Gets a single person.   
    /// </summary>  
    /// <param name="id">The requested person identifier.</param>   
    /// <returns>The requested person.</returns>    
    /// <response code="200">The person was successfully retrieved.</response>
    /// <response code="404">The person does not exist.</response>  
    [HttpGet( "{id:int}" )] 
    [Produces( "application/json" )]    
    [ProducesResponseType( typeof( Person ), 200 )] 
    [ProducesResponseType( 404 )]   
    public IActionResult Get( int id ) =>   
        Ok( new Person(){Id = id, FirstName = "John", LastName = "Doe"  }); 
}   

Document your code

Enable XML documentation creation in WebApp.csproj

1
2
3
<PropertyGroup>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

it’s the same as:

1
2
3
<PropertyGroup>
   <DocumentationFile>bin\$(Configuration)\$(TargetFramework)\WebApp.xml</DocumentationFile>
</PropertyGroup>

To disable XML warnings for whole project, include

1
<NoWarn>$(NoWarn);1591</NoWarn>

To disable warnings per file, include in top of file

1
#pragma warning disable 1591

https://docs.microsoft.com/en-us/dotnet/csharp/codedoc

Swagger and comments

Update Swagger conf to include XML comments and show full class/namespace on objects

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
    private readonly IApiVersionDescriptionProvider _provider;

    public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) =>
        _provider = provider;

    public void Configure(SwaggerGenOptions options)
    {

        ...

        // include xml comments (enable creation in csproj file)
        var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
        var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
        options.IncludeXmlComments(xmlPath);

        // use FullName for schemaId - avoids conflicts between classes using the same name (which are in different namespaces)
        options.CustomSchemaIds(i => i.FullName);
    }
}

Swagger and auth

Most of our endpoints need bearer auth.
Configuration to enable swagger authorization token support

DI

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
        options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
        {
            Description =
                "JWT Authorization header using the Bearer scheme.\r\n<br>" +
                "Enter 'Bearer'[space] and then your token in the text box below.\r\n<br>" +
                "Example: <b>Bearer eyJhbGciOiJIUzUxMiIsIn...</b>\r\n<br>" +
                "You will get the bearer from the <i>account/login</i> or <i>account/register</i> endpoint.",
            Name = "Authorization",
            In = ParameterLocation.Header,
            Type = SecuritySchemeType.ApiKey,
            Scheme = "Bearer"
        });

        options.AddSecurityRequirement(new OpenApiSecurityRequirement()
        {
            {
                new OpenApiSecurityScheme()
                {
                    Reference = new OpenApiReference()
                    {
                        Type = ReferenceType.SecurityScheme,
                        Id = "Bearer"
                    },
                    Scheme = "oauth2",
                    Name = "Bearer",
                    In = ParameterLocation.Header
                },
                new List<string>()
            }
        });
  • Read Microsoft REST guidelines

    • https://github.com/Microsoft/api-guidelines/blob/vNext/Guidelines.md
  • Read the WIKI for technical info

    • https://github.com/Microsoft/aspnet-api-versioning/wiki
  • Review samples for inspiration

    • https://github.com/Microsoft/aspnet-api-versioning/tree/master/samples/aspnetcore

Full Swagger conf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
using System.Reflection;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;

namespace WebApp;

public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
    private readonly IApiVersionDescriptionProvider _provider;

    public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) =>
        _provider = provider;

    public void Configure(SwaggerGenOptions options)
    {
        // add all possible api versions found
        foreach (var description in _provider.ApiVersionDescriptions)
        {
            options.SwaggerDoc(
                description.GroupName,
                new OpenApiInfo()
                {
                    Title = $"API {description.ApiVersion}",
                    Version = description.ApiVersion.ToString()
                });
        }

        // include xml comments (enable creation in csproj file)
        var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
        var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
        options.IncludeXmlComments(xmlPath);

        // use FullName for schemaId - avoids conflicts between classes using the same name (which are in different namespaces)
        options.CustomSchemaIds(i => i.FullName);

        options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
        {
            Description =
                "JWT Authorization header using the Bearer scheme.\r\n<br>" +
                "Enter 'Bearer'[space] and then your token in the text box below.\r\n<br>" +
                "Example: <b>Bearer eyJhbGciOiJIUzUxMiIsIn...</b>\r\n<br>" +
                "You will get the bearer from the <i>account/login</i> or <i>account/register</i> endpoint.",
            Name = "Authorization",
            In = ParameterLocation.Header,
            Type = SecuritySchemeType.ApiKey,
            Scheme = "Bearer"
        });

        options.AddSecurityRequirement(new OpenApiSecurityRequirement()
        {
            {
                new OpenApiSecurityScheme()
                {
                    Reference = new OpenApiReference()
                    {
                        Type = ReferenceType.SecurityScheme,
                        Id = "Bearer"
                    },
                    Scheme = "oauth2",
                    Name = "Bearer",
                    In = ParameterLocation.Header
                },
                new List<string>()
            }
        });
    }
}