8

Creating Discoverable HTTP APIs with ASP.NET Core 5 Web API

 3 years ago
source link: https://devblogs.microsoft.com/aspnet/creating-discoverable-http-apis-with-asp-net-core-5-web-api/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Creating Discoverable HTTP APIs with ASP.NET Core 5 Web API

Brady Gaster

February 4th, 2021

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
  • Open-source HTTP API packages and Tools
  • 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.

Brady Gaster

Principal Program Manager, ASP.NET Core

Follow


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK