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
builder.Services.AddApiVersioning(options =>
{
options.ReportApiVersions = true;
// in case of no explicit version
options.DefaultApiVersion = new ApiVersion(1, 0);
}
);
Controller configuration
[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
return CreatedAtAction(nameof(GetContactType), new
{
version = HttpContext.GetRequestedApiVersion().ToString(),
id = contactType.Id
}, contactType
);
Deprecated API versions
Add deprecated flag
[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.
Asp.Versioning.Mvc.ApiExplorer
Swashbuckle.AspNetCore
Swagger minimal config
builder.Services.AddControllersWithViews();
builder.Services.AddApiVersioning();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
app.UseSwagger();
app.UseSwaggerUI();
Swagger advanced config
Support multiple versions, add descriptions, etc
Swagger generation conf
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
var apiVersioningBuilder = builder.Services.AddApiVersioning(options =>
{
options.ReportApiVersions = true;
// in case of no explicit version
options.DefaultApiVersion = new ApiVersion(1, 0);
});
apiVersioningBuilder.AddApiExplorer(options =>
{
// add the versioned api explorer, which also adds IApiVersionDescriptionProvider service
// note: the specified format code will format the version as "'v'major[.minor][-status]"
options.GroupNameFormat = "'v'VVV";
// note: this option is only necessary when versioning by url segment. the SubstitutionFormat
// can also be used to control the format of the API version in route templates
options.SubstituteApiVersionInUrl = true;
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
builder.Services.AddSwaggerGen();
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
/// <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
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
it’s the same as:
<PropertyGroup>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\WebApp.xml</DocumentationFile>
</PropertyGroup>
To disable XML warnings for whole project, include
<NoWarn>$(NoWarn);1591</NoWarn>
To disable warnings per file, include in top of file
#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
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
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>()
}
});
External links
-
Read Microsoft REST guidelines
-
Read the WIKI for technical info
-
Review samples for inspiration
Full Swagger conf
using System.Reflection;
using Asp.Versioning.ApiExplorer;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace WebApp;
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
private readonly IApiVersionDescriptionProvider _descriptionProvider;
public ConfigureSwaggerOptions(IApiVersionDescriptionProvider descriptionProvider)
{
_descriptionProvider = descriptionProvider;
}
public void Configure(SwaggerGenOptions options)
{
foreach (var description in _descriptionProvider.ApiVersionDescriptions)
{
options.SwaggerDoc(
description.GroupName,
new OpenApiInfo()
{
Title = $"API {description.ApiVersion}",
Version = description.ApiVersion.ToString(),
// Description = , TermsOfService = , Contact = , License =
}
);
}
// use fqn for dto descriptoins
options.CustomSchemaIds(t => t.FullName);
// 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);
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
{
Description =
"foo bar",
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>()
}
});
}
}