Justice League - Client Code Gen Demo
Setup
-
In the solution root, Install the Kiota CLI
dotnet tool install Microsoft.OpenApi.Kiota -
Create the directory structure
mkdir JusticeLeague.Client
cd JusticeLeague.Client -
Create a console app
dotnet new console -
Add the console to our solution
cd ..
dotnet sln add JusticeLeague.Client -
Add the Kiota nuget packages
cd JusticeLeague.Client
dotnet add package Microsoft.Kiota.Bundle
Generate Client Code
-
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 ClientSee how much code is generated
-
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-outputNow look at the code generated and see the size difference
-
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 -
Bootstrap the application in
Program.csvar builder = Host.CreateApplicationBuilder();
builder.Configuration.AddUserSecrets<Program>(); -
Add
ApiConfigpublic 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
-
Try to manually create and use the
HeroClient. Show how many classes we need to instantiate in order to set this up. -
Create an enum for different client types
public enum HeroClientTypes
{
Anonymous,
Secure
} -
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);
}); -
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);
}); -
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.
-
Register our
AccessTokenProviderbuilder.Services.AddSingleton<IAccessTokenProvider, AccessTokenProvider>(); -
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
ApiConfigthat are decorated with data annotations, such as[Required]. -
Build and start our app
var app = builder.Build();
app.Start();
Console.ReadKey(); -
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. -
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
-
Run and test our application again. Ensure everything starts correctly
Calling the API
-
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
}); -
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(); -
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