Skip to main content

Justice League - Client Code Gen Demo

Setup

  1. In the solution root, Install the Kiota CLI

    dotnet tool install Microsoft.OpenApi.Kiota
  2. Create the directory structure

    mkdir JusticeLeague.Client
    cd JusticeLeague.Client
  3. Create a console app

    dotnet new console
  4. Add the console to our solution

    cd ..
    dotnet sln add JusticeLeague.Client
  5. Add the Kiota nuget packages

    cd JusticeLeague.Client

    dotnet add package Microsoft.Kiota.Bundle

Generate Client Code

  1. Generate the full client code using Kiota

    dotnet kiota generate -l CSharp -c HeroesClient -n JusticeLeague.Client --openapi https://localhost:7277/openapi/v1.json -o Client

    See how much code is generated

  2. Generate the client code using Kiota CLI

        dotnet kiota generate -l CSharp -c HeroesClient -n JusticeLeague.Client --openapi https://localhost:7277/openapi/v1.json -o Client --include-path /api/heroes --include-path /account/login --include-path /account/register --clean-output

    Now look at the code generated and see the size difference

  3. Add some .NET extension packages we'll use

    dotnet add package Microsoft.Extensions.Hosting
    dotnet add package Microsoft.Extensions.Configuration
    dotnet add package Microsoft.Extensions.Options.DataAnnotations
  4. Bootstrap the application in Program.cs

    var builder = Host.CreateApplicationBuilder();
    builder.Configuration.AddUserSecrets<Program>();
  5. Add ApiConfig

    public record ApiConfig
    {
    public const string SectionName = "Api";

    [Required]
    public required string Url { get; init; }

    [Required]
    public required string UserName { get; init; }

    [Required]
    public required string Password { get; init; }
    }

Authentication

We'll use this to store the configuration for our API

  1. Try to manually create and use the HeroClient. Show how many classes we need to instantiate in order to set this up.

  2. Create an enum for different client types

    public enum HeroClientTypes
    {
    Anonymous,
    Secure
    }
  3. Register an anonymous client (for before we are authenticated)

    builder.Services.AddKeyedSingleton<HeroesClient>(HeroClientTypes.Anonymous, (p, o) =>
    {
    var authProvider = new AnonymousAuthenticationProvider();
    var adapter = new HttpClientRequestAdapter(authProvider);
    return new HeroesClient(adapter);
    });
  4. Register a secure client (once we are authenticated)

    builder.Services.AddKeyedSingleton<HeroesClient>(HeroClientTypes.Secure, (sp, o) =>
    {
    var accessTokenProvider = sp.GetRequiredService<IAccessTokenProvider>();
    var authProvider = new BaseBearerTokenAuthenticationProvider(accessTokenProvider);
    var adapter = new HttpClientRequestAdapter(authProvider);
    return new HeroesClient(adapter);
    });
  5. Create an AccessTokenProvider - gets the token for every request.

    public class AccessTokenProvider(
    IOptions<ApiConfig> apiConfig,
    [FromKeyedServices(HeroClientTypes.Anonymous)]
    HeroesClient client)
    : IAccessTokenProvider
    {
    public async Task<string> GetAuthorizationTokenAsync(
    Uri requestUri,
    Dictionary<string, object>? options = default,
    CancellationToken cancellationToken = default)
    {
    var token = await client.Account.Login.PostAsync(
    new() { Email = apiConfig.Value.UserName, Password = apiConfig.Value.Password },
    cancellationToken: cancellationToken);
    return token?.AccessToken ?? string.Empty;
    }

    public AllowedHostsValidator AllowedHostsValidator { get; } = new();
    }

Note: The new(ish) KeyedServices attribute is used to resolve the HeroesClient of type Anonymous so we can call the login endpoint.

  1. Register our AccessTokenProvider

    builder.Services.AddSingleton<IAccessTokenProvider, AccessTokenProvider>();
  2. Bind our configuration

    builder.Services
    .AddOptionsWithValidateOnStart<ApiConfig>()
    .BindConfiguration(ApiConfig.SectionName)
    .ValidateDataAnnotations();

    The ValidateDataAnnotations will ensure that our configuration is valid before we start the application. If it is not valid, it will throw an exception. It validates the properties of ApiConfig that are decorated with data annotations, such as [Required].

  3. Build and start our app

    var app = builder.Build();

    app.Start();

    Console.ReadKey();
  4. Run and test our application

    💣 BOOM! ?? What's happened here?
    Our configuration validation had detected we forgot to add our configuration to our user secrets. Let's fix that.

  5. Add the following to user secrets

    {
    "Api": {
    "Url": "https://localhost:7277",
    "UserName": "gert@ssw.com",
    "Password": "Password1?"
    }
    }

    NOTE: The URL needs to be replaced with the URL of your API

  6. Run and test our application again. Ensure everything starts correctly

Calling the API

  1. Register a user

    var apiConfig = app.Services.GetRequiredService<IOptions<ApiConfig>>();
    var anonymousClient = app.Services.GetRequiredKeyedService<HeroesClient>(HeroClientTypes.Anonymous);
    await anonymousClient.Account.Register.PostAsync(new RegisterRequest
    {
    Email = apiConfig.Value.UserName,
    Password = apiConfig.Value.Password
    });
  2. Call our secure API

    var secureClient = app.Services.GetRequiredKeyedService<HeroesClient>(HeroClientTypes.Secure);
    var heroes = await secureClient.Api.Heroes.GetAsync();
    foreach (var hero in heroes)
    {
    Console.WriteLine($"Hero - {hero.HeroName} ({hero.CivilianName})");
    }

    Console.ReadKey();
  3. Start both applications at the same time and debug to ensure everything works

TODO

In the real world, users would enter details via a UI, OR if you did want to authenticate an app, you would use a Client Credentials flow (uses a secret instead of a username/password).

  • Finish the demo