Generating HTTP API clients using Visual Studio Connected Services

Avatar

Jon

We’re continuing our series on building HTTP APIs with .NET 5. In the first post in this series we talked about building well-described APIs using OpenAPI, and then followed that up taking a deeper dive into some of the open-source tools and packages you can use to make your HTTP API development easier. In this post, the third in the series, we’ll talk about how Visual Studio Connected Services gives you a convenient way of generating .NET clients for your HTTP APIs so your .NET apps can use the APIs via the Internet. Let’s get right to it!

Visual Studio Connected Services

Building an HTTP API is only useful when the API can be called from apps or other APIs. Consuming an HTTP API isn’t complex, but it does require a good amount of boilerplate, and often redundant, code. When using .NET code to call to a back-end API, the steps are relatively predictable. Developers create instances of the HttpClient class to initiate HTTP calls to an endpoint. Serialization and deserialization code needs to be written to serialize the request and responses to and from JSON, XML, or some other form of content.

While this isn’t a complex set of code, it becomes redundant quickly. Visual Studio Connected Services makes use of NSwag for generating strongly-typed clients from OpenAPI specification documents, and gRPC clients or servers from proto files.

Visual Studio Connected Services

By right-clicking on any project, you can add a Connected Service. Connected Services can be a variety of things – they can range from full-blown Azure Services like Key Vault or Cosmos DB that you’ll need to use in your app. Connected Services also enables you to use OpenAPI and gRPC services written by yourself or other folks on your team. The first step in generating an OpenAPI client is to right-click your project in Visual Studio and selecting “Add Connected Service.”

Adding a Connected Service in Visual Studio

Visual Studio for Mac is also enabled with these features. You can right-click a project’s Connected Services node in Visual Studio for Mac and select the Open Service Gallery command to access the Connected Services gallery.

Launching the Connected Services Gallery in Visual Studio for Mac

Once you’re in the Connected Services experience within Visual Studio you’ll see that generating a client for either a RESTful API described with OpenAPI or a gRPC API described with proto is right up front.

Connected Services experience in Visual Studio

Connected Services experience in Visual Studio for Mac

When you click the Add button within the Visual Studio Connected Services panel, you’ll be prompted to provide either a file path or a URL to a live API.

Adding a Connected Service in Visual Studio

Adding a Connected Service in Visual Studio for Mac

Once the OpenAPI file or URL is loaded by Visual Studio, the .csproj file you’re generating the OpenAPI client code into will be wired up to generate the client code on build. So, once you rebuild your project, the client code will be added to your project. A .csproj file from the sample Visual Studio Solution that has been set up with an OpenAPI Connected Service is shown below.

<ItemGroup>
  <OpenApiReference Include="..\ContosoOnlineOrders.Api\bin\Debug\net5.0\ContosoOnlineOrders.Api.json" Namespace="ContosoOnlineOrders.ConsoleClient">
    <Link>OpenAPIs\ContosoOnlineOrders.Api.json</Link>
  </OpenApiReference>
</ItemGroup>

This client project will have the client code used by my app to access the API each time the project is built. When working locally, with a Web API project in the same solution as a client project that will use that API, it is handy to also set the Web API project to generate the OpenAPI specification document on build. This way, a solution build results in a new OpenAPI file being generated at build-time and the client getting generated at the same time.

<PropertyGroup>
  <TargetFramework>net5.0</TargetFramework>
  <OpenApiDocumentName>1.1</OpenApiDocumentName>
  <Configurations>Debug;Release</Configurations>
</PropertyGroup>

<Target Name="Generate OpenAPI Specification Document" AfterTargets="Build">
    <Exec Command="dotnet swagger tofile --serializeasv2 --output $(OutputPath)$(AssemblyName).json $(OutputPath)$(AssemblyName).dll $(OpenApiDocumentName)" ContinueOnError="true" />
</Target>

Well-described APIs generate better client code

As mentioned in the first post of this series, it is extremely important when building HTTP APIs using Web API to use the Name property with your HTTP verb attributes.

[HttpGet("/orders/{id}", Name = nameof(GetOrder))]
public async Task <ActionResult<Order>> GetOrder([FromRoute] Guid id)
{
    // ...
}

Since OpenAPI generation uses the value of the Name property to define the operationId attribute value for each of the API’s endpoints, and since most client SDK generators use that operationId attribute to name the generated client methods, the generated code quality will be impacted by poorly-designed or described APIs. If you experience issues generating clients using Connected Services, first check the API description for the presence of operationId values in the spec.

Calling the HTTP API using the generated client

Because of the magic of OpenAPI, we now have a strongly typed .NET client that understands the back-end HTTP API. That means we can now focus on our actual business logic, leaving the HTTP internals to the generated client.

static async Task Main(string[] args)
{
    using var httpClient = new HttpClient();

    var apiClient = new ContosoOnlineOrders_ApiClient(httpClient);

    // create a product
    await apiClient.CreateProductAsync("1.1", new CreateProductRequest
    {
        Id = 1000,
        InventoryCount = 0,
        Name = "Test Product"
    });

    // update a product's inventory
    await apiClient.UpdateProductInventoryAsync(1000, "1.1",
        new InventoryUpdateRequest
        {
            CountToAdd = 50,
            ProductId = 1000
        });

    // get all products
    await apiClient.GetProductsAsync("1.1");

    // get one product
    await apiClient.GetProductAsync(1000, "1.1");

    // create a new order
    Guid orderId = Guid.NewGuid();

    await apiClient.CreateOrderAsync("1.1", new Order
    {
        Id = orderId,
        Items = new CartItem[]
        {
            new CartItem { ProductId = 1000, Quantity = 10 }
        }
    });

    // get one order
    await apiClient.GetOrderAsync(orderId, "1.1");

    // get all orders
    await apiClient.GetOrdersAsync("1.1");

    // check an order's inventory
    await apiClient.CheckInventoryAsync(orderId, "1.1");

    // ship an order
    await apiClient.ShipOrderAsync(orderId, "1.1");
}

Using a generated API client is good for two reasons: 1. The first time through, we start with working HTTP client code quickly. 2. When the backend HTTP service is updated, we can refresh our client in the same Connected Services dialog. This will grab the updated OpenAPI definition and build an updated client, automatically for us.

So, while it’s taken a little time to set up both the server and client side code, the real benefit here is that we now have automated the workflow on both ends. Updates to the service are painless, and if something in the server API change breaks our client code, we have a pretty good chance in catching it in our strongly typed client code.

Code Generation Internals

We’ve worked to make this as easy as possible to set up and use within Visual Studio, so you don’t really need to understand how the code is being generated to use it. However, it’s good to know how the code generation process works in case you need to extend, automate, or troubleshoot it.

The Visual Studio Connected Services code generation tooling is built on top of the Microsoft.dotnet-openapi global tool. You can install it using the following command:

dotnet tool install -g Microsoft.dotnet-openapi

This tool includes three commands: – Add: Adds a new OpenAPI reference by file or URL – Remove: Removes an existing reference – Refresh: Updates the OpenAPI reference with the latest content from the download URL

As an example of the kind of under the hood extensibility options available, the Add command includes a --code-generator option, which supports both NSwagCSharp (the default) and NSwagTypeScript. As mentioned earlier, we’re using NSwag to actually generate the code, so when you build your project in Visual Studio, here’s what’s actually happening:

  1. Visual Studio invokes the dotnet-openapi global tool using the parameters you specified when adding the service reference
  2. dotnet-openapi uses NSwag to generate the client code using a partial class

It’s important to note that the generated client is in a partial class. If you need to extend your HTTP client, you should do it in another partial class file, so that your customizations aren’t discarded if you regenerate the client code.

Summary

So far in this series, we’ve seen how to write great HTTP applications with .NET. The first two posts showed you how to leverage .NET features and open source tools to build great HTTP services. In this post, you learned how to generate .NET code to consume HTTP services. In the final post in this series, you’ll see how you can use API Management to expose your .NET HTTP APIs to low-code solutions like Power Apps, Logic Apps, and Azure Functions.

6 comments

Leave a comment

  • Avatar
    Andriy Savin

    It’s great to finally have at least something for client generation!
    But I can’t not mention some concerns:
    1. Dependency on the third-party tool which might stop to be actively supported at some point (e.g. this seems to be the destiny of Newtonsoft.JSON). But that could be OK if not 2;
    2. Generated client code is not included in the project and in the source control. That means ability to compile some “old” project may break if, say, the tool doesn’t work in new environment. I know this fear may look exaggerated, but I saw such things happened.
    3. No authentication setup is generated. I don’t know if OpenAPI can provide enough information about authentication/authorization, but I think this part can be difficult to tackle for many developers.

    • Brady Gaster
      Brady GasterMicrosoft employee

      All very valid questions. The nature of this is that we do the best job we can, like other code generators, of creating boilerplate code, the mundane parts of it that we know 97% of any developer will need to do. We try to leave the remaining details to you, so you can do what you do within the generated code, but not TO the generated code [directly].

      1. Valid point – the project is open-source, we have a great relationship with the owners of the project, and we would never take a dependency on an open-source package without having an agreement from the author/team that our team can be informed and involved if any changes come. Your concern is valid. I would ask that you look at very-large open-source projects in the Apache foundation, numerous packages that comprise ubiquitous frameworks like Node.js and Java and Spring. Not to mention OpenAPI – it’s an open spec, with many contributors and companies providing input to it. Also – folks can’t very easily deprecate NuGet packages and old NuGet package versions; that’s definitely a convention the community has established. The code generators exist AS NuGet packages, so the chances that they vanish is negligible.

      2. This is correct. If you look under the /obj folder, you’ll find the actual code. As Jon pointed out in our ASP.NET Community Stand-up show on the topic, the classes that are generated are generated as partial classes. So you can extend them. As usual with generated code (there are MANY code generation frameworks and tools in the OpenAPI space), we encourage customers not edit the actual generated code. Just extend the generated code’s partial classes with your own augmentation.

      3. OpenAPI has full support for OAuth, so you can describe your security boundary in both OpenAPI and in Swashbuckle and NSwag generation middleware and services. The generated code can be wired up to all the auth libraries and services. The generated code uses HttpClient, the standard for .NET developers that we put out in the BCL, so anything HttpClient can do, the generated code can do.

      I hope that helps!

      • Avatar
        Andriy Savin

        Hi, Thanks for your reply!
        Regarding authentication: yes, I know I can wire up any authentication approach I want to the HttpClient. As well as I can do any HTTP request I want (but with the tools I don’t need to). It would be nice if client generator tools could generate authentication code in addition to requests to an API. E.g. it could use IdentityModel library for OpenidConnect or MSAL for AAD. As far as I understand, currently the tools don’t do this even though OpenAPI provides needed information, as you mentioned.

  • Avatar
    Alexey Gritsenko

    Hi, just updated studio and checked out this case. It works horribly, the constructed class contains a lot of duplicates and compiles with an error. I can send you by mail the address of our OpenAPI site, where you will find many problems in the generated code. Where can I report this?