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

ClientModel: Add samples for System.ClientModel library #42369

Merged
merged 44 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
2737eab
Samples - WIP
annelo-msft Mar 2, 2024
e1f7a82
README updates
annelo-msft Mar 4, 2024
6bae518
Merge remote-tracking branch 'upstream/main' into clientmodel-samples
annelo-msft Mar 4, 2024
e43c767
Configuration samples
annelo-msft Mar 4, 2024
e0fee6f
updates before generating snippets
annelo-msft Mar 4, 2024
9dbf76b
update snippets
annelo-msft Mar 4, 2024
433c3fc
readme updates
annelo-msft Mar 4, 2024
a7934ed
intermediate backup
annelo-msft Mar 4, 2024
f7adcbf
updates
annelo-msft Mar 4, 2024
2d4ec41
fix
annelo-msft Mar 4, 2024
b9e406f
updates
annelo-msft Mar 4, 2024
8408cf3
nit
annelo-msft Mar 4, 2024
b316e9c
nit
annelo-msft Mar 4, 2024
f0c2877
fix links
annelo-msft Mar 5, 2024
456c6b5
updates from PR feedback
annelo-msft Mar 5, 2024
c0db649
revert engsys file
annelo-msft Mar 5, 2024
75d0161
update product
annelo-msft Mar 5, 2024
f6c136c
Merge remote-tracking branch 'upstream/main' into clientmodel-samples
annelo-msft Mar 5, 2024
262e923
add sample client implementation
annelo-msft Mar 5, 2024
552b485
add input model to sample client method
annelo-msft Mar 5, 2024
d83555e
change API key in samples
annelo-msft Mar 5, 2024
6d99f19
add inline comments to sample client and change defaults on HttpClien…
annelo-msft Mar 6, 2024
ee42d04
update impressions link
annelo-msft Mar 6, 2024
e8b6e09
restructure to address PR feedback
annelo-msft Mar 6, 2024
81eae19
nits
annelo-msft Mar 6, 2024
095324c
nits
annelo-msft Mar 6, 2024
47a47ca
nits
annelo-msft Mar 6, 2024
4ef9646
Merge remote-tracking branch 'upstream/main' into clientmodel-samples
annelo-msft Mar 6, 2024
91d3223
small updates from PR feedback
annelo-msft Mar 6, 2024
863cec6
add comment
annelo-msft Mar 7, 2024
7a02ba2
rework convenience methods section in README
annelo-msft Mar 7, 2024
c9fc7d0
more updates; add dotnet-api slug
annelo-msft Mar 7, 2024
f459730
Add sample showing response classifier
annelo-msft Mar 8, 2024
3c75fa9
updates:
annelo-msft Mar 8, 2024
8040105
reference error response configuration sample from README
annelo-msft Mar 8, 2024
24cc903
Merge remote-tracking branch 'upstream/main' into clientmodel-samples
annelo-msft Mar 8, 2024
a49b27b
update samples README
annelo-msft Mar 8, 2024
5f00c79
update md files
annelo-msft Mar 8, 2024
49896a6
show creation of BinaryContent from model in RequestOptions sample
annelo-msft Mar 8, 2024
81e2e20
add examples of different way to create BinaryContent
annelo-msft Mar 8, 2024
3aed272
show protocol method implementation and message.Apply(options)
annelo-msft Mar 8, 2024
d7a36c7
updates
annelo-msft Mar 8, 2024
6c56e7c
nits
annelo-msft Mar 8, 2024
2719c61
nits
annelo-msft Mar 8, 2024
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
243 changes: 209 additions & 34 deletions sdk/core/System.ClientModel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@

## Getting started

Typically, you will not need to install `System.ClientModel`.
it will be installed for you when you install one of the client libraries using it.
Typically, you will not need to install `System.ClientModel`. It will be installed for you when you install a client library that uses it.

### Install the package

Expand All @@ -23,69 +22,245 @@ dotnet add package System.ClientModel

None needed for `System.ClientModel`.

### Authenticate the client
## Key concepts

The `System.ClientModel` package provides a `KeyCredential` type for authentication.
`System.ClientModel` contains two categories of types: those used to author service clients and those used by the end-users of service clients. Types provided for the convenience of client end-users are in the `System.ClientModel` namespace. Types used by client authors and in lower-level service client APIs are in the `System.ClientModel.Primitives` namespace.

## Key concepts
The main concepts in `System.ClientModel` include:

The main shared concepts of `System.ClientModel` include:
- Client pipeline to send and receive HTTP messages (`ClientPipeline`).
- Interfaces to read and write input and output models in client convenience APIs (`IPersistableModel<T>` and `IJsonModel<T>`).
- Options to configure service clients (`ClientPipelineOptions`).
- Results to enable access to service response and HTTP response details (`ClientResult<T>`, `ClientResult`).
- Exceptions that result from failed requests (`ClientResultException`).
- Options to customize HTTP requests (`RequestOptions`).
- Reading and writing models in different formats (`ModelReaderWriter`).

- Configuring service clients (`ClientPipelineOptions`).
- Accessing HTTP response details (`ClientResult`, `ClientResult<T>`).
- Exceptions for reporting errors from service requests in a consistent fashion (`ClientResultException`).
- Customizing requests (`RequestOptions`).
- Providing APIs to read and write models in different formats (`ModelReaderWriter`).
Below, you will find sections explaining these shared concepts in more detail.

## Examples
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved

### Send a message using the MessagePipeline
### Send a message using the ClientPipeline

`System.ClientModel`-based clients, or **service clients**, use the `ClientPipeline` type to send and receive HTTP messages. The following sample shows a minimal example of what a service client implementation might look like.

```C# Snippet:ReadmeSampleClient
public class SampleClient
{
private readonly Uri _endpoint;
private readonly ApiKeyCredential _credential;
private readonly ClientPipeline _pipeline;

public SampleClient(Uri endpoint, ApiKeyCredential credential, SampleClientOptions? options = default)
{
options ??= new SampleClientOptions();

_endpoint = endpoint;
_credential = credential;

ApiKeyAuthenticationPolicy authenticationPolicy = ApiKeyAuthenticationPolicy.CreateBearerAuthorizationPolicy(credential);
_pipeline = ClientPipeline.Create(options,
perCallPolicies: ReadOnlySpan<PipelinePolicy>.Empty,
perTryPolicies: new PipelinePolicy[] { authenticationPolicy },
beforeTransportPolicies: ReadOnlySpan<PipelinePolicy>.Empty);
}

public ClientResult<SampleResource> UpdateResource(SampleResource resource)
{
PipelineMessage message = _pipeline.CreateMessage();
message.ResponseClassifier = PipelineMessageClassifier.Create(stackalloc ushort[] { 200 });

PipelineRequest request = message.Request;
request.Method = "PATCH";
request.Uri = new Uri($"https://www.example.com/update?id={resource.Id}");
request.Headers.Add("Accept", "application/json");
request.Content = BinaryContent.Create(resource);

_pipeline.Send(message);

PipelineResponse response = message.Response!;

if (response.IsError)
{
throw new ClientResultException(response);
}

SampleResource updated = ModelReaderWriter.Read<SampleResource>(response.Content)!;
return ClientResult.FromValue(updated, response);
}
}
```

### Reading and writing model content to HTTP messages

Service clients provide model types representing service resources as input parameters and return types from many service methods. Client authors can implement the `IPersistableModel<T>` and `IJsonModel<T>` in model type implementations to enable service clients to write input model content into request message bodies and read response body content to create instances of output models, as shown in the example client's service method above. The following sample shows a minimal example of what a persistable model implementation might look like.

```C# Snippet:ReadmeSampleModel
public class SampleResource : IJsonModel<SampleResource>
{
public SampleResource(string id)
{
Id = id;
}

public string Id { get; init; }

SampleResource IJsonModel<SampleResource>.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options)
=> FromJson(reader);

SampleResource IPersistableModel<SampleResource>.Create(BinaryData data, ModelReaderWriterOptions options)
=> FromJson(new Utf8JsonReader(data));

string IPersistableModel<SampleResource>.GetFormatFromOptions(ModelReaderWriterOptions options)
=> options.Format;

void IJsonModel<SampleResource>.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options)
=> ToJson(writer);

BinaryData IPersistableModel<SampleResource>.Write(ModelReaderWriterOptions options)
=> ModelReaderWriter.Write(this, options);

private void ToJson(Utf8JsonWriter writer)
{
writer.WriteStartObject();
writer.WritePropertyName("id");
writer.WriteStringValue(Id);
writer.WriteEndObject();
}

private static SampleResource FromJson(Utf8JsonReader reader)
{
reader.Read(); // start object
reader.Read(); // property name
reader.Read(); // id value

return new SampleResource(reader.GetString()!);
}
}
```

### Configuring service clients

Service clients provide a constructor that takes a service endpoint and a credential used to authenticate with the service. They also provide a constructor overload that takes an endpoint, a credential, and an instance of `ClientPipelineOptions`.
Passing `ClientPipelineOptions` when a client is created will configure the pipeline that the client uses to send and receive HTTP requests and responses. Client pipeline options can be used to override default values such as the network timeout used to send or retry a request.

```C# Snippet:ClientModelConfigurationReadme
MapsClientOptions options = new()
{
NetworkTimeout = TimeSpan.FromSeconds(120),
};

string? key = Environment.GetEnvironmentVariable("MAPS_API_KEY");
ApiKeyCredential credential = new(key!);
MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential, options);
```

For more information on client configuration, see [Client configuration samples](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/Configuration.md)

### Accessing HTTP response details

Service clients have methods that are used to call cloud services to invoke service operations. These methods on a client are called **service methods**. Service clients expose two types of service methods: _convenience methods_ and _protocol methods_.

**Convenience methods** provide a convenient way to invoke a service operation. They are methods that take a strongly-typed model as input and return a `ClientResult<T>` that holds a strongly-typed representation of the service response. Details from the HTTP response may also be obtained from the return value.

**Protocol method** are low-level methods that take parameters that correspond to the service HTTP API and return a `ClientResult` holding only the raw HTTP response details. These methods also take an optional `RequestOptions` parameter that allows the client pipeline and the request to be configured for the duration of the call.

The following sample illustrates how to call a convenience method and access both the strongly-typed output model and the details of the HTTP response.
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved

```C# Snippet:ClientResultTReadme
// Create a client
string? key = Environment.GetEnvironmentVariable("MAPS_API_KEY");
ApiKeyCredential credential = new(key!);
MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential);

A very basic client implementation might use the following approach:
// Call a service method, which returns ClientResult<T>
IPAddress ipAddress = IPAddress.Parse("2001:4898:80e8:b::189");
ClientResult<IPAddressCountryPair> result = await client.GetCountryCodeAsync(ipAddress);

```csharp
ApiKeyCredential credential = new ApiKeyCredential(key);
ApiKeyAuthenticationPolicy authenticationPolicy = ApiKeyAuthenticationPolicy.CreateBearerAuthorizationPolicy(credential);
ClientPipeline pipeline = ClientPipeline.Create(pipelineOptions, authenticationPolicy);
// ClientResult<T> has two members:
//
// (1) A Value property to access the strongly-typed output
IPAddressCountryPair value = result.Value;
Console.WriteLine($"Country is {value.CountryRegion.IsoCode}.");

PipelineMessage message = pipeline.CreateMessage();
message.Apply(requestOptions);
message.MessageClassifier = PipelineMessageClassifier.Create(stackalloc ushort[] { 200 });
// (2) A GetRawResponse method for accessing the details of the HTTP response
PipelineResponse response = result.GetRawResponse();

PipelineRequest request = message.Request;
request.Method = "GET";
request.Uri = new Uri("https://www.example.com/");
request.Headers.Add("Accept", "application/json");
Console.WriteLine($"Response status code: '{response.Status}'.");
Console.WriteLine("Response headers:");
foreach (KeyValuePair<string, string> header in response.Headers)
{
Console.WriteLine($"Name: '{header.Key}', Value: '{header.Value}'.");
}
```

For more information on client service methods, see [Client service method samples](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md).

### Handling exceptions that result from failed requests

When a service call fails, service clients throw a `ClientResultException`. The exception exposes the HTTP status code and the details of the service response if available.
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved

pipeline.Send(message);
Console.WriteLine(message.Response.Status);
```C# Snippet:ClientResultExceptionReadme
try
{
IPAddress ipAddress = IPAddress.Parse("2001:4898:80e8:b::189");
ClientResult<IPAddressCountryPair> result = await client.GetCountryCodeAsync(ipAddress);
}
// Handle exception with status code 404
catch (ClientResultException e) when (e.Status == 404)
{
// Handle not found error
Console.Error.WriteLine($"Error: Response failed with status code: '{e.Status}'");
}
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
```

### Customizing HTTP requests

Service clients expose low-level _protocol methods_ that allow callers to customize the details of HTTP requests by passing an optional `RequestOptions` parameter. `RequestOptions` can be used to modify various aspects of the request sent by the service method, such as adding a request header, or adding a policy to the client pipeline that can modify the request directly before sending it to the service. `RequestOptions` also enables passing a `CancellationToken` to the method.

annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
```C# Snippet:RequestOptionsReadme
// Create RequestOptions instance
RequestOptions options = new();

// Set CancellationToken
options.CancellationToken = cancellationToken;

// Add a header to the request
options.AddHeader("CustomHeader", "CustomHeaderValue");

// Call protocol method to pass RequestOptions
ClientResult output = await client.GetCountryCodeAsync(ipAddress.ToString(), options);
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
```

For more information on customizing requests, see [Protocol method samples](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#protocol-methods)

### Read and write persistable models

As a library author you can implement `IPersistableModel<T>` or `IJsonModel<T>` which will give library users the ability to read and write your models.
Client library authors can implement the `IPersistableModel<T>` or `IJsonModel<T>` interfaces on strongly-typed model implementations. If they do, end-users of service clients can then read and write those models in cases where they need to persist them to a backing store.

Example writing an instance of a model.
The example below shows how to write a persistable model to `BinaryData`.

```C# Snippet:Readme_Write_Simple
InputModel model = new InputModel();
BinaryData data = ModelReaderWriter.Write(model);
```

Example reading a model from json
The example below shows how to read JSON to create a strongly-typed model instance.

```C# Snippet:Readme_Read_Simple
string json = @"{
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
""x"": 1,
""y"": 2,
""z"": 3
}";
""x"": 1,
""y"": 2,
""z"": 3
}";
OutputModel? model = ModelReaderWriter.Read<OutputModel>(BinaryData.FromString(json));
```

## Troubleshooting

You can troubleshoot `System.ClientModel`-based clients by inspecting the result of any `ClientResultException` thrown from a pipeline's `Send` method.
You can troubleshoot service clients by inspecting the result of any `ClientResultException` thrown from a client's service method.

For more information on client service method errors, see [Handling exceptions that result from failed requests](#handling-exceptions-that-result-from-failed-requests).

## Next steps

annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
89 changes: 89 additions & 0 deletions sdk/core/System.ClientModel/samples/Configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# System.ClientModel-based client configuration samples

## Configuring retries

To modify the retry policy, create a new instance of `ClientRetryPolicy` and set it on the `ClientPipelineOptions` passed to the client constructor.

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.
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved

```C# Snippet:ConfigurationCustomizeRetries
MapsClientOptions options = new()
{
RetryPolicy = new ClientRetryPolicy(maxRetries: 5),
};

string? key = Environment.GetEnvironmentVariable("MAPS_API_KEY");
ApiKeyCredential credential = new(key!);
MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential, options);
```

## Add a custom policy to the pipeline

Azure SDKs provides a way to add policies to the pipeline at three positions, `PerCall`, `PerTry`, and `BeforeTransport`.

- `PerCall` policies run once per request

```C# Snippet:ConfigurationAddPerCallPolicy
MapsClientOptions options = new();
options.AddPolicy(new StopwatchPolicy(), PipelinePosition.PerCall);
```

- `PerTry` policies run each time a request is tried

```C# Snippet:ConfigurationAddPerTryPolicy
options.AddPolicy(new StopwatchPolicy(), PipelinePosition.PerTry);
```

- `BeforeTransport` policies run after all other policies in the pipeline and before the request is sent by the transport.

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.
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved

```C# Snippet:ConfigurationAddBeforeTransportPolicy
options.AddPolicy(new StopwatchPolicy(), PipelinePosition.BeforeTransport);
```

## Implement a custom policy
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved

To implement a policy 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.

```C# Snippet:ConfigurationCustomPolicy
public class StopwatchPolicy : PipelinePolicy
{
public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex)
{
Stopwatch stopwatch = new();
stopwatch.Start();

await ProcessNextAsync(message, pipeline, currentIndex);

stopwatch.Stop();

Console.WriteLine($"Request to {message.Request.Uri} took {stopwatch.Elapsed}");
}

public override void Process(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex)
{
Stopwatch stopwatch = new();
stopwatch.Start();

ProcessNext(message, pipeline, currentIndex);

stopwatch.Stop();

Console.WriteLine($"Request to {message.Request.Uri} took {stopwatch.Elapsed}");
}
}
```

## Provide a custom HttpClient instance

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.

```C# Snippet:ConfigurationCustomHttpClient
using HttpClient httpClient = new();
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved

MapsClientOptions options = new()
{
Transport = new HttpClientPipelineTransport(httpClient)
};
```
14 changes: 14 additions & 0 deletions sdk/core/System.ClientModel/samples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
page_type: sample
languages:
- csharp
products:
- dotnet-core
name: System.ClientModel samples for .NET
description: Samples for the System.ClientModel library
---

# System.ClientModel Samples

- [Client Configuration](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/Configuration.md)
- [Service Methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md)
Loading
Loading