This month, we’ll be focusing on building HTTP APIs with .NET 5. We’ll explore a myriad of different tools, technologies, and services that make your API development experience more delightful. Each week, we’ll release a new post on this blog that goes into a separate area of building HTTP APIs with .NET, focusing mostly on using ASP.NET Core 5 Web API and the OpenAPI Specification together to build, publish, consume, and re-use well-described HTTP APIs. Here’s a glance at the upcoming series on building HTTP APIs using .NET on the ASP.NET team blog:
- Creating Discoverable HTTP APIs with ASP.NET Core 5 Web API (this post)
- Open-source HTTP API packages and Tools
- Generating HTTP API clients using Visual Studio Connected Services
- App Building with Azure API Management, Power Apps, and Logic Apps
Whether APIs are your product, or they’re a facet of the topology you build by default in your daily consulting work or freelancing, the process of building, testing, and integrating APIs can appear daunting. The posts in this series will show you how .NET is a great choice, as it offers RESTful, serverless, and more modern gRPC and HTTP2/3 investments. By the end, you’ll know some new techniques and conventions, and have some sample code and open-source projects to follow.
Thinking design-first
If you’ve ever referenced a SOAP Web Service within Visual Studio using right-click Add Web Reference or, once WCF appeared, Add Service Reference, you know the joy brought to your development experience by the tools and their support for a standard description format – the Web Service Definition Language (WSDL).
The OpenAPI Specification has evolved as the leading industry convention for describing standard request/response HTTP APIs, and a myriad of tools, open-source packages, and frameworks have been built atop ASP.NET Web API to make the process of building HTTP APIs simple. Whilst OpenAPI isn’t as strict or verbose as WSDL, the relaxed nature of the language makes for a wide variety of misuses and missed opportunities.
The .NET community is still rich with an ecosystem of open-source packages and tools, some of which we use in the ASP.NET templates and Visual Studio tooling. In the ASP.NET Core 5 Web API space, there are a handful of packages developers can use to get going with an end-to-end development experience using OpenAPI specifications as a contract between the API and the clients:
- Swashbuckle.AspNetCore – The most popular OpenAPI-generating package for ASP.NET Core developers. Used not only by the Swagger Codegen project, but also by the ASP.NET Core 5 Web API templates (catch the HTTP APIs session from .NET Conf where we highlight these updates to the Web API template). Swashbuckle emits Swagger/OpenAPI 2.0, 3.0, and 3.0 YAML, and can output the Swagger UI test page to make testing and documenting your APIs easy.
- NSwag – NSwag is another fantastic choice for generating OpenAPI documents from ASP.NET Core 5 Web API, but the NSwag team has an entire toolchain complete with NSwagStudio.
- Microsoft.dotnet-openapi – .NET Global Tool that can generate C# client SDK code for an OpenAPI specification.
A well designed API is so much nicer to develop, maintain, and consume. By following a few simple conventions when building Web APIs that use these packages to describe your APIs, your APIs will be much more discoverable, integrate with other products and cloud services more easily, and in general, offer more usage scenarios.
The sample project
This post will use a sample Visual Studio Solution. In the solution you’ll find a simple Web API project for Contoso Online Orders. We’ll build more onto the API over time, but just to give you a glimpse at the API’s shape, take a look at the Swagger UI page from the Web API project’s Debug experience.
The solution comes with the API non-optimized for discoverability. Throughout the steps in this post, you’ll be shown how to use any of the Visual Studio family of IDEs to make changes to the projects so you’ll see a before-and-after experience of the API as it was at first, then improved over time by small incremental changes.
The changes we’ll make can be summarized as:
- Making the OpenAPI specification more concise
- Inform consumers or integrators of all the potential request and response shapes and statuses that could happen
- Ensure that OpenAPI code generators and OpenAPI-consuming services can ingest my OpenAPI code and thus, call to my API
With that, we’ll jump right in and see how some of the attributes built in to ASP.NET Core 5 Web API can make your APIs concise, right out of the box.
Produces / Consumes
The JSON code for the Web API project is, by default, rather verbose and, ironically, not very informative about what could happen when the API is called in various states. Take this orders
operation, shown here. The C# code in my Web API controller will always output objects serialized in JSON format, but the OpenAPI specification is advertising other content types, too.
{
"paths": {
"/orders": {
"get": {
"tags": [
"Admin"
],
"responses": {
"200": {
"description": "Success",
"content": {
"text/plain": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Order"
}
}
},
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Order"
}
}
},
"text/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Order"
}
}
}
}
}
}
}
}
}
}
ProducesAttribute is used to specify the content type of the output that will be sent by the API, and the corresponding ConsumesAttribute is used to specify the request content types the API expects to receive. As described in the docs on formatting Web API output, Produces
and Consumes
filters by either specifying the content-type you want to output:
[Produces("application/json")]
[Consumes("application/json")]
As shown in the sample project corresponding to this blog series, you can also use the MediaTypeName class to make it simpler to use well-known values for the media type. With the sample controllers, we want every request object and response object to be serialized as JSON. To facilitate this at the controller level, each controller is decorated with both the Produces
and Consumes
attributes. To each attribute is passed the well-known property MediaTypeNames.Application.Json
, thus specifying that the only content type our API should use for both directions is application/json
.
[Route("[controller]")]
[ApiController]
#if ProducesConsumes
[Produces(MediaTypeNames.Application.Json)]
[Consumes(MediaTypeNames.Application.Json)]
#endif
public class AdminController : ControllerBase
{
// controller code
}
Since the code in the sample project is built to check for certain symbles using compiler directives, you can easily tweak the build configuration to include the Defined Constant value of ProducesConsumes
to turn on the attributes in the Web API sample project code. In Visual Studio for Mac, constants can be added by double-clicking a .csproj
file in the Solution Explorer to open the project properties window.
Now, when the Web API project is re-built and run, the OpenAPI specification is considerably smaller, due in large part to the removal of the unnecessary request and response content nodes. The updated JSON, reflecting this change, now only shows the application/json
content type, thus making the API specification much more compact.
{
"paths": {
"/orders": {
"get": {
"tags": [
"Admin"
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Order"
}
}
}
}
}
}
}
}
}
}
HTTP Response Codes
Web API developers can make use of a variety of Action Result inheritors to send the appropriate HTTP status code to the calling client in addition to the object or operation implemented by the API. In the GetProduct
method, for example, the code tries to get a product by ID. If the product is found, it is returned along with an HTTP OK via the Ok
result. If the product isn’t found, the API returns an HTTP 404, with no payload.
[HttpGet("/products/{id}")]
public async Task<ActionResult<Product>> GetProduct(int id)
{
var product = StoreServices.GetProduct(id);
if(product == null)
{
return NotFound();
}
else
{
return Ok(product);
}
}
However, the OpenAPI JSON for this method only shows the 200 response code, the default behavior for an HTTP GET. OpenAPI supports the notion of communicating all potential response codes an API could return, so we’re missing out on an opportunity to inform potential consumers or code generators on “what could happen” when the API method is called.
"/products/{id}": {
"get": {
"tags": [
"Shop"
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Product"
}
}
}
}
}
}
}
Web API Conventions
Web API conventions, available in ASP.NET Core 2.2 and later,include a way to extract common API documentation and apply it to multiple actions, controllers, or all controllers within an assembly. Web API conventions are a substitute for decorating individual actions with [ProducesResponseType]. Since API Conventions are extensible, you could write your own to enforce more granular rules if needed. Common use cases of conventions would be to:
- Define the most common return types and status codes returned from a specific type of action.
- Identify actions that deviate from the defined standard.
The sample Web API project’s Program.cs
file is decorated with the API Convention attribute – this is an approach that will impact the output of every Web API controller in the assembly, but you can apply conventions more granularly if desired. See the documentation on Web API conventions for more details.
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
#if ApiConventions
[assembly: ApiConventionType(typeof(DefaultApiConventions))]
#endif
namespace ContosoOnlineOrders.Api
{
public class Program
{
For this second compiler change, here’s how to do the same thing in Visual Studio. You can also double-click any .csproj
file in the Solution Explorer and manually enter it (see below).
Once you’ve made the change, re-building and running the Web API project will result with the OpenAPI specification being equipped with response code details for each of the API operations.
"/products/{id}": {
"get": {
"tags": [
"Shop"
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"default": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Product"
}
}
}
}
}
}
}
Emit OpenAPIs operationId in Web API
In the OpenAPI specification, operationId
is defined as “an optional unique string used to identify an operation. If provided, these IDs must be unique among all operations described in your API.” The operationId
attribute is used essentially to provide a explicit string-based identifier for each operation in an API.
Whilst the operationId
attribute isn’t required according to the OpenAPI Specification, including it in your APIs offers significant improvements in the API Consumption experience – documentation, code-generation, and integration with a myriad of services.
Take a look at a condensed version of the OpenAPI specification. This snapshot summarizes the API to the various endpoints, verbs, and operations offered by the API.
{
"paths": {
"/orders": {
"get": {
"tags": [
"Admin"
]
},
"post": {
"tags": [
"Shop"
]
}
},
"/orders/{id}": {
"get": {
"tags": [
"Admin"
]
}
},
"/products/{id}/checkInventory": {
"put": {
"tags": [
"Admin"
]
}
},
"/products": {
"post": {
"tags": [
"Admin"
]
},
"get": {
"tags": [
"Shop"
]
}
}
}
}
Each of the Web API Action Methods in the sample project is decorated with two variations of the Attribute Route. The first, wrapped in the OperationId
compiler symbol, results in the name of the C# Action Method being automatically set as the operationId
value. The Name
property, when passed to the constructor of each HTTP verb filter, is used as the value of the operationId
attribute in the generated OpenAPI specification document. The second lacks the Name
property, which results in the operationId
attribute value being omitted.
#if OperationId
[HttpGet("/orders", Name = nameof(GetOrders))]
#else
[HttpGet("/orders")]
#endif
public async Task<ActionResult<IEnumerable<Order>>> GetOrders()
{
return Ok(StoreServices.GetOrders());
}
#if OperationId
[HttpPost("/orders", Name = nameof(CreateOrder))]
#else
[HttpPost("/orders")]
#endif
public async Task<ActionResult<Order>> CreateOrder(Order order)
{
try
{
StoreServices.CreateOrder(order);
return Created($"/orders/{order.Id}", order);
}
catch
{
return Conflict();
}
}
The 3rd and final compiler switch you’ll add to the sample project activates operationId
generation. If you’re using Visual Studio for Mac or Windows, use one of the techniques shown earlier to set it. Or, if you’re in Visual Studio Code or another text editor, just edit the .csproj
files in the solution (both of them) to include the operationId
value (and to expect it during consumption and code-generation).
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DefineConstants>TRACE;DEBUG;NET;NET5_0;NETCOREAPP;ProducesConsumes;ApiConventions;OperationId</DefineConstants>
</PropertyGroup>
Once the OperationId
symbol is set, the Action Methods on each of the Web API controllers emits operationId
attribute values. Considering the condensed version of the OpenAPI specification from earlier, here’s the new OpenAPI spec inclusive with the operationId
attributes.
{
"paths": {
"/orders": {
"get": {
"tags": [
"Admin"
],
"operationId": "GetOrders"
},
"post": {
"tags": [
"Shop"
],
"operationId": "CreateOrder"
}
},
"/orders/{id}": {
"get": {
"tags": [
"Admin"
],
"operationId": "GetOrder"
}
},
"/products/{id}/checkInventory": {
"put": {
"tags": [
"Admin"
],
"operationId": "CheckInventory"
}
},
"/products": {
"post": {
"tags": [
"Admin"
],
"operationId": "CreateProduct"
},
"get": {
"tags": [
"Shop"
],
"operationId": "GetProducts"
}
}
}
}
Benefits in the Generated SDK Code
Human readability isn’t the only benefit. Code generators like the Microsoft.dotnet-openapi tools, NSwag, and AutoRest operate more gracefully when OpenAPI specifications include the operationId
attribute.
Later in this blog series, we’ll take a look at how Visual Studio Connected Services makes use of the Microsoft.dotnet-openapi tools to streamline C# client SDK code generation with one click. For now, imagine inheriting code for an ordering-process test that looked like the code below.
// create a product
await apiClient.ProductsAllAsync(new CreateProductRequest
{
Id = 1000,
InventoryCount = 0,
Name = "Test Product"
});
// update a product's inventory
await apiClient.CheckInventory2Async(1,
new InventoryUpdateRequest
{
CountToAdd = 50,
ProductId = 1000
});
// get all products
await apiClient.ProductsAsync();
// get one product
await apiClient.Products2Async(1000);
// create a new order
Guid orderId = Guid.NewGuid();
await apiClient.OrdersAsync(new Order
{
Id = orderId,
Items = new CartItem[]
{
new CartItem { ProductId = 1000, Quantity = 10 }
}
});
// get one order
await apiClient.Orders2Async(orderId);
// get all orders
await apiClient.OrdersAllAsync();
// check an order's inventory
await apiClient.CheckInventoryAsync(orderId);
// ship an order
await apiClient.ShipAsync(orderId);
Without comments, most of the methods – especially OrdersAsync
and Orders2Async
– are discoverable only when looking at their arguments. This generated code suffers from poor readability or discoverability. This code was also generated from an OpenAPI spec lacking in operationId
values. So, the code generator had to make a host of assumptions based on the value of the tags
, the name of the operation, and so on.
But once the OpenAPI specification has been augmented with values for each of the operations’ operationId
attribute, the code generator has more information and can generate a more concise SDK that any developer can use and discover with little effort.
// create a product
await apiClient.CreateProductAsync(new CreateProductRequest
{
Id = 1000,
InventoryCount = 0,
Name = "Test Product"
});
// update a product's inventory
await apiClient.UpdateProductInventoryAsync(1,
new InventoryUpdateRequest
{
CountToAdd = 50,
ProductId = 1000
});
// get all products
await apiClient.GetProductsAsync();
// get one product
await apiClient.GetProductAsync(1000);
// create a new order
Guid orderId = Guid.NewGuid();
await apiClient.CreateOrderAsync(new Order
{
Id = orderId,
Items = new CartItem[]
{
new CartItem { ProductId = 1000, Quantity = 10 }
}
});
// get one order
await apiClient.GetOrderAsync(orderId);
// get all orders
await apiClient.GetOrdersAsync();
// check an order's inventory
await apiClient.CheckInventoryAsync(orderId);
// ship an order
await apiClient.ShipOrderAsync(orderId);
Summary
This first post, whilst somewhat theoretical and design-oriented, is very important to the subsequent phases of building and using HTTP APIs. With these simple steps taken early, your OpenAPI specification document will more thoroughly and concisely describe your API and make it simpler for consumers to use.
We look forward to exploring the world of HTTP APIs with .NET with this and other projects this month! Feel free to provide feedback on the article, the series, and as always, feel enabled to use our GitHub repositories to submit issues and ideas if you’re inspired to make the product better through feedback.
Thanks Brady for the look at the tooling. this is a more of a code first approach to an API dev where you start with the code first and use tools to generate the OpenAPi spec for you at build/runtime. I try to discourage this pattern in my teams and encourage a contract first API where they first write the OpenAPI spec first, and then use that to generate the server/client code from that.
One...
So I'm completely new to OpenAPI and swagger etc. so I though I'd give this a try, starting with getting the controllers decorated for Swashbuckle and then marvelling at the SwaggerUI with a view to eventually getting generated clients.
It took a bit of time to get SwaggerUI working. Swashbuckle could do with some guidance as to what has gone wrong! I excluded all my controllers from the project and started adding them back...
Hello Brady,
I am using swagger <code>
I am testing with fortellis CLI, From that, I am getting public API errors like I shared below, I am able to resolve major errors or warnings but I am not able to resolve
"operation objects should declare a property". and "operation objects should declare a property",
That is because in swagger generation response we do not have produces and consumes properties found. I used <code> it seems correct...
I’m unfamiliar with that CLI. It looks like every possible warning is being thrown during validation of the OpenAPI spec you’ve generated/authored. Does your API explicitly call out which content-type you expect?
Id also like to mention (shameless plug!) there is a
dotnet new
template out there called Boxed.AspNetCore which scaffolds out an API that uses almost all the approaches this article mentions. Plus tons of other features like versioning, logging, caching, compression, docker, kestrel security options and much more. https://github.com/Dotnet-Boxed/Templates/blob/main/Docs/API.mdOpenAPI is awesome and we use it for all our documentation but Swashbuckle is an outdated implementation based upon the original Swagger spec (and updated with some OpenAPI features) and is a complete nightmare to use. I personally would not recommend using it if there are other tools available.
I agree with the article's recommendation of using OpenAPI for documenting REST APIs. I just cannot possibly recommend Swashbuckle. I'm really surprised that MS is trying to...
I can only speak for NSwag (main maintainer of it). Here my answers:
It looks like we've had a varied experience with some of these, and it sounds like you've been building APIs with Web API for some time. I'd actually be interested in how we could make the experience better for you and other advanced customers, but for a lot of what you've said, we've found that new Web API customers and folks who are just getting started truly do benefit from some of the things it...
Yes it does appear that Swashbuckle has been extended to include some of the features in OpenAPI 3.x but it seems like a lot of these require you pull in yet more packages to do so.
I'm glad Swashbuckle is getting some attention as I believe OpenAPI is a great approach but it should be built into the API framework, it should be easily extensible, and it needs to be kept up to date. When we...
What do you mean by discoverable? As for me first thoughts when I read the post heading is that some kind of service discovery will be discribed.
That the client-side api that’s generated works well with intellisense and is thus discoverable that way.
Ideally the consumer – the client side developer – gets an API developed for them that means they have readable code, and can intuitively work out what to do due to well-named endpoints and typed parameters & return values
I thought the same with this article being on service discovery, not API client generation. That also seems to be the general consensus for web service discovery even for SOAP based services, like this and many other articles refer to, https://en.wikipedia.org/wiki/Web_Services_Discovery.
I appreciate that feedback from all of you - I debated that title a bunch, thinking "will folks think of service discovery?" I'd be open to changing the title (but not the slug) to something about "better descriptions" or "more descriptive specs," - none of that seemed to work as well.
Apologies for confusing the message with that title - hope the content made sense once that could be overlooked.
While we're at it - are there...
You can also use NSwag-based Visual Studio extension to generate client code.