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.

13 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
        Karsten Schramm

        “This is correct. If you look under the /obj folder”

        Since when does one check in the contents of the obj folder into the source control?

        But without the code, the project is not complete and not reproducibly compileable at any given point in the future.

  • 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?

    • Avatar
      Robert Lindblom

      I have the same experience.
      When generating from a swagger.json file, a class “swaggerClient” is generated for each endpoint (“controller”), resulting in a number of duplicate classes.

  • Avatar
    Rengar Lee

    Hi, a link error, click on “App Building with Azure API Management, Power Apps, and Logic Apps” but jump over this page.

  • Avatar
    mu88

    Any ideas what is going wrong when encountering the following error on client code generation from an OpenAPI file?

    NuGet.PackageManagement.PackageReferenceRollbackException: Package restore failed. 
    • Avatar
      Kris Golko

      If you execute ‘Restore NuGet Packages’ you’ll see the error’s details. I had this error, because generated code wanted ‘Newtonsoft.Json v12.0.2’ while my project already referenced newer ‘Newtonsoft.Json v12.0.3’.

  • Avatar
    Christopher Edwards

    I thought I’d try this with an existing solution with three projects a Web API, a Blazor client and a shared class library with DTOs.

    The generated code of course contains duplicates of each of the shared DTOs (slaps forehead). I suppose this codegen tool is currently best used with a public API that you don’t own in which having the generated types might be super useful.

    Still it would be nice if it still worked in the scenario of a owned API with shared DTO library because calling the API endpoints with typed methods is still nicer than supplying URL segments to HttpClient. Maybe you could have a setting to add the namespace of the shared DTO library (if you have one) when adding the connected service so that the generated code can reuse your existing types rather than creating redundant ones.

  • Avatar
    A. Gaus

    I prefer the old way. I’m developing a client that connects to an API with a lot of endpoints and DTOs. The old way generated a folder with a file for every class, more convinient IMHO. The new way generates only a file with, in my case, 25.224 lines of code. It’s even hard to Visual Studio to work with such a big file!

    Can we get the old experience (or both and select the one to work with) or a least can we mantain the old version in projects currently developed with the old way?

    Thanks!

  • Avatar
    Karsten Schramm

    I don’t understand why Microsoft chose to remove the REST API client generator that was present in VS until 2019 16.8 and replaced it with the OpenAPI generator in 2019 16.9.

    First of all, it doesn’t even generate compileable code when having more than one controller, see bugreport

    Second of all, it leaves out all the interfaces and synchronous method calls that the old generator generated which were really helpful for mocking and unit testing.

    Also concepts like retry and error detection strategies were much easier to add.

    Now all this has to be done by hand and it’s hailed as “progress”.