Skip to content

Commit 1d8a46c

Browse files
authored
ClientModel: Add samples for System.ClientModel library (#42369)
* Samples - WIP * README updates * Configuration samples * updates before generating snippets * update snippets * readme updates * intermediate backup * updates * fix * updates * nit * nit * fix links * updates from PR feedback * revert engsys file * update product * add sample client implementation * add input model to sample client method * change API key in samples * add inline comments to sample client and change defaults on HttpClient sample * update impressions link * restructure to address PR feedback * nits * nits * nits * small updates from PR feedback * add comment * rework convenience methods section in README * more updates; add dotnet-api slug * Add sample showing response classifier * updates: * reference error response configuration sample from README * update samples README * update md files * show creation of BinaryContent from model in RequestOptions sample * add examples of different way to create BinaryContent * show protocol method implementation and message.Apply(options) * updates * nits * nits
1 parent 0cd6095 commit 1d8a46c

15 files changed

+1951
-130
lines changed

sdk/core/System.ClientModel/README.md

Lines changed: 322 additions & 43 deletions
Large diffs are not rendered by default.
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
# System.ClientModel-based client implementation samples
2+
3+
## Introduction
4+
5+
`System.ClientModel`-based clients, or **service clients**, are built using types provided in the `System.ClientModel` library.
6+
7+
## Basic client implementation
8+
9+
The following sample shows a minimal example of what a service client implementation might look like.
10+
11+
```C# Snippet:ReadmeSampleClient
12+
public class SampleClient
13+
{
14+
private readonly Uri _endpoint;
15+
private readonly ApiKeyCredential _credential;
16+
private readonly ClientPipeline _pipeline;
17+
18+
// Constructor takes service endpoint, credential used to authenticate
19+
// with the service, and options for configuring the client pipeline.
20+
public SampleClient(Uri endpoint, ApiKeyCredential credential, SampleClientOptions? options = default)
21+
{
22+
// Default options are used if none are passed by the client's user.
23+
options ??= new SampleClientOptions();
24+
25+
_endpoint = endpoint;
26+
_credential = credential;
27+
28+
// Authentication policy instance is created from the user-provided
29+
// credential and service authentication scheme.
30+
ApiKeyAuthenticationPolicy authenticationPolicy = ApiKeyAuthenticationPolicy.CreateBearerAuthorizationPolicy(credential);
31+
32+
// Pipeline is created from user-provided options and policies
33+
// specific to the service client implementation.
34+
_pipeline = ClientPipeline.Create(options,
35+
perCallPolicies: ReadOnlySpan<PipelinePolicy>.Empty,
36+
perTryPolicies: new PipelinePolicy[] { authenticationPolicy },
37+
beforeTransportPolicies: ReadOnlySpan<PipelinePolicy>.Empty);
38+
}
39+
40+
// Service method takes an input model representing a service resource
41+
// and returns `ClientResult<T>` holding an output model representing
42+
// the value returned in the service response.
43+
public ClientResult<SampleResource> UpdateResource(SampleResource resource)
44+
{
45+
// Create a message that can be sent via the client pipeline.
46+
using PipelineMessage message = _pipeline.CreateMessage();
47+
48+
// Modify the request as needed to invoke the service operation.
49+
PipelineRequest request = message.Request;
50+
request.Method = "PATCH";
51+
request.Uri = new Uri($"https://www.example.com/update?id={resource.Id}");
52+
request.Headers.Add("Accept", "application/json");
53+
54+
// Add request body content that will be written using methods
55+
// defined by the model's implementation of the IJsonModel<T> interface.
56+
request.Content = BinaryContent.Create(resource);
57+
58+
// Send the message.
59+
_pipeline.Send(message);
60+
61+
// Obtain the response from the message Response property.
62+
// The PipelineTransport ensures that the Response value is set
63+
// so that every policy in the pipeline can access the property.
64+
PipelineResponse response = message.Response!;
65+
66+
// If the response is considered an error response, throw an
67+
// exception that exposes the response details.
68+
if (response.IsError)
69+
{
70+
throw new ClientResultException(response);
71+
}
72+
73+
// Read the content from the response body and create an instance of
74+
// a model from it, to include in the type returned by this method.
75+
SampleResource updated = ModelReaderWriter.Read<SampleResource>(response.Content)!;
76+
77+
// Return a ClientResult<T> holding the model instance and the HTTP
78+
// response details.
79+
return ClientResult.FromValue(updated, response);
80+
}
81+
}
82+
```
83+
84+
### Reading and writing model content to HTTP messages
85+
86+
Service clients provide **model types** representing service resources as input parameters and return values from service clients' [convenience methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#convenience-methods). Client authors can implement the `IPersistableModel<T>` and `IJsonModel<T>` interfaces their in model implementations to make it easy for clients to write input model content to request message bodies, and to read response content and create instances of output models from it. An example of how clients' service methods might use such models is shown in [Basic client implementation](#basic-client-implementation). The following sample shows a minimal example of what a persistable model implementation might look like.
87+
88+
```C# Snippet:ReadmeSampleModel
89+
public class SampleResource : IJsonModel<SampleResource>
90+
{
91+
public SampleResource(string id)
92+
{
93+
Id = id;
94+
}
95+
96+
public string Id { get; init; }
97+
98+
SampleResource IJsonModel<SampleResource>.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options)
99+
=> FromJson(reader);
100+
101+
SampleResource IPersistableModel<SampleResource>.Create(BinaryData data, ModelReaderWriterOptions options)
102+
=> FromJson(new Utf8JsonReader(data));
103+
104+
string IPersistableModel<SampleResource>.GetFormatFromOptions(ModelReaderWriterOptions options)
105+
=> options.Format;
106+
107+
void IJsonModel<SampleResource>.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options)
108+
=> ToJson(writer);
109+
110+
BinaryData IPersistableModel<SampleResource>.Write(ModelReaderWriterOptions options)
111+
=> ModelReaderWriter.Write(this, options);
112+
113+
// Write the model JSON that will populate the HTTP request content.
114+
private void ToJson(Utf8JsonWriter writer)
115+
{
116+
writer.WriteStartObject();
117+
writer.WritePropertyName("id");
118+
writer.WriteStringValue(Id);
119+
writer.WriteEndObject();
120+
}
121+
122+
// Read the JSON response content and create a model instance from it.
123+
private static SampleResource FromJson(Utf8JsonReader reader)
124+
{
125+
reader.Read(); // start object
126+
reader.Read(); // property name
127+
reader.Read(); // id value
128+
129+
return new SampleResource(reader.GetString()!);
130+
}
131+
}
132+
```
133+
134+
### Implementing protocol methods
135+
136+
The example shown in [Basic client implementation](#basic-client-implementation) illustrates what a service client [convenience method](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#convenience-methods) method implementation might look like. That is, the sample client defines a single service method that takes a model type parameter as input and returns a `ClientResult<T>` holding an output model type.
137+
138+
In contrast to convenience methods, client [protocol methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#protocol-methods) take `BinaryContent` as input and an optional `RequestOptions` parameter that holds user-provided options for configuring the request and the client pipeline for the duration of the service call. The following sample shows a minimal example of what a client that implements both convenience methods and protocol methods might look like.
139+
140+
#### Convenience method
141+
142+
The client's convenience method converts model types to request content and from response content, and calls through to the protocol method.
143+
144+
```C# Snippet:ClientImplementationConvenienceMethod
145+
// A convenience method takes a model type and returns a ClientResult<T>.
146+
public virtual async Task<ClientResult<CountryRegion>> AddCountryCodeAsync(CountryRegion country)
147+
{
148+
// Validate input parameters.
149+
if (country is null) throw new ArgumentNullException(nameof(country));
150+
151+
// Create the request body content to pass to the protocol method.
152+
// The content will be written using methods defined by the model's
153+
// implementation of the IJsonModel<T> interface.
154+
BinaryContent content = BinaryContent.Create(country);
155+
156+
// Call the protocol method.
157+
ClientResult result = await AddCountryCodeAsync(content).ConfigureAwait(false);
158+
159+
// Obtain the response from the ClientResult.
160+
PipelineResponse response = result.GetRawResponse();
161+
162+
// Create an instance of the model type representing the service response.
163+
CountryRegion value = ModelReaderWriter.Read<CountryRegion>(response.Content)!;
164+
165+
// Create the instance of ClientResult<T> to return from the convenience method.
166+
return ClientResult.FromValue(value, response);
167+
}
168+
```
169+
170+
#### Protocol method
171+
172+
The client's protocol method calls a helper method to create the message and request, passes the message to `ClientPipeline.Send`, and throws an exception if the response is an error response.
173+
174+
```C# Snippet:ClientImplementationProtocolMethod
175+
// Protocol method.
176+
public virtual async Task<ClientResult> AddCountryCodeAsync(BinaryContent country, RequestOptions? options = null)
177+
{
178+
// Validate input parameters.
179+
if (country is null) throw new ArgumentNullException(nameof(country));
180+
181+
// Use default RequestOptions if none were provided by the caller.
182+
options ??= new RequestOptions();
183+
184+
// Create a message that can be sent through the client pipeline.
185+
using PipelineMessage message = CreateAddCountryCodeRequest(country, options);
186+
187+
// Send the message.
188+
_pipeline.Send(message);
189+
190+
// Obtain the response from the message Response property.
191+
// The PipelineTransport ensures that the Response value is set
192+
// so that every policy in the pipeline can access the property.
193+
PipelineResponse response = message.Response!;
194+
195+
// If the response is considered an error response, throw an
196+
// exception that exposes the response details. The protocol method
197+
// caller can change the default exception behavior by setting error
198+
// options differently.
199+
if (response.IsError && options.ErrorOptions == ClientErrorBehaviors.Default)
200+
{
201+
// Use the CreateAsync factory method to create an exception instance
202+
// in an async context. In a sync method, the exception constructor can be used.
203+
throw await ClientResultException.CreateAsync(response).ConfigureAwait(false);
204+
}
205+
206+
// Return a ClientResult holding the HTTP response details.
207+
return ClientResult.FromResponse(response);
208+
}
209+
```
210+
211+
For more information on response classification, see [Configuring error response classification](#configuring-error-response-classification).
212+
213+
#### Request creation helper method
214+
215+
```C# Snippet:ClientImplementationRequestHelper
216+
private PipelineMessage CreateAddCountryCodeRequest(BinaryContent content, RequestOptions options)
217+
{
218+
// Create an instance of the message to send through the pipeline.
219+
PipelineMessage message = _pipeline.CreateMessage();
220+
221+
// Set a response classifier with known success codes for the operation.
222+
message.ResponseClassifier = PipelineMessageClassifier.Create(stackalloc ushort[] { 200 });
223+
224+
// Obtain the request to set its values directly.
225+
PipelineRequest request = message.Request;
226+
227+
// Set the request method.
228+
request.Method = "PATCH";
229+
230+
// Create the URI to set on the request.
231+
UriBuilder uriBuilder = new(_endpoint.ToString());
232+
233+
StringBuilder path = new();
234+
path.Append("countries");
235+
uriBuilder.Path += path.ToString();
236+
237+
StringBuilder query = new();
238+
query.Append("api-version=");
239+
query.Append(Uri.EscapeDataString(_apiVersion));
240+
uriBuilder.Query = query.ToString();
241+
242+
// Set the URI on the request.
243+
request.Uri = uriBuilder.Uri;
244+
245+
// Add headers to the request.
246+
request.Headers.Add("Accept", "application/json");
247+
248+
// Set the request content.
249+
request.Content = content;
250+
251+
// Apply the RequestOptions to the method. This sets properties on the
252+
// message that the client pipeline will use during processing,
253+
// including CancellationToken and any headers provided via calls to
254+
// AddHeader or SetHeader. It also stores policies that will be added
255+
// to the client pipeline before the first policy processes the message.
256+
message.Apply(options);
257+
258+
return message;
259+
}
260+
```
261+
262+
### Configuring error response classification
263+
264+
When a client sends a request to a service, the service may respond with a success response or an error response. The `PipelineTransport` used by the client's `ClientPipeline` sets the `IsError` property on the response to indicate to the client which category the response falls in. Service method implementations are expected to check the value of `response.IsError` and throw a `ClientResultException` when it is `true`, as shown in [Basic client implementation](#basic-client-implementation).
265+
266+
To classify the response, the transport uses the `PipelineMessageClassifier` value on the `PipelineMessage.ResponseClassifier` property. By default, the transport sets `IsError` to `true` for responses with a `4xx` or `5xx` HTTP status code. Clients can override the default behavior by setting the message classifier before the request is sent. Typically, a client creates a classifier that sets `response.IsError` to `false` for only response codes that are listed as success codes for the operation in the service's API definition. This type of status code-based classifier can be created using the `PipelineMessageClassifier.Create` factory method and passing the list of success status codes, as shown in the sample below.
267+
268+
```C# Snippet:ClientStatusCodeClassifier
269+
// Create a message that can be sent via the client pipeline.
270+
PipelineMessage message = _pipeline.CreateMessage();
271+
272+
// Set a classifier that will categorize only responses with status codes
273+
// indicating success for the service operation as non-error responses.
274+
message.ResponseClassifier = PipelineMessageClassifier.Create(stackalloc ushort[] { 200, 202 });
275+
```
276+
277+
Client authors can also customize classifier logic by creating a custom classifier type derived from `PipelineMessageClassifier`.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# System.ClientModel-based client configuration samples
2+
3+
## Configuring retries
4+
5+
To modify the retry policy, create a new instance of `ClientRetryPolicy` and set it on the `ClientPipelineOptions` passed to the client constructor.
6+
7+
By default, clients will retry a request three times using an exponential retry strategy with an initial delay of 0.8 seconds and a maximum delay of one minute.
8+
9+
```C# Snippet:ConfigurationCustomizeRetries
10+
MapsClientOptions options = new()
11+
{
12+
RetryPolicy = new ClientRetryPolicy(maxRetries: 5),
13+
};
14+
15+
string? key = Environment.GetEnvironmentVariable("MAPS_API_KEY");
16+
ApiKeyCredential credential = new(key!);
17+
MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential, options);
18+
```
19+
20+
## Implement a custom policy
21+
22+
To implement a custom policy that can be added to the client's pipeline, create a class that derives from `PipelinePolicy` and overide its `ProcessAsync` and `Process` methods. The request can be accessed via `message.Request`. The response is accessible via `message.Response`, but will have a value only after `ProcessNextAsync`/`ProcessNext` has been called.
23+
24+
```C# Snippet:ConfigurationCustomPolicy
25+
public class StopwatchPolicy : PipelinePolicy
26+
{
27+
public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex)
28+
{
29+
Stopwatch stopwatch = new();
30+
stopwatch.Start();
31+
32+
await ProcessNextAsync(message, pipeline, currentIndex);
33+
34+
stopwatch.Stop();
35+
36+
Console.WriteLine($"Request to {message.Request.Uri} took {stopwatch.Elapsed}");
37+
}
38+
39+
public override void Process(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex)
40+
{
41+
Stopwatch stopwatch = new();
42+
stopwatch.Start();
43+
44+
ProcessNext(message, pipeline, currentIndex);
45+
46+
stopwatch.Stop();
47+
48+
Console.WriteLine($"Request to {message.Request.Uri} took {stopwatch.Elapsed}");
49+
}
50+
}
51+
```
52+
53+
## Add a custom policy to the pipeline
54+
55+
Azure SDKs provides a way to add policies to the pipeline at three positions, `PerCall`, `PerTry`, and `BeforeTransport`.
56+
57+
- `PerCall` policies run once per request
58+
59+
```C# Snippet:ConfigurationAddPerCallPolicy
60+
MapsClientOptions options = new();
61+
options.AddPolicy(new StopwatchPolicy(), PipelinePosition.PerCall);
62+
```
63+
64+
- `PerTry` policies run each time a request is tried
65+
66+
```C# Snippet:ConfigurationAddPerTryPolicy
67+
options.AddPolicy(new StopwatchPolicy(), PipelinePosition.PerTry);
68+
```
69+
70+
- `BeforeTransport` policies run after all other policies in the pipeline and before the request is sent by the transport.
71+
72+
Adding policies at the `BeforeTransport` position should be done with care since changes made to the request by a before-transport policy will not be visible to any logging policies that come before it in the pipeline.
73+
74+
```C# Snippet:ConfigurationAddBeforeTransportPolicy
75+
options.AddPolicy(new StopwatchPolicy(), PipelinePosition.BeforeTransport);
76+
```
77+
78+
## Provide a custom HttpClient instance
79+
80+
In some cases, users may want to provide a custom instance of the `HttpClient` used by a client's transport to send and receive HTTP messages. To provide a custom `HttpClient`, create a new instance of `HttpClientPipelineTransport` and pass the custom `HttpClient` instance to its constructor.
81+
82+
```C# Snippet:ConfigurationCustomHttpClient
83+
using HttpClientHandler handler = new()
84+
{
85+
// Reduce the max connections per server, which defaults to 50.
86+
MaxConnectionsPerServer = 25,
87+
88+
// Preserve default System.ClientModel redirect behavior.
89+
AllowAutoRedirect = false,
90+
};
91+
92+
using HttpClient httpClient = new(handler);
93+
94+
MapsClientOptions options = new()
95+
{
96+
Transport = new HttpClientPipelineTransport(httpClient)
97+
};
98+
```

0 commit comments

Comments
 (0)