Skip to content

Commit 7f68b16

Browse files
authored
Added part-3 online client tutorial (#312)
1 parent 383a660 commit 7f68b16

File tree

3 files changed

+185
-1
lines changed

3 files changed

+185
-1
lines changed

docs/tutorial/client/part-2.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ The `HttpClientFactory` class is provided inside the `CommunityToolkit.Datasync.
267267

268268
The main problem with authentication in datasync is the same as authentication in a Web API world. You have to get that going before you can configure the datasync library to use it. Once you have configured authentication to work, it's as simple as an additional single line in the client options. You can also use the same mechanism in your own HTTP clients. This makes building authenticated clients for other purposes (like calling a non-datasync web API) simple as well.
269269

270-
In the next tutorial, we'll take a final look at the online client by investigating the pipeline and how it can be used for API keys, logging, and more.
270+
In the [next tutorial](./part-3.md), we'll take a final look at the online client by investigating the pipeline and how it can be used for API keys, logging, and more.
271271

272272
<!-- Links -->
273273
[1]: https://github.com/CommunityToolkit/Datasync

docs/tutorial/client/part-3.md

+183
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# Managing the HTTP pipeline
2+
3+
The [Datasync Community Toolkit][1] is a set of open-source libraries for building client-server application where the application data is available offline. Unlike, for example, [Google Firebase][2] or [AWS AppSync][3] (which are two competitors in this space), the [Datasync Community Toolkit][1] allows you to connect to any database, use any authentication, and provides robust authorization rules. You can also run the service anywhere - on your local machine, in a container, or on any cloud provider. Each side of the application (client and server) is implemented using .NET - [ASP.NET Core Web APIs][4] on the server side, and any .NET client technology (including [Avalonia][5], [MAUI][6], [Uno Platform][7], [WinUI3][8], and [WPF][9]) on the client side.
4+
5+
In the [last tutorial](./part-2.md), I enhanced the [basic functionality](./part-1.md) by introducing authentication and authorization to both the service and client. This was done by adjusting the HTTP pipeline to introduce a delegating handler called the `GenericAuthenticationProvider`. When a HTTP request gets sent to the service, the request goes through a number of delegating handlers before being sent to the service. These delegating handlers can each adjust the request. Similarly, when the response comes back from the service, it goes through the delegating handlers in the opposite direction.
6+
7+
For example, let's consider the following configuration:
8+
9+
```csharp
10+
HttpClientOptions options = new()
11+
{
12+
Endpoint = new Uri("https://myserver/"),
13+
HttpPipeline = [
14+
new LoggingHandler(),
15+
new AuthenticationHandler()
16+
],
17+
Timeout = TimeSpan.FromSeconds(120),
18+
UserAgent = "Enterprise/Datasync-myserver-service"
19+
};
20+
DatasyncServiceClient<TodoItem> serviceClient = new(options);
21+
```
22+
23+
When you call `serviceClient.GetAsync("1234")`, the following will happen:
24+
25+
* The `serviceClient` will construct a `HttpRequestMessage`: GET /tables/todoitem/1234 and call the configured `HttpClient`.
26+
* The `HttpClient` will then pass the request message to the root delegating handler (in this case, the `LoggingHandler`).
27+
* Each delegating handler will pass the request message to the next delegating handler in the sequence.
28+
* The final delegating handler (in this case, the `AuthenticationHandler`) will pass the request to the `HttpClientHandler`
29+
* The `HttpClientHandler` will transmit the request to the service and await the response, encoding the response as a `HttpResponseMessage`.
30+
* As the response is returned, it is passed up the chain of delegating handlers - `AuthenticationHandler`, then `LoggingHandler`.
31+
* Finally, the root delegating handler passes the response message back to the `HttpClient`, which returns the message to the `serviceClient`.
32+
* The `serviceClient` then decodes the response and passes it back to your code.
33+
34+
The point is - the order of those delegating handlers matters. If, for example, you have the order as suggested above, the authentication header will not be logged because the logging handler won't see the authentication header in the request.
35+
36+
## The general form of a delegating handler
37+
38+
A delegating handler looks like this:
39+
40+
```csharp
41+
public class MyDelegatingHandler : DelegatingHandler
42+
{
43+
public MyDelegatingHandler() : base()
44+
{
45+
}
46+
47+
public MyDelegatingHandler(HttpMessageHandler inner) : base(inner)
48+
{
49+
}
50+
51+
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
52+
{
53+
// Adjust the request here
54+
55+
HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
56+
57+
// Adjust the response here
58+
59+
return response;
60+
}
61+
}
62+
```
63+
64+
Let's take a look at two very common delegating handlers, starting with the logging handler.
65+
66+
### The logging handler
67+
68+
The logging handler is something I build into just about every single client application during development. The purpose is to provide detailed HTTP level logging for every single request. You can find this delegating handler in most of the samples as well.
69+
70+
```csharp
71+
using System.Diagnostics;
72+
73+
public class LoggingHandler : DelegatingHandler
74+
{
75+
public LoggingHandler() : base() { }
76+
public LoggingHandler(HttpMessageHandler inner) : base(inner) { }
77+
78+
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
79+
{
80+
Debug.WriteLine($"[HTTP] >>> {request.Message} {request.RequestUri}");
81+
await WriteContentAsync(request.Content, cancellationToken);
82+
83+
HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
84+
85+
Debug.WriteLine($"[HTTP] <<< {response.StatusCode} {response.ReasonPhrase}");
86+
await WriteContentAsync(response.Content, cancellationToken)
87+
}
88+
89+
private static async Task WriteContentAsync(HttpContent? content, CancellationToken cancellationToken = default)
90+
{
91+
if (content is not null)
92+
{
93+
Debug.WriteLine(await content.ReadAsStringAsync(cancellationToken));
94+
}
95+
}
96+
}
97+
```
98+
99+
!!! warning "Do not use with HttpClient"
100+
While this code works for the datasync client, it likely does not work in all `HttpClient` cases. This is because the content property can be a read-once stream. In this case, your logging code would interfere with the application.
101+
102+
### Serilog logging
103+
104+
I've seen a lot of developers use [Serilog] for logging. It's a solid framework, so there is no surprise that it is so popular. Serilog has [a specific package][Serilog.HttpClient] for handling `HttpClient` logging. After you have set up Serilog (probably in your App.xaml.cs file), you can do the following:
105+
106+
```csharp
107+
using Serilog.HttpClient;
108+
109+
HttpClientOptions options = new()
110+
{
111+
Endpoint = new Uri("https://myserver/"),
112+
HttpPipeline = [
113+
new LoggingDelegatingHandler(new RequestLoggingOptions()),
114+
]
115+
};
116+
DatasyncServiceClient<TodoItem> serviceClient = new(options);
117+
```
118+
119+
For more information, consult the documentation for [Serilog] and [Serilog.HttpClient].
120+
121+
### Adding an API Key
122+
123+
Another common request is to handle API keys. Azure API Management, as an example, allows you to associate specific backend APIs with products (a collection of APIs) that are chosen with an API or subscription key. This is done by passing an `Ocp-Apim-Subscription-Key` HTTP header with the request. Here is the delegating handler:
124+
125+
```csharp
126+
public class AzureApiManagementSubscriptionHandler : DelegatingHandler
127+
{
128+
public AzureApiManagementSubscriptionHandler() : base() { }
129+
public AzureApiManagementSubscriptionHandler(HttpMessageHandler inner) : base(inner) { }
130+
131+
public string ApiKey { get; set; } = string.Empty;
132+
133+
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
134+
{
135+
if (!string.IsNullOrWhiteSpace(ApiKey))
136+
{
137+
request.Headers.Add("Ocp-Apim-Subscription-Key", ApiKey);
138+
}
139+
140+
return base.SendAsync(request, cancellationToken);
141+
}
142+
}
143+
```
144+
145+
!!! warning "Do not use API keys for authentication"
146+
API keys are easily retrieved from client applications and not suitable to authenticate a user or client application.
147+
148+
You can now use the following client options to route the request to the correct product within Azure API Management:
149+
150+
```csharp
151+
string apiKey = GetApiKeyFromConfiguration();
152+
HttpClientOptions options = new()
153+
{
154+
Endpoint = new Uri("https://myserver/"),
155+
HttpPipeline = [
156+
new LoggingHandler(),
157+
new AzureApiManagementSubscriptionKey(apiKey),
158+
new GenericAuthenticationProvider(GetAuthenticationTokenAsync)
159+
]
160+
};
161+
DatasyncServiceClient<TodoItem> serviceClient = new(options);
162+
```
163+
164+
Note that we log the request, then add the API key and Authorization headers. In this way, privileged information (such as the authorization token) is not logged.
165+
166+
## Wrapping up
167+
168+
Adding delegating handlers to your client HTTP pipeline allows you to integrate any functionality you want on a per-request basis. This includes any authentication scheme, API keys, and request/response logging.
169+
170+
In the next tutorial, we move onto offline operations.
171+
172+
<!-- Links -->
173+
[1]: https://github.com/CommunityToolkit/Datasync
174+
[2]: https://firebase.google.com/
175+
[3]: https://docs.aws.amazon.com/appsync/latest/devguide/what-is-appsync.html
176+
[4]: https://learn.microsoft.com/training/modules/build-web-api-aspnet-core/
177+
[5]: https://avaloniaui.net/
178+
[6]: https://dotnet.microsoft.com/apps/maui
179+
[7]: https://platform.uno/
180+
[8]: https://learn.microsoft.com/windows/apps/winui/winui3/
181+
[9]: https://wpf-tutorial.com/
182+
[Serilog]: https://serilog.net/
183+
[Serilog.HttpClient]: https://github.com/alirezavafi/serilog-httpclient

mkdocs.shared.yml

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ nav:
3939
- "Online access":
4040
- "The basics": tutorial/client/part-1.md
4141
- Authentication: tutorial/client/part-2.md
42+
- "The HTTP pipeline": tutorial/client/part-3.md
4243
- In depth:
4344
- Server:
4445
- The basics: in-depth/server/index.md

0 commit comments

Comments
 (0)