Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for Kiota #2509

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Uno.Extensions-packageonly.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"src\\Uno.Extensions.Core\\Uno.Extensions.Core.csproj",
"src\\Uno.Extensions.Hosting.UI\\Uno.Extensions.Hosting.WinUI.csproj",
"src\\Uno.Extensions.Hosting\\Uno.Extensions.Hosting.csproj",
"src\\Uno.Extensions.Http.Kiota\\Uno.Extensions.Http.Kiota.csproj",
"src\\Uno.Extensions.Http.Refit\\Uno.Extensions.Http.Refit.csproj",
"src\\Uno.Extensions.Http.UI\\Uno.Extensions.Http.WinUI.csproj",
"src\\Uno.Extensions.Http\\Uno.Extensions.Http.csproj",
Expand Down
19 changes: 19 additions & 0 deletions Uno.Extensions.sln
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Uno.Extensions.RuntimeTests
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Uno.Extensions.RuntimeTests.Core", "src\Uno.Extensions.RuntimeTests\Uno.Extensions.RuntimeTests.Core\Uno.Extensions.RuntimeTests.Core.csproj", "{869C9E5B-0F85-4316-BC4B-CB6CBFCC02A3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Uno.Extensions.Http.Kiota", "src\Uno.Extensions.Http.Kiota\Uno.Extensions.Http.Kiota.csproj", "{C9827C80-312B-4E81-B539-2D305D893A6C}"
Kunal22shah marked this conversation as resolved.
Show resolved Hide resolved
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -733,6 +735,22 @@ Global
{869C9E5B-0F85-4316-BC4B-CB6CBFCC02A3}.Release|x64.Build.0 = Release|Any CPU
{869C9E5B-0F85-4316-BC4B-CB6CBFCC02A3}.Release|x86.ActiveCfg = Release|Any CPU
{869C9E5B-0F85-4316-BC4B-CB6CBFCC02A3}.Release|x86.Build.0 = Release|Any CPU
{C9827C80-312B-4E81-B539-2D305D893A6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C9827C80-312B-4E81-B539-2D305D893A6C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C9827C80-312B-4E81-B539-2D305D893A6C}.Debug|arm64.ActiveCfg = Debug|Any CPU
{C9827C80-312B-4E81-B539-2D305D893A6C}.Debug|arm64.Build.0 = Debug|Any CPU
{C9827C80-312B-4E81-B539-2D305D893A6C}.Debug|x64.ActiveCfg = Debug|Any CPU
{C9827C80-312B-4E81-B539-2D305D893A6C}.Debug|x64.Build.0 = Debug|Any CPU
{C9827C80-312B-4E81-B539-2D305D893A6C}.Debug|x86.ActiveCfg = Debug|Any CPU
{C9827C80-312B-4E81-B539-2D305D893A6C}.Debug|x86.Build.0 = Debug|Any CPU
{C9827C80-312B-4E81-B539-2D305D893A6C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C9827C80-312B-4E81-B539-2D305D893A6C}.Release|Any CPU.Build.0 = Release|Any CPU
{C9827C80-312B-4E81-B539-2D305D893A6C}.Release|arm64.ActiveCfg = Release|Any CPU
{C9827C80-312B-4E81-B539-2D305D893A6C}.Release|arm64.Build.0 = Release|Any CPU
{C9827C80-312B-4E81-B539-2D305D893A6C}.Release|x64.ActiveCfg = Release|Any CPU
{C9827C80-312B-4E81-B539-2D305D893A6C}.Release|x64.Build.0 = Release|Any CPU
{C9827C80-312B-4E81-B539-2D305D893A6C}.Release|x86.ActiveCfg = Release|Any CPU
{C9827C80-312B-4E81-B539-2D305D893A6C}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -779,6 +797,7 @@ Global
{B72698C5-6706-4275-8A2B-A1D39FE9B13E} = {2197ADCE-59C4-465A-B380-0B06BF68BBBC}
{A42362AF-8A61-4BBA-AA8A-E43323D5A063} = {FB399485-A0B1-4416-A494-E19AC7F5A665}
{869C9E5B-0F85-4316-BC4B-CB6CBFCC02A3} = {FB399485-A0B1-4416-A494-E19AC7F5A665}
{C9827C80-312B-4E81-B539-2D305D893A6C} = {45179294-70DC-47E8-AD22-1296F897B594}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6E7B035D-9A64-4D95-89AA-9D4653F17C42}
Expand Down
114 changes: 114 additions & 0 deletions doc/Learn/Http/HowTo-Kiota.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
---
uid: Uno.Extensions.Http.HowToKiota
---
# How-To: Quickly create and register a Kiota Client for an API

When working with APIs in your application, having a strongly-typed client can simplify communication and reduce boilerplate code. **Kiota** is a tool that generates strongly-typed API clients from Swagger/OpenAPI definitions. With Uno.Extensions, you can easily register and use Kiota clients in your Uno Platform app without additional setup.

## Step-by-Step Guide

> [!IMPORTANT]
> This guide assumes you used the template wizard or `dotnet new unoapp` to create your solution. If not, it is recommended that you follow the [**Creating an application with Uno.Extensions** documentation](xref:Uno.Extensions.HowToGettingStarted) to create an application from the template.

### 1. Installation

* Add `Http` to the `<UnoFeatures>` property in the Class Library (`.csproj`) file:

```diff
<UnoFeatures>
Material;
Extensions;
+ Http;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious about this. I see there is no separate UnoFeature for Refit, it just comes as part of the Http feature. I know @francoistanguay mentioned we may replace Refit with Kiota rather than having both. We could instead split this as HttpRefit and HttpKiota feature?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Along with that, @jeromelaban we would need to make some changes to the uno.sdk to include the Uno.Extensions.Kiota packages as part of the Http UnoFeature in order for this to work as is, correct?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It depends. If this package is transitively included, there's no need. If it's optional, then yes, we'll need to.

Toolkit;
MVUX;
</UnoFeatures>
```

### 2. Enable Http in Host Builder

* Add the UseHttp method to the `IHostBuilder`:

```csharp
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
var appBuilder = this.CreateBuilder(args)
.Configure(hostBuilder =>
{
hostBuilder.UseHttp();
});
...
}
```

### 3. Generate the Kiota Client

* Install the Kiota tool:

```xml
dotnet tool install --global Microsoft.OpenApi.Kiota
```

* Generate the Client using the OpenAPI specification URL:

```xml
kiota generate -l CSharp -c MyApiClient -n MyApp.Client -d PATH_TO_YOUR_API_SPEC.json -o ./Client
```

This will create a client named `MyApiClient` in the Client folder.

### 4. Register the Kiota Client

* Register the generated client in the IHostBuilder using `AddKiotaClient` from `Uno.Extensions`:

```csharp

protected override void OnLaunched(LaunchActivatedEventArgs args)
{
var appBuilder = this.CreateBuilder(args)
.Configure(hostBuilder =>
{
hostBuilder.UseHttp((context, services) =>
services.AddKiotaClient<MyApiClient>(
context,
options: new EndpointOptions { Url = "https://localhost:5002" }
)
);
});
}
```

### 5. Use the Kiota Client in Your Code

* Inject the ChefsApiClient into your view model or service and make API requests:

```csharp
public class MyViewModel
{
private readonly MyApiClient _apiClient;

public MyViewModel(MyApiClient apiClient)
{
_apiClient = apiClient;
}

public async Task GetAll()
{
var something = await _apiClient.Api.GetAsync();
Console.WriteLine($"Retrieved {something?.Count}.");
}
}

```

## Important Considerations

* With `Uno.Extensions.Authentication`, the HttpClient automatically includes the **Authorization** header. You don't need to manually handle token injection. The middleware ensures the access token is included in each request.

* Ensure your server is running and the swagger.json file is accessible at the specified URL when generating the Kiota client.

## See also

* [Overview: What is Kiota?](https://learn.microsoft.com/en-us/openapi/kiota/)
* [Overview: HTTP](xref:Uno.Extensions.Http.Overview)
* [How-To: Consume a web API with HttpClient](xref:Uno.Development.ConsumeWebApi)
* [How-To: Register an Endpoint for HTTP Requests](xref:Uno.Extensions.Http.HowToHttp)
2 changes: 2 additions & 0 deletions doc/Learn/Http/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
href: xref:Uno.Extensions.Http.HowToRefit
- name: "How-To: Configure with endpoint options"
href: xref:Uno.Extensions.Http.HowToEndpointOptions
- name: "How-To: Use create Kiota Client"
href: Uno.Extensions.Http.HowToKiota
7 changes: 7 additions & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
<PackageVersion Include="DotNet.ReproducibleBuilds" Version="1.1.1" />
<PackageVersion Include="FluentAssertions" Version="6.7.0" />
<PackageVersion Include="IdentityModel.OidcClient" Version="5.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Kiota.Abstractions" Version="1.11.0" />
<PackageVersion Include="Microsoft.Kiota.Http.HttpClientLibrary" Version="1.11.0" />
<PackageVersion Include="Microsoft.Kiota.Serialization.Form" Version="1.11.0" />
<PackageVersion Include="Microsoft.Kiota.Serialization.Json" Version="1.11.0" />
<PackageVersion Include="Microsoft.Kiota.Serialization.Multipart" Version="1.11.0" />
<PackageVersion Include="Microsoft.Kiota.Serialization.Text" Version="1.11.0" />
<PackageVersion Include="MSTest.TestAdapter" Version="2.2.9" />
<PackageVersion Include="MSTest.TestFramework" Version="2.2.9" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
Expand Down
127 changes: 127 additions & 0 deletions src/Uno.Extensions.Http.Kiota/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Kiota.Abstractions;
using Microsoft.Kiota.Abstractions.Authentication;
using Microsoft.Kiota.Http.HttpClientLibrary;

namespace Uno.Extensions.Http.Kiota;

/// <summary>
/// Provides extension methods for registering Kiota clients within the <see cref="IServiceCollection"/>.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Registers a Kiota client with the specified <paramref name="name"/> and endpoint options.
/// </summary>
/// <typeparam name="TClient">The Kiota client type to register.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> to register the client with.</param>
/// <param name="context">The <see cref="HostBuilderContext"/> providing the hosting context.</param>
/// <param name="options">[Optional] The endpoint options for the client (loaded from appsettings if not specified).</param>
/// <param name="name">[Optional] The name for locating endpoint information in appsettings.</param>
/// <param name="configure">[Optional] A callback for configuring the endpoint.</param>
/// <returns>The updated <see cref="IServiceCollection"/> with the registered Kiota client.</returns>
public static IServiceCollection AddKiotaClient<TClient>(
Kunal22shah marked this conversation as resolved.
Show resolved Hide resolved
this IServiceCollection services,
HostBuilderContext context,
EndpointOptions? options = null,
string? name = null,
Func<IHttpClientBuilder, EndpointOptions?, IHttpClientBuilder>? configure = null
)
where TClient : class =>
services.AddKiotaClientWithEndpoint<TClient, EndpointOptions>(context, options, name, configure);

/// <summary>
/// Registers a Kiota client with the specified <paramref name="name"/> and supports additional endpoint options.
/// </summary>
/// <typeparam name="TClient">The Kiota client type to register.</typeparam>
/// <typeparam name="TEndpoint">The type of endpoint to register.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> to register the client with.</param>
/// <param name="context">The <see cref="HostBuilderContext"/> providing the hosting context.</param>
/// <param name="options">[Optional] The endpoint options for the client (loaded from appsettings if not specified).</param>
/// <param name="name">[Optional] The name for locating endpoint information in appsettings.</param>
/// <param name="configure">[Optional] A callback for configuring the endpoint.</param>
/// <returns>The updated <see cref="IServiceCollection"/> with the registered Kiota client.</returns>
public static IServiceCollection AddKiotaClientWithEndpoint<TClient, TEndpoint>(
this IServiceCollection services,
HostBuilderContext context,
TEndpoint? options = null,
string? name = null,
Func<IHttpClientBuilder, TEndpoint?, IHttpClientBuilder>? configure = null
)
where TClient : class
where TEndpoint : EndpointOptions, new()
{
services.AddKiotaHandlers();

return services.AddClientWithEndpoint<TClient, TEndpoint>(
context,
options,
name: name ?? typeof(TClient).FullName ?? "DefaultClient",
httpClientFactory: (s, c) => s.AddHttpClient<TClient>(name ?? typeof(TClient).FullName ?? "DefaultClient")
.AttachKiotaHandlers()
.ConfigureHttpClient(client =>
{
if (options?.Url != null)
{
client.BaseAddress = new Uri(options.Url);
}
}),
configure: configure
)
.AddSingleton<IRequestAdapter, HttpClientRequestAdapter>(sp =>
{
var httpClient = sp.GetRequiredService<HttpClient>();
var authProvider = new AnonymousAuthenticationProvider();

var parseNodeFactory = new Microsoft.Kiota.Serialization.Json.JsonParseNodeFactory();
var serializationWriterFactory = new Microsoft.Kiota.Serialization.Json.JsonSerializationWriterFactory();

var requestAdapter = new HttpClientRequestAdapter(authProvider, parseNodeFactory, serializationWriterFactory, httpClient);

if (options?.Url != null)
{
requestAdapter.BaseUrl = options.Url;
}

return requestAdapter;
})
.AddSingleton<TClient>(sp =>
{
var requestAdapter = sp.GetRequiredService<IRequestAdapter>();
return (TClient)Activator.CreateInstance(typeof(TClient), requestAdapter)!;
});
}
/// <summary>
/// Dynamically adds Kiota handlers to the service collection.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to register the handlers with.</param>
/// <returns>The updated <see cref="IServiceCollection"/> with the registered Kiota handlers.</returns>
private static IServiceCollection AddKiotaHandlers(this IServiceCollection services)
{
var kiotaHandlers = KiotaClientFactory.GetDefaultHandlerTypes();
foreach (var handler in kiotaHandlers)
{
services.AddTransient(handler);
}

return services;
}

/// <summary>
/// Attaches Kiota handlers to the <see cref="IHttpClientBuilder"/>.
/// </summary>
/// <param name="builder">The <see cref="IHttpClientBuilder"/> to attach the handlers to.</param>
/// <returns>The updated <see cref="IHttpClientBuilder"/> with the attached Kiota handlers.</returns>
private static IHttpClientBuilder AttachKiotaHandlers(this IHttpClientBuilder builder)
{
var kiotaHandlers = KiotaClientFactory.GetDefaultHandlerTypes();
foreach (var handler in kiotaHandlers)
{
builder.AddHttpMessageHandler((sp) => (DelegatingHandler)sp.GetRequiredService(handler));
}

return builder;
}

}
28 changes: 28 additions & 0 deletions src/Uno.Extensions.Http.Kiota/Uno.Extensions.Http.Kiota.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="Microsoft.Kiota.Abstractions" />
<PackageReference Include="Microsoft.Kiota.Http.HttpClientLibrary" />
<PackageReference Include="Microsoft.Kiota.Serialization.Form" />
<PackageReference Include="Microsoft.Kiota.Serialization.Json" />
<PackageReference Include="Microsoft.Kiota.Serialization.Multipart" />
<PackageReference Include="Microsoft.Kiota.Serialization.Text" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Uno.Extensions.Authentication\Uno.Extensions.Authentication.csproj" />
<ProjectReference Include="..\Uno.Extensions.Configuration\Uno.Extensions.Configuration.csproj" />
<ProjectReference Include="..\Uno.Extensions.Http\Uno.Extensions.Http.csproj" />
<ProjectReference Include="..\Uno.Extensions.Serialization\Uno.Extensions.Serialization.csproj" />
</ItemGroup>

</Project>
1 change: 1 addition & 0 deletions testing/TestHarness/TestHarness.Core/TestSections.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public enum TestSections
Localization,
Http_Endpoints,
Http_Refit,
Http_Kiota,
Toolkit_ThemeService,
Validation
}
24 changes: 24 additions & 0 deletions testing/TestHarness/TestHarness.UITest/Ext/Kiota/Given_Kiota.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace TestHarness.UITest;

public class Given_Kiota : NavigationTestBase
{
[Test]
public async Task When_KiotaClient_Registered()
{
InitTestSection(TestSections.Http_Kiota);

App.WaitThenTap("ShowAppButton");

App.WaitElement("KiotaHomeNavigationBar");

var initializationStatus = App.GetText("InitializationStatusTextBlock");
initializationStatus.Should().Contain("Kiota Client initialized successfully.");

App.WaitThenTap("FetchPostsButton");

await Task.Delay(1000);

var fetchResult = App.GetText("FetchPostsResultTextBlock");
fetchResult.Should().Contain("Retrieved").And.Contain("posts");
}
}
2 changes: 2 additions & 0 deletions testing/TestHarness/TestHarness.sln
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestHarness", "TestHarness\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Uno.Extensions.Reactive.Tests", "..\..\src\Uno.Extensions.Reactive.Tests\Uno.Extensions.Reactive.Tests.csproj", "{A39E9AEC-4F8B-4B6F-A4DD-16201F427948}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Uno.Extensions.Http.Kiota", "..\..\src\Uno.Extensions.Http.Kiota\Uno.Extensions.Http.Kiota.csproj", "{49716A71-0D2A-4EB2-83C0-3A7CDD9549B5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Uno.Extensions.Reactive.Testing", "..\..\src\Uno.Extensions.Reactive.Testing\Uno.Extensions.Reactive.Testing.csproj", "{1A90701A-FE5D-478A-9D5E-E264D5D0287F}"
EndProject
Global
Expand Down
Loading
Loading