From 2737eab0d30bc93b6309f26fef6e3de447106932 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Fri, 1 Mar 2024 17:17:53 -0800 Subject: [PATCH 01/40] Samples - WIP --- sdk/core/System.ClientModel/README.md | 60 +++--- .../samples/Configuration.md | 190 ++++++++++++++++++ .../samples/ConvenienceMethods.md | 100 +++++++++ .../System.ClientModel/samples/Pipeline.md | 95 +++++++++ .../samples/ProtocolMethods.md | 131 ++++++++++++ sdk/core/System.ClientModel/samples/README.md | 16 ++ .../tests/Samples/ConfigurationSamples.cs | 151 ++++++++++++++ ...rWriter.cs => ModelReaderWriterSamples.cs} | 0 .../tests/Samples/PipelineSamples.cs | 99 +++++++++ .../tests/Samples/ResponseSamples.cs | 157 +++++++++++++++ 10 files changed, 975 insertions(+), 24 deletions(-) create mode 100644 sdk/core/System.ClientModel/samples/Configuration.md create mode 100644 sdk/core/System.ClientModel/samples/ConvenienceMethods.md create mode 100644 sdk/core/System.ClientModel/samples/Pipeline.md create mode 100644 sdk/core/System.ClientModel/samples/ProtocolMethods.md create mode 100644 sdk/core/System.ClientModel/samples/README.md create mode 100644 sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs rename sdk/core/System.ClientModel/tests/Samples/{ReadmeModelReaderWriter.cs => ModelReaderWriterSamples.cs} (100%) create mode 100644 sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs create mode 100644 sdk/core/System.ClientModel/tests/Samples/ResponseSamples.cs diff --git a/sdk/core/System.ClientModel/README.md b/sdk/core/System.ClientModel/README.md index c3429ab13d84..11b8de25f8db 100644 --- a/sdk/core/System.ClientModel/README.md +++ b/sdk/core/System.ClientModel/README.md @@ -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 @@ -25,41 +24,54 @@ None needed for `System.ClientModel`. ### Authenticate the client -The `System.ClientModel` package provides a `KeyCredential` type for authentication. +The `System.ClientModel` package provides an `ApiKeyCredential` type for authentication. ## Key concepts -The main shared concepts of `System.ClientModel` include: +The main concepts used by types in `System.ClientModel` include: - Configuring service clients (`ClientPipelineOptions`). - Accessing HTTP response details (`ClientResult`, `ClientResult`). -- 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`). +- Handling exceptions that result from failed requests (`ClientResultException`). +- Customizing HTTP requests (`RequestOptions`). +- Reading and writing models in different formats (`ModelReaderWriter`). + +Below, you will find sections explaining these shared concepts in more detail. ## Examples -### Send a message using the MessagePipeline +### Configuring service clients + +`System.ClientModel`-based clients provide a constructor that takes a service endpoint and a credential used to authenticate with the service. They also provide an overload that takes an endpoint, a credential, and an instance of `ClientPipelineOptions` that can be used to configure the pipeline the client uses to send and receive HTTP requests and responses. -A very basic client implementation might use the following approach: +`ClientPipelineOptions` allows overriding default client values for things like the network timeout used when sending a request or the maximum number of retries to send when a request fails. -```csharp -ApiKeyCredential credential = new ApiKeyCredential(key); -ApiKeyAuthenticationPolicy authenticationPolicy = ApiKeyAuthenticationPolicy.CreateBearerAuthorizationPolicy(credential); -ClientPipeline pipeline = ClientPipeline.Create(pipelineOptions, authenticationPolicy); +```C# Snippet:ClientModelConfigurationReadme +``` -PipelineMessage message = pipeline.CreateMessage(); -message.Apply(requestOptions); -message.MessageClassifier = PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); +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) -PipelineRequest request = message.Request; -request.Method = "GET"; -request.Uri = new Uri("https://www.example.com/"); -request.Headers.Add("Accept", "application/json"); +### Accessing HTTP response details -pipeline.Send(message); -Console.WriteLine(message.Response.Status); -``` +_Service clients_ have methods that are used to call cloud services to invoke service operations. These methods on a client are called _service methods_. + +`System.ClientModel`-based clients expose two types of service methods: _convenience methods_ and _protocol methods_. + +**Convenience methods** are methods that take a strongly-typed model as input and return a `ClientResult` that holds a strongly-typed representation of the service response. Details from the HTTP response can be obtained from the return value. + +**Protocol method** are low-level methods that take parameters that correspond to the service HTTP API and return only the raw HTTP response details. These methods also take an optional `RequestOptions` value that allows the client pipeline and the request to be configured for the duration of the call. + +// TODO: move the below to the detailed sample file + +**Convenience methods** are service methods that take a strongly-typed model representing schematized data needed to communicate with the service as input, and return a strongly-typed model representing the payload from the service response as output. Having strongly-typed models that represent service concepts provides a layer of convenience over working with the raw payload format. This is because these models unify the client user experience when cloud services differ in payload formats. That is, a client-user can learn the patterns for strongly-typed models that `System.ClientModel`-based clients provide, and use them together without having to reason about whether a cloud service represents resources using, for example, JSON or XML formats. + +**Protocol methods** are service methods that provide very little convenience over the raw HTTP APIs a cloud service exposes. They represent request and response message bodies using types that are very thin layers over raw JSON/binary/other formats. Users of client protocol methods must reference a service's API documentation directly, rather than relying on the client to provide developer conveniences via strongly-typing service schemas. + + + +### Handling exceptions that result from failed requests + +### Customizing HTTP requests ### Read and write persistable models @@ -85,7 +97,7 @@ OutputModel? model = ModelReaderWriter.Read(BinaryData.FromString(j ## 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 `System.ClientModel`-based clients by inspecting the result of any `ClientResultException` thrown from a client's service method. ## Next steps diff --git a/sdk/core/System.ClientModel/samples/Configuration.md b/sdk/core/System.ClientModel/samples/Configuration.md new file mode 100644 index 000000000000..be0a6afa1c59 --- /dev/null +++ b/sdk/core/System.ClientModel/samples/Configuration.md @@ -0,0 +1,190 @@ +# System.ClientModel-based client configuration samples + +## Configuring retry options + +To modify the retry options, use the `Retry` property of the `ClientOptions` class. + +By default, clients are setup to retry 3 times using an exponential retry strategy with an initial delay of 0.8 sec, and a max delay of 1 minute. + +```C# Snippet:RetryOptions +SecretClientOptions options = new SecretClientOptions() +{ + Retry = + { + Delay = TimeSpan.FromSeconds(2), + MaxRetries = 10, + Mode = RetryMode.Fixed + } +}; +``` + +## Setting a custom retry policy + +Using `RetryOptions` to configure retry behavior is sufficient for the vast majority of scenarios. For more advanced scenarios, it's possible to use a custom retry policy by setting the `RetryPolicy` property of client options class. This can be accomplished by implementing a retry policy that derives from the `RetryPolicy` class, or by passing in a `DelayStrategy` into the existing `RetryPolicy` constructor. The `RetryPolicy` class contains hooks to determine if a request should be retried and how long to wait before retrying. + +In the following example, we implement a policy that will prevent retries from taking place if the overall processing time has exceeded a configured threshold. Notice that the policy takes in `RetryOptions` as one of the constructor parameters and passes it to the base constructor. By doing this, we are able to delegate to the base `RetryPolicy` as needed (either by explicitly invoking the base methods, or by not overriding methods that we do not need to customize) which will respect the `RetryOptions`. + +```C# Snippet:GlobalTimeoutRetryPolicy +internal class GlobalTimeoutRetryPolicy : RetryPolicy +{ + private readonly TimeSpan _timeout; + + public GlobalTimeoutRetryPolicy(int maxRetries, DelayStrategy delayStrategy, TimeSpan timeout) : base(maxRetries, delayStrategy) + { + _timeout = timeout; + } + + protected internal override bool ShouldRetry(HttpMessage message, Exception exception) + { + return ShouldRetryInternalAsync(message, exception, false).EnsureCompleted(); + } + protected internal override ValueTask ShouldRetryAsync(HttpMessage message, Exception exception) + { + return ShouldRetryInternalAsync(message, exception, true); + } + + private ValueTask ShouldRetryInternalAsync(HttpMessage message, Exception exception, bool async) + { + TimeSpan elapsedTime = message.ProcessingContext.StartTime - DateTimeOffset.UtcNow; + if (elapsedTime > _timeout) + { + return new ValueTask(false); + } + + return async ? base.ShouldRetryAsync(message, exception) : new ValueTask(base.ShouldRetry(message, exception)); + } +} +``` + +Here is how we would configure the client to use the policy we just created. + +```C# Snippet:SetGlobalTimeoutRetryPolicy +var delay = DelayStrategy.CreateFixedDelayStrategy(TimeSpan.FromSeconds(2)); +SecretClientOptions options = new SecretClientOptions() +{ + RetryPolicy = new GlobalTimeoutRetryPolicy(maxRetries: 4, delayStrategy: delay, timeout: TimeSpan.FromSeconds(30)) +}; +``` + +Another scenario where it may be helpful to use a custom retry policy is when you need to customize the delay behavior, but don't need to adjust the logic used to determine whether a request should be retried or not. In this case, it isn't necessary to create a custom `RetryPolicy` class - instead, you can pass in a `DelayStrategy` into the `RetryPolicy` constructor. + +In the below example, we create a customized delay strategy that uses a fixed sequence of delays that are iterated through as the number of retries increases. We then pass the strategy into the `RetryPolicy` constructor and set the constructed policy in our options. +```C# Snippet:SequentialDelayStrategy +public class SequentialDelayStrategy : DelayStrategy +{ + private static readonly TimeSpan[] PollingSequence = new TimeSpan[] + { + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(2), + TimeSpan.FromSeconds(4), + TimeSpan.FromSeconds(8), + TimeSpan.FromSeconds(16), + TimeSpan.FromSeconds(32) + }; + private static readonly TimeSpan MaxDelay = PollingSequence[PollingSequence.Length - 1]; + + protected override TimeSpan GetNextDelayCore(Response response, int retryNumber) + { + int index = retryNumber - 1; + return index >= PollingSequence.Length ? MaxDelay : PollingSequence[index]; + } +} +``` + +Here is how the custom delay would be used in the client options. +```C# Snippet:CustomizedDelay +SecretClientOptions options = new SecretClientOptions() +{ + RetryPolicy = new RetryPolicy(delayStrategy: new SequentialDelayStrategy()) +}; +``` + +It's also possible to have full control over the retry logic by setting the `RetryPolicy` property to an implementation of `HttpPipelinePolicy` where you would need to implement the retry loop yourself. One use case for this is if you want to implement your own retry policy with Polly. Note that if you replace the `RetryPolicy` with a `HttpPipelinePolicy`, you will need to make sure to update the `HttpMessage.ProcessingContext` that other pipeline policies may be relying on. + +```C# Snippet:PollyPolicy +internal class PollyPolicy : HttpPipelinePolicy +{ + public override void Process(HttpMessage message, ReadOnlyMemory pipeline) + { + Policy.Handle() + .Or(ex => ex.Status == 0) + .OrResult(r => r.Status >= 400) + .WaitAndRetry( + new[] + { + // some custom retry delay pattern + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(2), + TimeSpan.FromSeconds(3) + }, + onRetry: (result, _) => + { + // Since we are overriding the RetryPolicy, it is our responsibility to increment the RetryNumber + // that other policies in the pipeline may be depending on. + var context = message.ProcessingContext; + context.RetryNumber++; + } + ) + .Execute(() => + { + ProcessNext(message, pipeline); + return message.Response; + }); + } + + public override ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline) + { + // async version omitted for brevity + throw new NotImplementedException(); + } +} +``` + +To set the policy, use the `RetryPolicy` property of client options class. +```C# Snippet:SetPollyRetryPolicy +SecretClientOptions options = new SecretClientOptions() +{ + RetryPolicy = new PollyPolicy() +}; +``` + +> **_A note to library authors:_** +Library-specific response classifiers _will_ be respected if a user sets a custom policy deriving from `RetryPolicy` as long as they call into the base `ShouldRetry` method. If a user doesn't call the base method, or sets a `HttpPipelinePolicy` in the `RetryPolicy` property, then the library-specific response classifiers _will not_ be respected. + +## User provided HttpClient instance + +```C# Snippet:SettingHttpClient +using HttpClient client = new HttpClient(); + +SecretClientOptions options = new SecretClientOptions +{ + Transport = new HttpClientTransport(client) +}; +``` + +## Configuring a proxy + +```C# Snippet:HttpClientProxyConfiguration +using HttpClientHandler handler = new HttpClientHandler() +{ + Proxy = new WebProxy(new Uri("http://example.com")) +}; + +SecretClientOptions options = new SecretClientOptions +{ + Transport = new HttpClientTransport(handler) +}; +``` + +## Configuring a proxy using environment variables + +You can also configure a proxy using the following environment variables: + +* `HTTP_PROXY`: the proxy server used on HTTP requests. +* `HTTPS_PROXY`: the proxy server used on HTTPS requests. +* `ALL_PROXY`: the proxy server used on HTTP and HTTPS requests in case `HTTP_PROXY` or `HTTPS_PROXY` are not defined. +* `NO_PROXY`: a comma-separated list of hostnames that should be excluded from proxying. + +**Warning:** setting these environment variables will affect every new client created within the current process. diff --git a/sdk/core/System.ClientModel/samples/ConvenienceMethods.md b/sdk/core/System.ClientModel/samples/ConvenienceMethods.md new file mode 100644 index 000000000000..e4390e25a1fa --- /dev/null +++ b/sdk/core/System.ClientModel/samples/ConvenienceMethods.md @@ -0,0 +1,100 @@ +# Azure.Core Response samples + +**NOTE:** Samples in this file apply only to packages that follow [Azure SDK Design Guidelines](https://azure.github.io/azure-sdk/dotnet_introduction.html). Names of such packages usually start with `Azure`. + +Most client methods return one of the following types: + +- `Response` - An HTTP response. +- `Response` - A value and HTTP response. +- `Pageable` - A collection of values retrieved synchronously in pages. See [Pagination with the Azure SDK for .NET](https://docs.microsoft.com/dotnet/azure/sdk/pagination). +- `AsyncPageable` - A collection of values retrieved asynchronously in pages. See [Pagination with the Azure SDK for .NET](https://docs.microsoft.com/dotnet/azure/sdk/pagination). +- `*Operation` - A long-running operation. See [long running operation samples](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/samples/LongRunningOperations.md). + +## Accessing HTTP response properties + +```C# Snippet:ResponseTHelloWorld +// create a client +var client = new SecretClient(new Uri("http://example.com"), new DefaultAzureCredential()); + +// call a service method, which returns Response +Response response = await client.GetSecretAsync("SecretName"); + +// Response has two main accessors. +// Value property for accessing the deserialized result of the call +KeyVaultSecret secret = response.Value; + +// .. and GetRawResponse method for accessing all the details of the HTTP response +Response http = response.GetRawResponse(); + +// for example, you can access HTTP status +int status = http.Status; + +// or the headers +foreach (HttpHeader header in http.Headers) +{ + Console.WriteLine($"{header.Name} {header.Value}"); +} +``` + +## Accessing HTTP response content with dynamic + +If a service method does not return `Response`, JSON content can be accessed using `dynamic`. + +```C# Snippet:AzureCoreGetDynamicJsonProperty +Response response = client.GetWidget(); +dynamic widget = response.Content.ToDynamicFromJson(); +string name = widget.name; +``` + +See [dynamic content samples](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/samples/DynamicContent.md) for more details. + +## Accessing HTTP response content with ContentStream + +```C# Snippet:ResponseTContent +// call a service method, which returns Response +Response response = await client.GetSecretAsync("SecretName"); + +Response http = response.GetRawResponse(); + +Stream contentStream = http.ContentStream; + +// Rewind the stream +contentStream.Position = 0; + +using (StreamReader reader = new StreamReader(contentStream)) +{ + Console.WriteLine(reader.ReadToEnd()); +} +``` + +## Accessing HTTP response well-known headers + +You can access well known response headers via properties of `ResponseHeaders` object: + +```C# Snippet:ResponseHeaders +// call a service method, which returns Response +Response response = await client.GetSecretAsync("SecretName"); + +Response http = response.GetRawResponse(); + +Console.WriteLine("ETag " + http.Headers.ETag); +Console.WriteLine("Content-Length " + http.Headers.ContentLength); +Console.WriteLine("Content-Type " + http.Headers.ContentType); +``` + +## Handling exceptions + +When a service call fails `Azure.RequestFailedException` would get thrown. The exception type provides a Status property with an HTTP status code an an ErrorCode property with a service-specific error code. + +```C# Snippet:RequestFailedException +try +{ + KeyVaultSecret secret = client.GetSecret("NonexistentSecret"); +} +// handle exception with status code 404 +catch (RequestFailedException e) when (e.Status == 404) +{ + // handle not found error + Console.WriteLine("ErrorCode " + e.ErrorCode); +} +``` diff --git a/sdk/core/System.ClientModel/samples/Pipeline.md b/sdk/core/System.ClientModel/samples/Pipeline.md new file mode 100644 index 000000000000..03e8afcfa78a --- /dev/null +++ b/sdk/core/System.ClientModel/samples/Pipeline.md @@ -0,0 +1,95 @@ +# Azure.Core pipeline samples + +**NOTE:** Samples in this file apply only to packages that follow [Azure SDK Design Guidelines](https://azure.github.io/azure-sdk/dotnet_introduction.html). Names of such packages usually start with `Azure`. + +Before request is sent to the service it travels through the pipeline which consists of a set of policies that get to modify the request before it's being sent and observe the response after it's received and a transport that is responsible for sending request and receiving the response. + +## Adding custom policy to the pipeline + +Azure SDKs provides a way to add policies to the pipeline at two positions: + +- per-call policies get executed once per request + +```C# Snippet:AddPerCallPolicy +SecretClientOptions options = new SecretClientOptions(); +options.AddPolicy(new CustomRequestPolicy(), HttpPipelinePosition.PerCall); +``` + +- per-retry policies get executed every time request is retried, see [Retries samples](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/samples/Configuration.md#configuring-retry-options) for how to configure retries. + +```C# Snippet:AddPerRetryPolicy +options.AddPolicy(new StopwatchPolicy(), HttpPipelinePosition.PerRetry); +``` + +## Implementing a policy + +To implement a policy create a class deriving from `HttpPipelinePolicy` and overide `ProcessAsync` and `Process` methods. Request can be accessed via `message.Request`. Response is accessible via `message.Response` but only after `ProcessNextAsync`/`ProcessNext` was called. + +```C# Snippet:StopwatchPolicy +public class StopwatchPolicy : HttpPipelinePolicy +{ + public override async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline) + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); + + await ProcessNextAsync(message, pipeline); + + stopwatch.Stop(); + + Console.WriteLine($"Request to {message.Request.Uri} took {stopwatch.Elapsed}"); + } + + public override void Process(HttpMessage message, ReadOnlyMemory pipeline) + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); + + ProcessNext(message, pipeline); + + stopwatch.Stop(); + + Console.WriteLine($"Request to {message.Request.Uri} took {stopwatch.Elapsed}"); + } +} +``` + +## Implementing a synchronous policy + +If your policy doesn't do any asynchronous operations you can derive from `HttpPipelineSynchronousPolicy` and override `OnSendingRequest` or `OnResponseReceived` method. + +Below is an example on how to modify request before sending it to back-end. + +```C# Snippet:SyncPolicy +public class CustomRequestPolicy : HttpPipelineSynchronousPolicy +{ + public override void OnSendingRequest(HttpMessage message) + { + message.Request.Uri.AppendQuery("additional-query-parameter", "42"); + message.Request.Headers.Add("Custom-Header", "Value"); + } +} +``` + +## Customizing error formatting + +The pipeline can be configured to utilize a custom error response parser. This is typically needed when a service does not always conform to the standard Azure error format, or when additional information needs to be added to an error. + +To configure custom error formatting, a client must implement a `RequestFailedDetailsParser` and provide it to the `HttpPipelineOptions` when building the pipeline. + +### Create an implementation + +An example implementation can be found [here](https://github.com/Azure/azure-sdk-for-net/blob/02ca346fdff349be0d9181955f36c60497fa5c60/sdk/tables/Azure.Data.Tables/src/TablesRequestFailedDetailsParser.cs) + +### Configure the pipeline with a custom `RequestFailedDetailsParser` + +Below is an example of how clients would specify their custom parser in the `HttpPiplineOptions` + +```C# Snippet:RequestFailedDetailsParser +var pipelineOptions = new HttpPipelineOptions(options) +{ + RequestFailedDetailsParser = new FooClientRequestFailedDetailsParser() +}; + +_pipeline = HttpPipelineBuilder.Build(pipelineOptions); +``` diff --git a/sdk/core/System.ClientModel/samples/ProtocolMethods.md b/sdk/core/System.ClientModel/samples/ProtocolMethods.md new file mode 100644 index 000000000000..633023823d58 --- /dev/null +++ b/sdk/core/System.ClientModel/samples/ProtocolMethods.md @@ -0,0 +1,131 @@ +# C# Azure SDK Clients that Contain Protocol Methods + +## Introduction + +Azure SDK clients provide an interface to Azure services by translating library calls to REST requests. + +In Azure SDK clients, there are two ways to expose the schematized body in the request or response, known as the `message body`: + +- Most Azure SDK Clients expose methods that take ['model types'](https://azure.github.io/azure-sdk/dotnet_introduction.html#dotnet-model-types) as parameters, C# classes which map to the `message body` of the REST call. Those methods can be called here '**convenience methods**'. + +- However, some clients expose methods that mirror the message body directly. Those methods are called here '**protocol methods**', as they provide more direct access to the REST protocol used by the client library. + +### Pet's Example + +To compare the two approaches, imagine a service that stores information about pets, with a pair of `GetPet` and `SetPet` operations. + +Pets are represented in the message body as a JSON object: + +```json +{ + "name": "snoopy", + "species": "beagle" +} +``` + +An API using model types could be: + +```csharp +// This is an example model class +public class Pet +{ + string Name { get; } + string Species { get; } +} + +Response GetPet(string dogName); +Response SetPet(Pet dog); +``` + +While the protocol methods version would be: + +```csharp +// Request: "id" in the context path, like "/pets/{id}" +// Response: { +// "name": "snoopy", +// "species": "beagle" +// } +Response GetPet(string id, RequestContext context = null) +// Request: { +// "name": "snoopy", +// "species": "beagle" +// } +// Response: { +// "name": "snoopy", +// "species": "beagle" +// } +Response SetPet(RequestContent requestBody, RequestContext context = null); +``` + +**[Note]**: This document is a general quickstart in using SDK Clients that expose '**protocol methods**'. + +## Usage + +The basic structure of calling protocol methods remains the same as that of convenience methods: + +1. [Initialize Your Client](#1-initialize-your-client "Initialize Your Client") + +2. [Create and Send a request](#2-create-and-send-a-request "Create and Send a Request") + +3. [Handle the Response](#3-handle-the-response "Handle the Response") + +### 1. Initialize Your Client + +The first step in interacting with a service via protocol methods is to create a client instance. + +```csharp +using System; +using Azure.Pets; +using Azure.Core; +using Azure.Identity; + +const string endpoint = "http://localhost:3000"; +var credential = new AzureKeyCredential(/*SERVICE-API-KEY*/); +var client = new PetStoreClient(new Uri(endpoint), credential, new PetStoreClientOptions()); +``` + +### 2. Create and Send a Request + +Protocol methods need a JSON object of the shape required by the schema of the service. + +See the specific service documentation for details, but as an example: + +```csharp +// anonymous class is serialized by System.Text.Json using runtime reflection +var data = new { + name = "snoopy", + species = "beagle" +}; +/* +{ + "name": "snoopy", + "species": "beagle" +} +*/ +client.SetPet(RequestContent.Create(data)); +``` + +### 3. Handle the Response + +Protocol methods all return a `Response` object that contains information returned from the service request. + +The most important field on Response contains the REST content returned from the service: + +```C# Snippet:GetPetAsync +Response response = await client.GetPetAsync("snoopy", new RequestContext()); + +var doc = JsonDocument.Parse(response.Content.ToMemory()); +var name = doc.RootElement.GetProperty("name").GetString(); +``` + +JSON properties can also be accessed using a dynamic layer. + +```C# Snippet:AzureCoreGetDynamicJsonProperty +Response response = client.GetWidget(); +dynamic widget = response.Content.ToDynamicFromJson(); +string name = widget.name; +``` + +## Configuration And Customization + +**Protocol methods** share the same configuration and customization as **convenience methods**. For details, see the [ReadMe](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/README.md). You can find more samples [here](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/samples/README.md). diff --git a/sdk/core/System.ClientModel/samples/README.md b/sdk/core/System.ClientModel/samples/README.md new file mode 100644 index 000000000000..ae12cbb7fe6f --- /dev/null +++ b/sdk/core/System.ClientModel/samples/README.md @@ -0,0 +1,16 @@ +--- +page_type: sample +languages: +- csharp +products: +- system.clientmodel +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) +- [Convenience Methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ConvenienceMethods.md) +- [Protocol Methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ProtocolMethods.md) +- [Client Pipeline](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/Pipeline.md) diff --git a/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs b/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs new file mode 100644 index 000000000000..3154bbb88e08 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Azure.Core.Pipeline; +using Azure.Identity; +using Azure.Security.KeyVault.Secrets; +using NUnit.Framework; + +namespace Azure.Core.Samples +{ + public class ConfigurationSamples + { + [Test] + public void ConfigurationHelloWorld() + { + #region Snippet:ConfigurationHelloWorld + + SecretClientOptions options = new SecretClientOptions() + { + Retry = + { + Delay = TimeSpan.FromSeconds(2), + MaxRetries = 10, + Mode = RetryMode.Fixed + }, + Diagnostics = + { + IsLoggingContentEnabled = true, + ApplicationId = "myApplicationId" + } + }; + + SecretClient client = new SecretClient(new Uri("http://example.com"), new DefaultAzureCredential(), options); + #endregion + } + + [Test] + public void RetryOptions() + { + #region Snippet:RetryOptions + + SecretClientOptions options = new SecretClientOptions() + { + Retry = + { + Delay = TimeSpan.FromSeconds(2), + MaxRetries = 10, + Mode = RetryMode.Fixed + } + }; + + #endregion + } + + [Test] + public void SettingHttpClient() + { + #region Snippet:SettingHttpClient + + using HttpClient client = new HttpClient(); + + SecretClientOptions options = new SecretClientOptions + { + Transport = new HttpClientTransport(client) + }; + + #endregion + } + + [Test] + public void HttpClientProxyConfiguration() + { + #region Snippet:HttpClientProxyConfiguration + + using HttpClientHandler handler = new HttpClientHandler() + { + Proxy = new WebProxy(new Uri("http://example.com")) + }; + + SecretClientOptions options = new SecretClientOptions + { + Transport = new HttpClientTransport(handler) + }; + + #endregion + } + + [Test] + public void SetPollyRetryPolicy() + { + #region Snippet:SetPollyRetryPolicy + SecretClientOptions options = new SecretClientOptions() + { + RetryPolicy = new PollyPolicy() + }; + #endregion + } + + [Test] + public void SetGlobalTimeoutRetryPolicy() + { + #region Snippet:SetGlobalTimeoutRetryPolicy + + var delay = DelayStrategy.CreateFixedDelayStrategy(TimeSpan.FromSeconds(2)); + SecretClientOptions options = new SecretClientOptions() + { + RetryPolicy = new GlobalTimeoutRetryPolicy(maxRetries: 4, delayStrategy: delay, timeout: TimeSpan.FromSeconds(30)) + }; + #endregion + } + + [Test] + public void CustomizedDelayStrategy() + { + #region Snippet:CustomizedDelay + SecretClientOptions options = new SecretClientOptions() + { + RetryPolicy = new RetryPolicy(delayStrategy: new SequentialDelayStrategy()) + }; + #endregion + } + + #region Snippet:SequentialDelayStrategy + public class SequentialDelayStrategy : DelayStrategy + { + private static readonly TimeSpan[] PollingSequence = new TimeSpan[] + { + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(2), + TimeSpan.FromSeconds(4), + TimeSpan.FromSeconds(8), + TimeSpan.FromSeconds(16), + TimeSpan.FromSeconds(32) + }; + private static readonly TimeSpan MaxDelay = PollingSequence[PollingSequence.Length - 1]; + + protected override TimeSpan GetNextDelayCore(Response response, int retryNumber) + { + int index = retryNumber - 1; + return index >= PollingSequence.Length ? MaxDelay : PollingSequence[index]; + } + } + #endregion + } +} diff --git a/sdk/core/System.ClientModel/tests/Samples/ReadmeModelReaderWriter.cs b/sdk/core/System.ClientModel/tests/Samples/ModelReaderWriterSamples.cs similarity index 100% rename from sdk/core/System.ClientModel/tests/Samples/ReadmeModelReaderWriter.cs rename to sdk/core/System.ClientModel/tests/Samples/ModelReaderWriterSamples.cs diff --git a/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs b/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs new file mode 100644 index 000000000000..7a7fb80fd5c6 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using Azure.Core.Pipeline; +using Azure.Security.KeyVault.Secrets; +using NUnit.Framework; + +namespace Azure.Core.Samples +{ + public class PipelineSamples + { + [Test] + public void AddPolicies() + { + #region Snippet:AddPerCallPolicy + SecretClientOptions options = new SecretClientOptions(); + options.AddPolicy(new CustomRequestPolicy(), HttpPipelinePosition.PerCall); + #endregion + + #region Snippet:AddPerRetryPolicy + options.AddPolicy(new StopwatchPolicy(), HttpPipelinePosition.PerRetry); + #endregion + } + + #region Snippet:StopwatchPolicy + public class StopwatchPolicy : HttpPipelinePolicy + { + public override async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline) + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); + + await ProcessNextAsync(message, pipeline); + + stopwatch.Stop(); + + Console.WriteLine($"Request to {message.Request.Uri} took {stopwatch.Elapsed}"); + } + + public override void Process(HttpMessage message, ReadOnlyMemory pipeline) + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); + + ProcessNext(message, pipeline); + + stopwatch.Stop(); + + Console.WriteLine($"Request to {message.Request.Uri} took {stopwatch.Elapsed}"); + } + } + #endregion + + #region Snippet:SyncPolicy + public class CustomRequestPolicy : HttpPipelineSynchronousPolicy + { + public override void OnSendingRequest(HttpMessage message) + { + message.Request.Uri.AppendQuery("additional-query-parameter", "42"); + message.Request.Headers.Add("Custom-Header", "Value"); + } + } + #endregion + + private class RequestFailedDetailsParserSample + { + public SampleClientOptions options; + private readonly HttpPipeline _pipeline; + + public RequestFailedDetailsParserSample() + { + options = new(); + #region Snippet:RequestFailedDetailsParser + var pipelineOptions = new HttpPipelineOptions(options) + { + RequestFailedDetailsParser = new FooClientRequestFailedDetailsParser() + }; + + _pipeline = HttpPipelineBuilder.Build(pipelineOptions); + #endregion + if (_pipeline == null) + { throw new Exception(); }; + } + } + + private class SampleClientOptions : ClientOptions { } + private class FooClientRequestFailedDetailsParser : RequestFailedDetailsParser + { + public override bool TryParse(Response response, out ResponseError error, out IDictionary data) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/sdk/core/System.ClientModel/tests/Samples/ResponseSamples.cs b/sdk/core/System.ClientModel/tests/Samples/ResponseSamples.cs new file mode 100644 index 000000000000..b831e4eaf6e1 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/Samples/ResponseSamples.cs @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Azure.Identity; +using Azure.Security.KeyVault.Secrets; +using NUnit.Framework; + +namespace Azure.Core.Samples +{ + public class ResponseSamples + { + [Test] + [Ignore("Only verifying that the sample builds")] + public async Task ResponseTHelloWorld() + { + #region Snippet:ResponseTHelloWorld + // create a client + var client = new SecretClient(new Uri("http://example.com"), new DefaultAzureCredential()); + + // call a service method, which returns Response + Response response = await client.GetSecretAsync("SecretName"); + + // Response has two main accessors. + // Value property for accessing the deserialized result of the call + KeyVaultSecret secret = response.Value; + + // .. and GetRawResponse method for accessing all the details of the HTTP response + Response http = response.GetRawResponse(); + + // for example, you can access HTTP status + int status = http.Status; + + // or the headers + foreach (HttpHeader header in http.Headers) + { + Console.WriteLine($"{header.Name} {header.Value}"); + } + #endregion + } + + [Test] + [Ignore("Only verifying that the sample builds")] + public async Task ResponseTContent() + { + // create a client + var client = new SecretClient(new Uri("http://example.com"), new DefaultAzureCredential()); + + #region Snippet:ResponseTContent + // call a service method, which returns Response + Response response = await client.GetSecretAsync("SecretName"); + + Response http = response.GetRawResponse(); + + Stream contentStream = http.ContentStream; + + // Rewind the stream + contentStream.Position = 0; + + using (StreamReader reader = new StreamReader(contentStream)) + { + Console.WriteLine(reader.ReadToEnd()); + } + + #endregion + } + + [Test] + [Ignore("Only verifying that the sample builds")] + public async Task ResponseHeaders() + { + // create a client + var client = new SecretClient(new Uri("http://example.com"), new DefaultAzureCredential()); + + #region Snippet:ResponseHeaders + // call a service method, which returns Response + Response response = await client.GetSecretAsync("SecretName"); + + Response http = response.GetRawResponse(); + + Console.WriteLine("ETag " + http.Headers.ETag); + Console.WriteLine("Content-Length " + http.Headers.ContentLength); + Console.WriteLine("Content-Type " + http.Headers.ContentType); + #endregion + } + + [Test] + [Ignore("Only verifying that the sample builds")] + public async Task AsyncPageable() + { + // create a client + var client = new SecretClient(new Uri("http://example.com"), new DefaultAzureCredential()); + + #region Snippet:AsyncPageable + // call a service method, which returns AsyncPageable + AsyncPageable allSecretProperties = client.GetPropertiesOfSecretsAsync(); + + await foreach (SecretProperties secretProperties in allSecretProperties) + { + Console.WriteLine(secretProperties.Name); + } + #endregion + } + + [Test] + [Ignore("Only verifying that the sample builds")] + public async Task AsyncPageableLoop() + { + // create a client + var client = new SecretClient(new Uri("http://example.com"), new DefaultAzureCredential()); + + #region Snippet:AsyncPageableLoop + // call a service method, which returns AsyncPageable + AsyncPageable allSecretProperties = client.GetPropertiesOfSecretsAsync(); + + IAsyncEnumerator enumerator = allSecretProperties.GetAsyncEnumerator(); + try + { + while (await enumerator.MoveNextAsync()) + { + SecretProperties secretProperties = enumerator.Current; + Console.WriteLine(secretProperties.Name); + } + } + finally + { + await enumerator.DisposeAsync(); + } + #endregion + } + + [Test] + [Ignore("Only verifying that the sample builds")] + public void RequestFailedException() + { + // create a client + var client = new SecretClient(new Uri("http://example.com"), new DefaultAzureCredential()); + + #region Snippet:RequestFailedException + try + { + KeyVaultSecret secret = client.GetSecret("NonexistentSecret"); + } + // handle exception with status code 404 + catch (RequestFailedException e) when (e.Status == 404) + { + // handle not found error + Console.WriteLine("ErrorCode " + e.ErrorCode); + } + #endregion + } + } +} From e1f7a82ce201fb108afe420b1800d2eb990b0a0d Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 4 Mar 2024 09:43:42 -0800 Subject: [PATCH 02/40] README updates --- sdk/core/System.ClientModel/README.md | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/sdk/core/System.ClientModel/README.md b/sdk/core/System.ClientModel/README.md index 11b8de25f8db..d4d52fb5a3e0 100644 --- a/sdk/core/System.ClientModel/README.md +++ b/sdk/core/System.ClientModel/README.md @@ -42,7 +42,7 @@ Below, you will find sections explaining these shared concepts in more detail. ### Configuring service clients -`System.ClientModel`-based clients provide a constructor that takes a service endpoint and a credential used to authenticate with the service. They also provide an overload that takes an endpoint, a credential, and an instance of `ClientPipelineOptions` that can be used to configure the pipeline the client uses to send and receive HTTP requests and responses. +`System.ClientModel`-based 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` that can be used to configure the pipeline the client uses to send and receive HTTP requests and responses. `ClientPipelineOptions` allows overriding default client values for things like the network timeout used when sending a request or the maximum number of retries to send when a request fails. @@ -57,9 +57,14 @@ _Service clients_ have methods that are used to call cloud services to invoke se `System.ClientModel`-based clients expose two types of service methods: _convenience methods_ and _protocol methods_. -**Convenience methods** are methods that take a strongly-typed model as input and return a `ClientResult` that holds a strongly-typed representation of the service response. Details from the HTTP response can be obtained from the return value. +**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` that holds a strongly-typed representation of the service response. Details from the HTTP response can be obtained from the return value. -**Protocol method** are low-level methods that take parameters that correspond to the service HTTP API and return only the raw HTTP response details. These methods also take an optional `RequestOptions` value that allows the client pipeline and the request to be configured for the duration of the call. +**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` value that allows the client pipeline and the request to be configured for the duration of the call. + +```C# Snippet:ClientResultTReadme +``` + +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) // TODO: move the below to the detailed sample file @@ -67,12 +72,22 @@ _Service clients_ have methods that are used to call cloud services to invoke se **Protocol methods** are service methods that provide very little convenience over the raw HTTP APIs a cloud service exposes. They represent request and response message bodies using types that are very thin layers over raw JSON/binary/other formats. Users of client protocol methods must reference a service's API documentation directly, rather than relying on the client to provide developer conveniences via strongly-typing service schemas. +### Handling exceptions that result from failed requests +When a service call fails, `System.ClientModel`-based clients throw a `ClientResultException`. The exception exposes the HTTP status code and the details of the service response if available. -### Handling exceptions that result from failed requests +```C# Snippet:ClientResultExceptionReadme +``` ### Customizing HTTP requests +`System.ClientModel`-based clients expose low-level _protocol methods_ that allow callers to customize the details of HTTP requests. Protocol methods take an optional `RequestOptions` value that allows callers to add a header to the request, or to add a policy to the client pipeline that can modify the request in any way before sending it to the service. `RequestOptions` also allows passing a `CancellationToken` to the method. + +```C# Snippet:RequestOptionsReadme +``` + +For more information on customizing request, 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` or `IJsonModel` which will give library users the ability to read and write your models. From e43c767975b194abcfaa09d070e298ee3c46130d Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 4 Mar 2024 10:27:38 -0800 Subject: [PATCH 03/40] Configuration samples --- sdk/core/System.ClientModel/README.md | 2 +- .../samples/Configuration.md | 191 ++++------------- .../samples/ConvenienceMethods.md | 100 --------- .../System.ClientModel/samples/Pipeline.md | 95 --------- .../samples/ServiceMethods.md | 191 +++++++++++++++++ .../tests/Samples/ConfigurationSamples.cs | 197 +++++++----------- .../tests/Samples/PipelineSamples.cs | 99 --------- .../TestClients/MapsClient/CountryRegion.cs | 69 ++++++ .../MapsClient/IPAddressCountryPair.cs | 101 +++++++++ .../TestClients/MapsClient/MapsClient.cs | 136 ++++++++++++ .../MapsClient/MapsClientOptions.cs | 28 +++ 11 files changed, 645 insertions(+), 564 deletions(-) delete mode 100644 sdk/core/System.ClientModel/samples/ConvenienceMethods.md delete mode 100644 sdk/core/System.ClientModel/samples/Pipeline.md create mode 100644 sdk/core/System.ClientModel/samples/ServiceMethods.md delete mode 100644 sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs create mode 100644 sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/CountryRegion.cs create mode 100644 sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/IPAddressCountryPair.cs create mode 100644 sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClient.cs create mode 100644 sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClientOptions.cs diff --git a/sdk/core/System.ClientModel/README.md b/sdk/core/System.ClientModel/README.md index d4d52fb5a3e0..53e953e5393c 100644 --- a/sdk/core/System.ClientModel/README.md +++ b/sdk/core/System.ClientModel/README.md @@ -28,7 +28,7 @@ The `System.ClientModel` package provides an `ApiKeyCredential` type for authent ## Key concepts -The main concepts used by types in `System.ClientModel` include: +The main concepts in `System.ClientModel` include: - Configuring service clients (`ClientPipelineOptions`). - Accessing HTTP response details (`ClientResult`, `ClientResult`). diff --git a/sdk/core/System.ClientModel/samples/Configuration.md b/sdk/core/System.ClientModel/samples/Configuration.md index be0a6afa1c59..05628f22827a 100644 --- a/sdk/core/System.ClientModel/samples/Configuration.md +++ b/sdk/core/System.ClientModel/samples/Configuration.md @@ -1,161 +1,77 @@ # System.ClientModel-based client configuration samples -## Configuring retry options +## Configuring retries -To modify the retry options, use the `Retry` property of the `ClientOptions` class. +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 are setup to retry 3 times using an exponential retry strategy with an initial delay of 0.8 sec, and a max delay of 1 minute. -```C# Snippet:RetryOptions -SecretClientOptions options = new SecretClientOptions() -{ - Retry = - { - Delay = TimeSpan.FromSeconds(2), - MaxRetries = 10, - Mode = RetryMode.Fixed - } -}; +```C# Snippet:ConfigurationCustomizeRetries ``` -## Setting a custom retry policy +## Add a custom policy to the pipeline -Using `RetryOptions` to configure retry behavior is sufficient for the vast majority of scenarios. For more advanced scenarios, it's possible to use a custom retry policy by setting the `RetryPolicy` property of client options class. This can be accomplished by implementing a retry policy that derives from the `RetryPolicy` class, or by passing in a `DelayStrategy` into the existing `RetryPolicy` constructor. The `RetryPolicy` class contains hooks to determine if a request should be retried and how long to wait before retrying. +Azure SDKs provides a way to add policies to the pipeline at three positions: -In the following example, we implement a policy that will prevent retries from taking place if the overall processing time has exceeded a configured threshold. Notice that the policy takes in `RetryOptions` as one of the constructor parameters and passes it to the base constructor. By doing this, we are able to delegate to the base `RetryPolicy` as needed (either by explicitly invoking the base methods, or by not overriding methods that we do not need to customize) which will respect the `RetryOptions`. +- per-call policies are run once per request -```C# Snippet:GlobalTimeoutRetryPolicy -internal class GlobalTimeoutRetryPolicy : RetryPolicy -{ - private readonly TimeSpan _timeout; - - public GlobalTimeoutRetryPolicy(int maxRetries, DelayStrategy delayStrategy, TimeSpan timeout) : base(maxRetries, delayStrategy) - { - _timeout = timeout; - } - - protected internal override bool ShouldRetry(HttpMessage message, Exception exception) - { - return ShouldRetryInternalAsync(message, exception, false).EnsureCompleted(); - } - protected internal override ValueTask ShouldRetryAsync(HttpMessage message, Exception exception) - { - return ShouldRetryInternalAsync(message, exception, true); - } +```C# Snippet:ConfigurationAddPerCallPolicy +SecretClientOptions options = new SecretClientOptions(); +options.AddPolicy(new CustomRequestPolicy(), HttpPipelinePosition.PerCall); +``` - private ValueTask ShouldRetryInternalAsync(HttpMessage message, Exception exception, bool async) - { - TimeSpan elapsedTime = message.ProcessingContext.StartTime - DateTimeOffset.UtcNow; - if (elapsedTime > _timeout) - { - return new ValueTask(false); - } +- per-try policies are run each time the request is tried - return async ? base.ShouldRetryAsync(message, exception) : new ValueTask(base.ShouldRetry(message, exception)); - } -} +```C# Snippet:ConfigurationAddPerTryPolicy +options.AddPolicy(new StopwatchPolicy(), HttpPipelinePosition.PerRetry); ``` -Here is how we would configure the client to use the policy we just created. +- before-transport policies are run after other policies and before the request is sent -```C# Snippet:SetGlobalTimeoutRetryPolicy -var delay = DelayStrategy.CreateFixedDelayStrategy(TimeSpan.FromSeconds(2)); -SecretClientOptions options = new SecretClientOptions() -{ - RetryPolicy = new GlobalTimeoutRetryPolicy(maxRetries: 4, delayStrategy: delay, timeout: TimeSpan.FromSeconds(30)) -}; +Adding policies at this 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. + +```C# Snippet:ConfigurationAddBeforeTransportPolicy +options.AddPolicy(new StopwatchPolicy(), HttpPipelinePosition.BeforeTransport); ``` -Another scenario where it may be helpful to use a custom retry policy is when you need to customize the delay behavior, but don't need to adjust the logic used to determine whether a request should be retried or not. In this case, it isn't necessary to create a custom `RetryPolicy` class - instead, you can pass in a `DelayStrategy` into the `RetryPolicy` constructor. +## Implement a custom policy -In the below example, we create a customized delay strategy that uses a fixed sequence of delays that are iterated through as the number of retries increases. We then pass the strategy into the `RetryPolicy` constructor and set the constructed policy in our options. -```C# Snippet:SequentialDelayStrategy -public class SequentialDelayStrategy : DelayStrategy +To implement a policy create a class deriving from `HttpPipelinePolicy` and overide `ProcessAsync` and `Process` methods. Request can be accessed via `message.Request`. Response is accessible via `message.Response` but only after `ProcessNextAsync`/`ProcessNext` was called. + +```C# Snippet:ConfigurationCustomPolicy +public class StopwatchPolicy : HttpPipelinePolicy { - private static readonly TimeSpan[] PollingSequence = new TimeSpan[] - { - TimeSpan.FromSeconds(1), - TimeSpan.FromSeconds(1), - TimeSpan.FromSeconds(1), - TimeSpan.FromSeconds(2), - TimeSpan.FromSeconds(4), - TimeSpan.FromSeconds(8), - TimeSpan.FromSeconds(16), - TimeSpan.FromSeconds(32) - }; - private static readonly TimeSpan MaxDelay = PollingSequence[PollingSequence.Length - 1]; - - protected override TimeSpan GetNextDelayCore(Response response, int retryNumber) + public override async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline) { - int index = retryNumber - 1; - return index >= PollingSequence.Length ? MaxDelay : PollingSequence[index]; - } -} -``` + var stopwatch = new Stopwatch(); + stopwatch.Start(); -Here is how the custom delay would be used in the client options. -```C# Snippet:CustomizedDelay -SecretClientOptions options = new SecretClientOptions() -{ - RetryPolicy = new RetryPolicy(delayStrategy: new SequentialDelayStrategy()) -}; -``` + await ProcessNextAsync(message, pipeline); -It's also possible to have full control over the retry logic by setting the `RetryPolicy` property to an implementation of `HttpPipelinePolicy` where you would need to implement the retry loop yourself. One use case for this is if you want to implement your own retry policy with Polly. Note that if you replace the `RetryPolicy` with a `HttpPipelinePolicy`, you will need to make sure to update the `HttpMessage.ProcessingContext` that other pipeline policies may be relying on. + stopwatch.Stop(); -```C# Snippet:PollyPolicy -internal class PollyPolicy : HttpPipelinePolicy -{ - public override void Process(HttpMessage message, ReadOnlyMemory pipeline) - { - Policy.Handle() - .Or(ex => ex.Status == 0) - .OrResult(r => r.Status >= 400) - .WaitAndRetry( - new[] - { - // some custom retry delay pattern - TimeSpan.FromSeconds(1), - TimeSpan.FromSeconds(2), - TimeSpan.FromSeconds(3) - }, - onRetry: (result, _) => - { - // Since we are overriding the RetryPolicy, it is our responsibility to increment the RetryNumber - // that other policies in the pipeline may be depending on. - var context = message.ProcessingContext; - context.RetryNumber++; - } - ) - .Execute(() => - { - ProcessNext(message, pipeline); - return message.Response; - }); + Console.WriteLine($"Request to {message.Request.Uri} took {stopwatch.Elapsed}"); } - public override ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline) + public override void Process(HttpMessage message, ReadOnlyMemory pipeline) { - // async version omitted for brevity - throw new NotImplementedException(); + var stopwatch = new Stopwatch(); + stopwatch.Start(); + + ProcessNext(message, pipeline); + + stopwatch.Stop(); + + Console.WriteLine($"Request to {message.Request.Uri} took {stopwatch.Elapsed}"); } } ``` -To set the policy, use the `RetryPolicy` property of client options class. -```C# Snippet:SetPollyRetryPolicy -SecretClientOptions options = new SecretClientOptions() -{ - RetryPolicy = new PollyPolicy() -}; -``` - -> **_A note to library authors:_** -Library-specific response classifiers _will_ be respected if a user sets a custom policy deriving from `RetryPolicy` as long as they call into the base `ShouldRetry` method. If a user doesn't call the base method, or sets a `HttpPipelinePolicy` in the `RetryPolicy` property, then the library-specific response classifiers _will not_ be respected. +## Provide a custom HttpClient instance -## User provided 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 create the custom `HttpClient` instance to its constructor. -```C# Snippet:SettingHttpClient +```C# Snippet:ConfigurationCustomHttpClient using HttpClient client = new HttpClient(); SecretClientOptions options = new SecretClientOptions @@ -163,28 +79,3 @@ SecretClientOptions options = new SecretClientOptions Transport = new HttpClientTransport(client) }; ``` - -## Configuring a proxy - -```C# Snippet:HttpClientProxyConfiguration -using HttpClientHandler handler = new HttpClientHandler() -{ - Proxy = new WebProxy(new Uri("http://example.com")) -}; - -SecretClientOptions options = new SecretClientOptions -{ - Transport = new HttpClientTransport(handler) -}; -``` - -## Configuring a proxy using environment variables - -You can also configure a proxy using the following environment variables: - -* `HTTP_PROXY`: the proxy server used on HTTP requests. -* `HTTPS_PROXY`: the proxy server used on HTTPS requests. -* `ALL_PROXY`: the proxy server used on HTTP and HTTPS requests in case `HTTP_PROXY` or `HTTPS_PROXY` are not defined. -* `NO_PROXY`: a comma-separated list of hostnames that should be excluded from proxying. - -**Warning:** setting these environment variables will affect every new client created within the current process. diff --git a/sdk/core/System.ClientModel/samples/ConvenienceMethods.md b/sdk/core/System.ClientModel/samples/ConvenienceMethods.md deleted file mode 100644 index e4390e25a1fa..000000000000 --- a/sdk/core/System.ClientModel/samples/ConvenienceMethods.md +++ /dev/null @@ -1,100 +0,0 @@ -# Azure.Core Response samples - -**NOTE:** Samples in this file apply only to packages that follow [Azure SDK Design Guidelines](https://azure.github.io/azure-sdk/dotnet_introduction.html). Names of such packages usually start with `Azure`. - -Most client methods return one of the following types: - -- `Response` - An HTTP response. -- `Response` - A value and HTTP response. -- `Pageable` - A collection of values retrieved synchronously in pages. See [Pagination with the Azure SDK for .NET](https://docs.microsoft.com/dotnet/azure/sdk/pagination). -- `AsyncPageable` - A collection of values retrieved asynchronously in pages. See [Pagination with the Azure SDK for .NET](https://docs.microsoft.com/dotnet/azure/sdk/pagination). -- `*Operation` - A long-running operation. See [long running operation samples](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/samples/LongRunningOperations.md). - -## Accessing HTTP response properties - -```C# Snippet:ResponseTHelloWorld -// create a client -var client = new SecretClient(new Uri("http://example.com"), new DefaultAzureCredential()); - -// call a service method, which returns Response -Response response = await client.GetSecretAsync("SecretName"); - -// Response has two main accessors. -// Value property for accessing the deserialized result of the call -KeyVaultSecret secret = response.Value; - -// .. and GetRawResponse method for accessing all the details of the HTTP response -Response http = response.GetRawResponse(); - -// for example, you can access HTTP status -int status = http.Status; - -// or the headers -foreach (HttpHeader header in http.Headers) -{ - Console.WriteLine($"{header.Name} {header.Value}"); -} -``` - -## Accessing HTTP response content with dynamic - -If a service method does not return `Response`, JSON content can be accessed using `dynamic`. - -```C# Snippet:AzureCoreGetDynamicJsonProperty -Response response = client.GetWidget(); -dynamic widget = response.Content.ToDynamicFromJson(); -string name = widget.name; -``` - -See [dynamic content samples](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/samples/DynamicContent.md) for more details. - -## Accessing HTTP response content with ContentStream - -```C# Snippet:ResponseTContent -// call a service method, which returns Response -Response response = await client.GetSecretAsync("SecretName"); - -Response http = response.GetRawResponse(); - -Stream contentStream = http.ContentStream; - -// Rewind the stream -contentStream.Position = 0; - -using (StreamReader reader = new StreamReader(contentStream)) -{ - Console.WriteLine(reader.ReadToEnd()); -} -``` - -## Accessing HTTP response well-known headers - -You can access well known response headers via properties of `ResponseHeaders` object: - -```C# Snippet:ResponseHeaders -// call a service method, which returns Response -Response response = await client.GetSecretAsync("SecretName"); - -Response http = response.GetRawResponse(); - -Console.WriteLine("ETag " + http.Headers.ETag); -Console.WriteLine("Content-Length " + http.Headers.ContentLength); -Console.WriteLine("Content-Type " + http.Headers.ContentType); -``` - -## Handling exceptions - -When a service call fails `Azure.RequestFailedException` would get thrown. The exception type provides a Status property with an HTTP status code an an ErrorCode property with a service-specific error code. - -```C# Snippet:RequestFailedException -try -{ - KeyVaultSecret secret = client.GetSecret("NonexistentSecret"); -} -// handle exception with status code 404 -catch (RequestFailedException e) when (e.Status == 404) -{ - // handle not found error - Console.WriteLine("ErrorCode " + e.ErrorCode); -} -``` diff --git a/sdk/core/System.ClientModel/samples/Pipeline.md b/sdk/core/System.ClientModel/samples/Pipeline.md deleted file mode 100644 index 03e8afcfa78a..000000000000 --- a/sdk/core/System.ClientModel/samples/Pipeline.md +++ /dev/null @@ -1,95 +0,0 @@ -# Azure.Core pipeline samples - -**NOTE:** Samples in this file apply only to packages that follow [Azure SDK Design Guidelines](https://azure.github.io/azure-sdk/dotnet_introduction.html). Names of such packages usually start with `Azure`. - -Before request is sent to the service it travels through the pipeline which consists of a set of policies that get to modify the request before it's being sent and observe the response after it's received and a transport that is responsible for sending request and receiving the response. - -## Adding custom policy to the pipeline - -Azure SDKs provides a way to add policies to the pipeline at two positions: - -- per-call policies get executed once per request - -```C# Snippet:AddPerCallPolicy -SecretClientOptions options = new SecretClientOptions(); -options.AddPolicy(new CustomRequestPolicy(), HttpPipelinePosition.PerCall); -``` - -- per-retry policies get executed every time request is retried, see [Retries samples](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/samples/Configuration.md#configuring-retry-options) for how to configure retries. - -```C# Snippet:AddPerRetryPolicy -options.AddPolicy(new StopwatchPolicy(), HttpPipelinePosition.PerRetry); -``` - -## Implementing a policy - -To implement a policy create a class deriving from `HttpPipelinePolicy` and overide `ProcessAsync` and `Process` methods. Request can be accessed via `message.Request`. Response is accessible via `message.Response` but only after `ProcessNextAsync`/`ProcessNext` was called. - -```C# Snippet:StopwatchPolicy -public class StopwatchPolicy : HttpPipelinePolicy -{ - public override async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline) - { - var stopwatch = new Stopwatch(); - stopwatch.Start(); - - await ProcessNextAsync(message, pipeline); - - stopwatch.Stop(); - - Console.WriteLine($"Request to {message.Request.Uri} took {stopwatch.Elapsed}"); - } - - public override void Process(HttpMessage message, ReadOnlyMemory pipeline) - { - var stopwatch = new Stopwatch(); - stopwatch.Start(); - - ProcessNext(message, pipeline); - - stopwatch.Stop(); - - Console.WriteLine($"Request to {message.Request.Uri} took {stopwatch.Elapsed}"); - } -} -``` - -## Implementing a synchronous policy - -If your policy doesn't do any asynchronous operations you can derive from `HttpPipelineSynchronousPolicy` and override `OnSendingRequest` or `OnResponseReceived` method. - -Below is an example on how to modify request before sending it to back-end. - -```C# Snippet:SyncPolicy -public class CustomRequestPolicy : HttpPipelineSynchronousPolicy -{ - public override void OnSendingRequest(HttpMessage message) - { - message.Request.Uri.AppendQuery("additional-query-parameter", "42"); - message.Request.Headers.Add("Custom-Header", "Value"); - } -} -``` - -## Customizing error formatting - -The pipeline can be configured to utilize a custom error response parser. This is typically needed when a service does not always conform to the standard Azure error format, or when additional information needs to be added to an error. - -To configure custom error formatting, a client must implement a `RequestFailedDetailsParser` and provide it to the `HttpPipelineOptions` when building the pipeline. - -### Create an implementation - -An example implementation can be found [here](https://github.com/Azure/azure-sdk-for-net/blob/02ca346fdff349be0d9181955f36c60497fa5c60/sdk/tables/Azure.Data.Tables/src/TablesRequestFailedDetailsParser.cs) - -### Configure the pipeline with a custom `RequestFailedDetailsParser` - -Below is an example of how clients would specify their custom parser in the `HttpPiplineOptions` - -```C# Snippet:RequestFailedDetailsParser -var pipelineOptions = new HttpPipelineOptions(options) -{ - RequestFailedDetailsParser = new FooClientRequestFailedDetailsParser() -}; - -_pipeline = HttpPipelineBuilder.Build(pipelineOptions); -``` diff --git a/sdk/core/System.ClientModel/samples/ServiceMethods.md b/sdk/core/System.ClientModel/samples/ServiceMethods.md new file mode 100644 index 000000000000..92edb02e1331 --- /dev/null +++ b/sdk/core/System.ClientModel/samples/ServiceMethods.md @@ -0,0 +1,191 @@ +# System.ClientModel-based client service methods + +## Introduction + +_Service clients_ have methods that are used to call cloud services to invoke service operations. These methods on a client are called _service methods_. + +`System.ClientModel`-based 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 service methods that take a strongly-typed model representing schematized data sent to the service as input, and return a strongly-typed model representing the payload from the service response as output. Having strongly-typed models that represent service concepts provides a layer of convenience over working with the raw payload format. This is because these models unify the client user experience when cloud services differ in payload formats. That is, a client-user can learn the patterns for strongly-typed models that `System.ClientModel`-based clients provide, and use them together without having to reason about whether a cloud service represents resources using, for example, JSON or XML formats. + +**Protocol methods** are service methods that provide very little convenience over the raw HTTP APIs a cloud service exposes. They represent request and response message bodies using types that are very thin layers over raw JSON/binary/other formats. Users of client protocol methods must reference a service's API documentation directly, rather than relying on the client to provide developer conveniences via strongly-typing service schemas. + +## Convenience methods + +```C# Snippet:ResponseTHelloWorld +// create a client +var client = new SecretClient(new Uri("http://example.com"), new DefaultAzureCredential()); + +// call a service method, which returns Response +Response response = await client.GetSecretAsync("SecretName"); + +// Response has two main accessors. +// Value property for accessing the deserialized result of the call +KeyVaultSecret secret = response.Value; + +// .. and GetRawResponse method for accessing all the details of the HTTP response +Response http = response.GetRawResponse(); + +// for example, you can access HTTP status +int status = http.Status; + +// or the headers +foreach (HttpHeader header in http.Headers) +{ + Console.WriteLine($"{header.Name} {header.Value}"); +} +``` + +## Accessing HTTP response well-known headers + +You can access well known response headers via properties of `ResponseHeaders` object: + +```C# Snippet:ResponseHeaders +// call a service method, which returns Response +Response response = await client.GetSecretAsync("SecretName"); + +Response http = response.GetRawResponse(); + +Console.WriteLine("ETag " + http.Headers.ETag); +Console.WriteLine("Content-Length " + http.Headers.ContentLength); +Console.WriteLine("Content-Type " + http.Headers.ContentType); +``` + +## Handling exceptions + +When a service call fails `Azure.RequestFailedException` would get thrown. The exception type provides a Status property with an HTTP status code an an ErrorCode property with a service-specific error code. + +```C# Snippet:RequestFailedException +try +{ + KeyVaultSecret secret = client.GetSecret("NonexistentSecret"); +} +// handle exception with status code 404 +catch (RequestFailedException e) when (e.Status == 404) +{ + // handle not found error + Console.WriteLine("ErrorCode " + e.ErrorCode); +} +``` + +## Protocol methods + +### Pet's Example + +To compare the two approaches, imagine a service that stores information about pets, with a pair of `GetPet` and `SetPet` operations. + +Pets are represented in the message body as a JSON object: + +```json +{ + "name": "snoopy", + "species": "beagle" +} +``` + +An API using model types could be: + +```csharp +// This is an example model class +public class Pet +{ + string Name { get; } + string Species { get; } +} + +Response GetPet(string dogName); +Response SetPet(Pet dog); +``` + +While the protocol methods version would be: + +```csharp +// Request: "id" in the context path, like "/pets/{id}" +// Response: { +// "name": "snoopy", +// "species": "beagle" +// } +Response GetPet(string id, RequestContext context = null) +// Request: { +// "name": "snoopy", +// "species": "beagle" +// } +// Response: { +// "name": "snoopy", +// "species": "beagle" +// } +Response SetPet(RequestContent requestBody, RequestContext context = null); +``` + +**[Note]**: This document is a general quickstart in using SDK Clients that expose '**protocol methods**'. + +## Usage + +The basic structure of calling protocol methods remains the same as that of convenience methods: + +1. [Initialize Your Client](#1-initialize-your-client "Initialize Your Client") + +2. [Create and Send a request](#2-create-and-send-a-request "Create and Send a Request") + +3. [Handle the Response](#3-handle-the-response "Handle the Response") + +### 1. Initialize Your Client + +The first step in interacting with a service via protocol methods is to create a client instance. + +```csharp +using System; +using Azure.Pets; +using Azure.Core; +using Azure.Identity; + +const string endpoint = "http://localhost:3000"; +var credential = new AzureKeyCredential(/*SERVICE-API-KEY*/); +var client = new PetStoreClient(new Uri(endpoint), credential, new PetStoreClientOptions()); +``` + +### 2. Create and Send a Request + +Protocol methods need a JSON object of the shape required by the schema of the service. + +See the specific service documentation for details, but as an example: + +```csharp +// anonymous class is serialized by System.Text.Json using runtime reflection +var data = new { + name = "snoopy", + species = "beagle" +}; +/* +{ + "name": "snoopy", + "species": "beagle" +} +*/ +client.SetPet(RequestContent.Create(data)); +``` + +### 3. Handle the Response + +Protocol methods all return a `Response` object that contains information returned from the service request. + +The most important field on Response contains the REST content returned from the service: + +```C# Snippet:GetPetAsync +Response response = await client.GetPetAsync("snoopy", new RequestContext()); + +var doc = JsonDocument.Parse(response.Content.ToMemory()); +var name = doc.RootElement.GetProperty("name").GetString(); +``` + +JSON properties can also be accessed using a dynamic layer. + +```C# Snippet:AzureCoreGetDynamicJsonProperty +Response response = client.GetWidget(); +dynamic widget = response.Content.ToDynamicFromJson(); +string name = widget.name; +``` + +## Configuration And Customization + +**Protocol methods** share the same configuration and customization as **convenience methods**. For details, see the [ReadMe](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/README.md). You can find more samples [here](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/samples/README.md). diff --git a/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs b/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs index 3154bbb88e08..72836e6ad374 100644 --- a/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs +++ b/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs @@ -1,151 +1,110 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System; -using System.Net; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Diagnostics; using System.Net.Http; using System.Threading.Tasks; -using Azure.Core.Pipeline; -using Azure.Identity; -using Azure.Security.KeyVault.Secrets; +using Maps; using NUnit.Framework; -namespace Azure.Core.Samples +namespace System.ClientModel.Tests.Samples; + +public class ConfigurationSamples { - public class ConfigurationSamples + [Test] + public void ClientModelConfigurationReadme() { - [Test] - public void ConfigurationHelloWorld() - { - #region Snippet:ConfigurationHelloWorld - - SecretClientOptions options = new SecretClientOptions() - { - Retry = - { - Delay = TimeSpan.FromSeconds(2), - MaxRetries = 10, - Mode = RetryMode.Fixed - }, - Diagnostics = - { - IsLoggingContentEnabled = true, - ApplicationId = "myApplicationId" - } - }; - - SecretClient client = new SecretClient(new Uri("http://example.com"), new DefaultAzureCredential(), options); - #endregion - } + #region Snippet:ClientModelConfigurationReadme - [Test] - public void RetryOptions() + MapsClientOptions options = new() { - #region Snippet:RetryOptions - - SecretClientOptions options = new SecretClientOptions() - { - Retry = - { - Delay = TimeSpan.FromSeconds(2), - MaxRetries = 10, - Mode = RetryMode.Fixed - } - }; - - #endregion - } + NetworkTimeout = TimeSpan.FromSeconds(120), + }; + + string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; + ApiKeyCredential credential = new(key); + MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential, options); + + #endregion + } - [Test] - public void SettingHttpClient() + [Test] + public void ConfigurationCustomizeRetries() + { + #region Snippet:ConfigurationCustomizeRetries + + MapsClientOptions options = new() { - #region Snippet:SettingHttpClient + RetryPolicy = new ClientRetryPolicy(maxRetries: 5), + }; - using HttpClient client = new HttpClient(); + string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; + ApiKeyCredential credential = new(key); + MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential, options); + + #endregion + } - SecretClientOptions options = new SecretClientOptions - { - Transport = new HttpClientTransport(client) - }; + [Test] + public void ConfigurationAddPolicies() + { + #region Snippet:ConfigurationAddPerCallPolicy + MapsClientOptions options = new(); + options.AddPolicy(new StopwatchPolicy(), PipelinePosition.PerCall); + #endregion - #endregion - } + #region Snippet:ConfigurationAddPerTryPolicy + options.AddPolicy(new StopwatchPolicy(), PipelinePosition.PerTry); + #endregion + + #region Snippet:ConfigurationAddBeforeTransportPolicy + options.AddPolicy(new StopwatchPolicy(), PipelinePosition.BeforeTransport); + #endregion + } - [Test] - public void HttpClientProxyConfiguration() + #region Snippet:ConfigurationCustomPolicy + public class StopwatchPolicy : PipelinePolicy + { + public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) { - #region Snippet:HttpClientProxyConfiguration + Stopwatch stopwatch = new(); + stopwatch.Start(); - using HttpClientHandler handler = new HttpClientHandler() - { - Proxy = new WebProxy(new Uri("http://example.com")) - }; + await ProcessNextAsync(message, pipeline, currentIndex); - SecretClientOptions options = new SecretClientOptions - { - Transport = new HttpClientTransport(handler) - }; + stopwatch.Stop(); - #endregion + Console.WriteLine($"Request to {message.Request.Uri} took {stopwatch.Elapsed}"); } - [Test] - public void SetPollyRetryPolicy() + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) { - #region Snippet:SetPollyRetryPolicy - SecretClientOptions options = new SecretClientOptions() - { - RetryPolicy = new PollyPolicy() - }; - #endregion - } + Stopwatch stopwatch = new(); + stopwatch.Start(); - [Test] - public void SetGlobalTimeoutRetryPolicy() - { - #region Snippet:SetGlobalTimeoutRetryPolicy - - var delay = DelayStrategy.CreateFixedDelayStrategy(TimeSpan.FromSeconds(2)); - SecretClientOptions options = new SecretClientOptions() - { - RetryPolicy = new GlobalTimeoutRetryPolicy(maxRetries: 4, delayStrategy: delay, timeout: TimeSpan.FromSeconds(30)) - }; - #endregion - } + ProcessNext(message, pipeline, currentIndex); - [Test] - public void CustomizedDelayStrategy() - { - #region Snippet:CustomizedDelay - SecretClientOptions options = new SecretClientOptions() - { - RetryPolicy = new RetryPolicy(delayStrategy: new SequentialDelayStrategy()) - }; - #endregion + stopwatch.Stop(); + + Console.WriteLine($"Request to {message.Request.Uri} took {stopwatch.Elapsed}"); } + } + #endregion + + [Test] + public void ConfigurationCustomHttpClient() + { + #region Snippet:ConfigurationCustomHttpClient + + using HttpClient client = new(); - #region Snippet:SequentialDelayStrategy - public class SequentialDelayStrategy : DelayStrategy + MapsClientOptions options = new() { - private static readonly TimeSpan[] PollingSequence = new TimeSpan[] - { - TimeSpan.FromSeconds(1), - TimeSpan.FromSeconds(1), - TimeSpan.FromSeconds(1), - TimeSpan.FromSeconds(2), - TimeSpan.FromSeconds(4), - TimeSpan.FromSeconds(8), - TimeSpan.FromSeconds(16), - TimeSpan.FromSeconds(32) - }; - private static readonly TimeSpan MaxDelay = PollingSequence[PollingSequence.Length - 1]; - - protected override TimeSpan GetNextDelayCore(Response response, int retryNumber) - { - int index = retryNumber - 1; - return index >= PollingSequence.Length ? MaxDelay : PollingSequence[index]; - } - } + Transport = new HttpClientPipelineTransport(client) + }; + #endregion } } diff --git a/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs b/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs deleted file mode 100644 index 7a7fb80fd5c6..000000000000 --- a/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Threading.Tasks; -using Azure.Core.Pipeline; -using Azure.Security.KeyVault.Secrets; -using NUnit.Framework; - -namespace Azure.Core.Samples -{ - public class PipelineSamples - { - [Test] - public void AddPolicies() - { - #region Snippet:AddPerCallPolicy - SecretClientOptions options = new SecretClientOptions(); - options.AddPolicy(new CustomRequestPolicy(), HttpPipelinePosition.PerCall); - #endregion - - #region Snippet:AddPerRetryPolicy - options.AddPolicy(new StopwatchPolicy(), HttpPipelinePosition.PerRetry); - #endregion - } - - #region Snippet:StopwatchPolicy - public class StopwatchPolicy : HttpPipelinePolicy - { - public override async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline) - { - var stopwatch = new Stopwatch(); - stopwatch.Start(); - - await ProcessNextAsync(message, pipeline); - - stopwatch.Stop(); - - Console.WriteLine($"Request to {message.Request.Uri} took {stopwatch.Elapsed}"); - } - - public override void Process(HttpMessage message, ReadOnlyMemory pipeline) - { - var stopwatch = new Stopwatch(); - stopwatch.Start(); - - ProcessNext(message, pipeline); - - stopwatch.Stop(); - - Console.WriteLine($"Request to {message.Request.Uri} took {stopwatch.Elapsed}"); - } - } - #endregion - - #region Snippet:SyncPolicy - public class CustomRequestPolicy : HttpPipelineSynchronousPolicy - { - public override void OnSendingRequest(HttpMessage message) - { - message.Request.Uri.AppendQuery("additional-query-parameter", "42"); - message.Request.Headers.Add("Custom-Header", "Value"); - } - } - #endregion - - private class RequestFailedDetailsParserSample - { - public SampleClientOptions options; - private readonly HttpPipeline _pipeline; - - public RequestFailedDetailsParserSample() - { - options = new(); - #region Snippet:RequestFailedDetailsParser - var pipelineOptions = new HttpPipelineOptions(options) - { - RequestFailedDetailsParser = new FooClientRequestFailedDetailsParser() - }; - - _pipeline = HttpPipelineBuilder.Build(pipelineOptions); - #endregion - if (_pipeline == null) - { throw new Exception(); }; - } - } - - private class SampleClientOptions : ClientOptions { } - private class FooClientRequestFailedDetailsParser : RequestFailedDetailsParser - { - public override bool TryParse(Response response, out ResponseError error, out IDictionary data) - { - throw new NotImplementedException(); - } - } - } -} diff --git a/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/CountryRegion.cs b/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/CountryRegion.cs new file mode 100644 index 000000000000..a5ae4b58b821 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/CountryRegion.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ClientModel.Primitives; +using System.Text.Json; + +namespace Maps; + +public class CountryRegion : IJsonModel +{ + internal CountryRegion(string isoCode) + { + IsoCode = isoCode; + } + + public string IsoCode { get; } + + internal static CountryRegion FromJson(JsonElement element) + { + if (element.ValueKind == JsonValueKind.Null) + { + throw new JsonException($"Invalid JSON provided to deserialize type '{nameof(CountryRegion)}'"); + } + + string? isoCode = default; + + foreach (var property in element.EnumerateObject()) + { + if (property.NameEquals("isoCode"u8)) + { + isoCode = property.Value.GetString(); + continue; + } + } + + if (isoCode is null) + { + throw new JsonException($"Invalid JSON provided to deserialize type '{nameof(CountryRegion)}': Missing 'isoCode' property"); + } + + return new CountryRegion(isoCode); + } + + public string GetFormatFromOptions(ModelReaderWriterOptions options) + => "J"; + + public CountryRegion Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) + { + using var document = JsonDocument.ParseValue(ref reader); + return FromJson(document.RootElement); + } + + public CountryRegion Create(BinaryData data, ModelReaderWriterOptions options) + { + using var document = JsonDocument.Parse(data.ToString()); + return FromJson(document.RootElement); + } + + public void Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) + { + throw new NotSupportedException("This model is used for output only"); + } + + public BinaryData Write(ModelReaderWriterOptions options) + { + throw new NotSupportedException("This model is used for output only"); + } +} diff --git a/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/IPAddressCountryPair.cs b/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/IPAddressCountryPair.cs new file mode 100644 index 000000000000..395730bd2d7d --- /dev/null +++ b/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/IPAddressCountryPair.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ClientModel.Primitives; +using System.Net; +using System.Text.Json; + +namespace Maps; + +public class IPAddressCountryPair : IJsonModel +{ + internal IPAddressCountryPair(CountryRegion countryRegion, IPAddress ipAddress) + { + CountryRegion = countryRegion; + IpAddress = ipAddress; + } + + public CountryRegion CountryRegion { get; } + + public IPAddress IpAddress { get; } + + internal static IPAddressCountryPair FromJson(JsonElement element) + { + if (element.ValueKind == JsonValueKind.Null) + { + throw new JsonException($"Invalid JSON provided to deserialize type '{nameof(IPAddressCountryPair)}'"); + } + + CountryRegion? countryRegion = default; + IPAddress? ipAddress = default; + + foreach (var property in element.EnumerateObject()) + { + if (property.NameEquals("countryRegion"u8)) + { + if (property.Value.ValueKind == JsonValueKind.Null) + { + continue; + } + + countryRegion = CountryRegion.FromJson(property.Value); + continue; + } + + if (property.NameEquals("ipAddress"u8)) + { + if (property.Value.ValueKind == JsonValueKind.Null) + { + continue; + } + + ipAddress = IPAddress.Parse(property.Value.GetString()!); + continue; + } + } + + if (countryRegion is null) + { + throw new JsonException($"Invalid JSON provided to deserialize type '{nameof(IPAddressCountryPair)}': Missing 'countryRegion' property"); + } + + if (ipAddress is null) + { + throw new JsonException($"Invalid JSON provided to deserialize type '{nameof(IPAddressCountryPair)}': Missing 'ipAddress' property"); + } + + return new IPAddressCountryPair(countryRegion, ipAddress); + } + + internal static IPAddressCountryPair FromResponse(PipelineResponse response) + { + using var document = JsonDocument.Parse(response.Content); + return FromJson(document.RootElement); + } + + public string GetFormatFromOptions(ModelReaderWriterOptions options) + => "J"; + + public IPAddressCountryPair Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) + { + using var document = JsonDocument.ParseValue(ref reader); + return FromJson(document.RootElement); + } + + public IPAddressCountryPair Create(BinaryData data, ModelReaderWriterOptions options) + { + using var document = JsonDocument.Parse(data.ToString()); + return FromJson(document.RootElement); + } + + public void Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) + { + throw new NotSupportedException("This model is used for output only"); + } + + public BinaryData Write(ModelReaderWriterOptions options) + { + throw new NotSupportedException("This model is used for output only"); + } +} diff --git a/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClient.cs b/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClient.cs new file mode 100644 index 000000000000..fd1f5085304a --- /dev/null +++ b/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClient.cs @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace Maps; + +public class MapsClient +{ + private readonly Uri _endpoint; + private readonly ApiKeyCredential _credential; + private readonly ClientPipeline _pipeline; + private readonly string _apiVersion; + + public MapsClient(Uri endpoint, ApiKeyCredential credential, MapsClientOptions? options = default) + { + if (endpoint is null) throw new ArgumentNullException(nameof(endpoint)); + if (credential is null) throw new ArgumentNullException(nameof(credential)); + + options ??= new MapsClientOptions(); + + _endpoint = endpoint; + _credential = credential; + _apiVersion = options.Version; + + var authenticationPolicy = ApiKeyAuthenticationPolicy.CreateHeaderApiKeyPolicy(credential, "subscription-key"); + _pipeline = ClientPipeline.Create(options, + perCallPolicies: ReadOnlySpan.Empty, + perTryPolicies: new PipelinePolicy[] { authenticationPolicy }, + beforeTransportPolicies: ReadOnlySpan.Empty); + } + + public virtual async Task> GetCountryCodeAsync(IPAddress ipAddress) + { + if (ipAddress is null) + throw new ArgumentNullException(nameof(ipAddress)); + + ClientResult result = await GetCountryCodeAsync(ipAddress.ToString()).ConfigureAwait(false); + + PipelineResponse response = result.GetRawResponse(); + IPAddressCountryPair value = IPAddressCountryPair.FromResponse(response); + + return ClientResult.FromValue(value, response); + } + + public virtual async Task GetCountryCodeAsync(string ipAddress, RequestOptions? options = null) + { + if (ipAddress is null) + throw new ArgumentNullException(nameof(ipAddress)); + + options ??= new RequestOptions(); + + using PipelineMessage message = CreateGetLocationRequest(ipAddress, options); + + _pipeline.Send(message); + + PipelineResponse response = message.Response!; + + if (response.IsError && options.ErrorOptions == ClientErrorBehaviors.Default) + { + throw await ClientResultException.CreateAsync(response).ConfigureAwait(false); + } + + return ClientResult.FromResponse(response); + } + + public virtual ClientResult GetCountryCode(IPAddress ipAddress) + { + if (ipAddress is null) throw new ArgumentNullException(nameof(ipAddress)); + + ClientResult result = GetCountryCode(ipAddress.ToString()); + + PipelineResponse response = result.GetRawResponse(); + IPAddressCountryPair value = IPAddressCountryPair.FromResponse(response); + + return ClientResult.FromValue(value, response); + } + + public virtual ClientResult GetCountryCode(string ipAddress, RequestOptions? options = null) + { + if (ipAddress is null) throw new ArgumentNullException(nameof(ipAddress)); + + options ??= new RequestOptions(); + + using PipelineMessage message = CreateGetLocationRequest(ipAddress, options); + + _pipeline.Send(message); + + PipelineResponse response = message.Response!; + + if (response.IsError && options.ErrorOptions == ClientErrorBehaviors.Default) + { + throw new ClientResultException(response); + } + + return ClientResult.FromResponse(response); + } + + private PipelineMessage CreateGetLocationRequest(string ipAddress, RequestOptions options) + { + PipelineMessage message = _pipeline.CreateMessage(); + message.ResponseClassifier = PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); + + PipelineRequest request = message.Request; + request.Method = "GET"; + + UriBuilder uriBuilder = new(_endpoint.ToString()); + + StringBuilder path = new(); + path.Append("geolocation/ip"); + path.Append("/json"); + uriBuilder.Path += path.ToString(); + + StringBuilder query = new(); + query.Append("api-version="); + query.Append(Uri.EscapeDataString(_apiVersion)); + query.Append("&ip="); + query.Append(Uri.EscapeDataString(ipAddress)); + uriBuilder.Query = query.ToString(); + + request.Uri = uriBuilder.Uri; + + request.Headers.Add("Accept", "application/json"); + + // Note: due to addition of SetHeader method on RequestOptions, we now + // need to apply options at the end of the CreateRequest routine. + message.Apply(options); + + return message; + } +} diff --git a/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClientOptions.cs b/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClientOptions.cs new file mode 100644 index 000000000000..ba4a287ec2bd --- /dev/null +++ b/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClientOptions.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ClientModel.Primitives; + +namespace Maps; + +public class MapsClientOptions : ClientPipelineOptions +{ + private const ServiceVersion LatestVersion = ServiceVersion.V1; + + public enum ServiceVersion + { + V1 = 1 + } + + internal string Version { get; } + + public MapsClientOptions(ServiceVersion version = LatestVersion) + { + Version = version switch + { + ServiceVersion.V1 => "1.0", + _ => throw new NotSupportedException() + }; + } +} From e0fee6f1507bc48df72ed109385465fc9e259159 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 4 Mar 2024 10:59:25 -0800 Subject: [PATCH 04/40] updates before generating snippets --- .../tests/Samples/ModelReaderWriterSamples.cs | 115 +++++++------ .../tests/Samples/ResponseSamples.cs | 157 ------------------ .../tests/Samples/ServiceMethodSamples.cs | 104 ++++++++++++ .../TestClients/MapsClient/CountryRegion.cs | 6 +- .../MapsClient/IPAddressCountryPair.cs | 8 +- 5 files changed, 168 insertions(+), 222 deletions(-) delete mode 100644 sdk/core/System.ClientModel/tests/Samples/ResponseSamples.cs create mode 100644 sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs diff --git a/sdk/core/System.ClientModel/tests/Samples/ModelReaderWriterSamples.cs b/sdk/core/System.ClientModel/tests/Samples/ModelReaderWriterSamples.cs index b094c68db351..774bc2dc6c2c 100644 --- a/sdk/core/System.ClientModel/tests/Samples/ModelReaderWriterSamples.cs +++ b/sdk/core/System.ClientModel/tests/Samples/ModelReaderWriterSamples.cs @@ -4,84 +4,83 @@ using System.ClientModel.Primitives; using System.Text.Json; -namespace System.ClientModel.Tests.Samples +namespace System.ClientModel.Tests.Samples; + +internal class ModelReaderWriterSamples { - internal class ReadmeModelReaderWriter + public void Write_Simple() { - public void Write_Simple() - { - #region Snippet:Readme_Write_Simple - InputModel model = new InputModel(); - BinaryData data = ModelReaderWriter.Write(model); - #endregion - } + #region Snippet:Readme_Write_Simple + InputModel model = new InputModel(); + BinaryData data = ModelReaderWriter.Write(model); + #endregion + } - public void Read_Simple() - { - #region Snippet:Readme_Read_Simple - string json = @"{ + public void Read_Simple() + { + #region Snippet:Readme_Read_Simple + string json = @"{ ""x"": 1, ""y"": 2, ""z"": 3 }"; - OutputModel? model = ModelReaderWriter.Read(BinaryData.FromString(json)); - #endregion - } + OutputModel? model = ModelReaderWriter.Read(BinaryData.FromString(json)); + #endregion + } - private class OutputModel : IJsonModel + private class OutputModel : IJsonModel + { + OutputModel IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) { - OutputModel IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); + } - OutputModel IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) - { - throw new NotImplementedException(); - } + OutputModel IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } - string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) - { - throw new NotImplementedException(); - } + string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } - void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) - { - throw new NotImplementedException(); - } + void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } - BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) - { - throw new NotImplementedException(); - } + BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) + { + throw new NotImplementedException(); } + } - private class InputModel : IJsonModel + private class InputModel : IJsonModel + { + void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) { - void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); + } - InputModel IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) - { - throw new NotImplementedException(); - } + InputModel IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } - BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) - { - throw new NotImplementedException(); - } + BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } - InputModel IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) - { - throw new NotImplementedException(); - } + InputModel IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } - string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) - { - throw new NotImplementedException(); - } + string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) + { + throw new NotImplementedException(); } } } diff --git a/sdk/core/System.ClientModel/tests/Samples/ResponseSamples.cs b/sdk/core/System.ClientModel/tests/Samples/ResponseSamples.cs deleted file mode 100644 index b831e4eaf6e1..000000000000 --- a/sdk/core/System.ClientModel/tests/Samples/ResponseSamples.cs +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Azure.Identity; -using Azure.Security.KeyVault.Secrets; -using NUnit.Framework; - -namespace Azure.Core.Samples -{ - public class ResponseSamples - { - [Test] - [Ignore("Only verifying that the sample builds")] - public async Task ResponseTHelloWorld() - { - #region Snippet:ResponseTHelloWorld - // create a client - var client = new SecretClient(new Uri("http://example.com"), new DefaultAzureCredential()); - - // call a service method, which returns Response - Response response = await client.GetSecretAsync("SecretName"); - - // Response has two main accessors. - // Value property for accessing the deserialized result of the call - KeyVaultSecret secret = response.Value; - - // .. and GetRawResponse method for accessing all the details of the HTTP response - Response http = response.GetRawResponse(); - - // for example, you can access HTTP status - int status = http.Status; - - // or the headers - foreach (HttpHeader header in http.Headers) - { - Console.WriteLine($"{header.Name} {header.Value}"); - } - #endregion - } - - [Test] - [Ignore("Only verifying that the sample builds")] - public async Task ResponseTContent() - { - // create a client - var client = new SecretClient(new Uri("http://example.com"), new DefaultAzureCredential()); - - #region Snippet:ResponseTContent - // call a service method, which returns Response - Response response = await client.GetSecretAsync("SecretName"); - - Response http = response.GetRawResponse(); - - Stream contentStream = http.ContentStream; - - // Rewind the stream - contentStream.Position = 0; - - using (StreamReader reader = new StreamReader(contentStream)) - { - Console.WriteLine(reader.ReadToEnd()); - } - - #endregion - } - - [Test] - [Ignore("Only verifying that the sample builds")] - public async Task ResponseHeaders() - { - // create a client - var client = new SecretClient(new Uri("http://example.com"), new DefaultAzureCredential()); - - #region Snippet:ResponseHeaders - // call a service method, which returns Response - Response response = await client.GetSecretAsync("SecretName"); - - Response http = response.GetRawResponse(); - - Console.WriteLine("ETag " + http.Headers.ETag); - Console.WriteLine("Content-Length " + http.Headers.ContentLength); - Console.WriteLine("Content-Type " + http.Headers.ContentType); - #endregion - } - - [Test] - [Ignore("Only verifying that the sample builds")] - public async Task AsyncPageable() - { - // create a client - var client = new SecretClient(new Uri("http://example.com"), new DefaultAzureCredential()); - - #region Snippet:AsyncPageable - // call a service method, which returns AsyncPageable - AsyncPageable allSecretProperties = client.GetPropertiesOfSecretsAsync(); - - await foreach (SecretProperties secretProperties in allSecretProperties) - { - Console.WriteLine(secretProperties.Name); - } - #endregion - } - - [Test] - [Ignore("Only verifying that the sample builds")] - public async Task AsyncPageableLoop() - { - // create a client - var client = new SecretClient(new Uri("http://example.com"), new DefaultAzureCredential()); - - #region Snippet:AsyncPageableLoop - // call a service method, which returns AsyncPageable - AsyncPageable allSecretProperties = client.GetPropertiesOfSecretsAsync(); - - IAsyncEnumerator enumerator = allSecretProperties.GetAsyncEnumerator(); - try - { - while (await enumerator.MoveNextAsync()) - { - SecretProperties secretProperties = enumerator.Current; - Console.WriteLine(secretProperties.Name); - } - } - finally - { - await enumerator.DisposeAsync(); - } - #endregion - } - - [Test] - [Ignore("Only verifying that the sample builds")] - public void RequestFailedException() - { - // create a client - var client = new SecretClient(new Uri("http://example.com"), new DefaultAzureCredential()); - - #region Snippet:RequestFailedException - try - { - KeyVaultSecret secret = client.GetSecret("NonexistentSecret"); - } - // handle exception with status code 404 - catch (RequestFailedException e) when (e.Status == 404) - { - // handle not found error - Console.WriteLine("ErrorCode " + e.ErrorCode); - } - #endregion - } - } -} diff --git a/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs b/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs new file mode 100644 index 000000000000..e008ab1478e6 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Maps; +using NUnit.Framework; + +namespace System.ClientModel.Tests.Samples; + +public class ServiceMethodSamples +{ + [Test] + public async Task ClientResultTReadme() + { + #region Snippet:ClientResultTReadme + // create a client + string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; + ApiKeyCredential credential = new(key); + MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential); + + // call a service method, which returns ClientResult + IPAddress ipAddress = IPAddress.Parse("2001:4898:80e8:b::189"); + ClientResult result = await client.GetCountryCodeAsync(ipAddress); + + // ClientResult has two members: + // + // (1) A Value property to access the strongly-typed output + IPAddressCountryPair value = result.Value; + Console.WriteLine($"Country is {value.CountryRegion.IsoCode}."); + + // (2) A GetRawResponse method for accessing the details of the HTTP response + PipelineResponse response = result.GetRawResponse(); + + Console.WriteLine($"Response status code: '{response.Status}'."); + Console.WriteLine("Response headers:"); + foreach (KeyValuePair header in response.Headers) + { + Console.WriteLine($"Name: '{header.Key}', Value: '{header.Value}'."); + } + #endregion + } + + [Test] + public async Task ClientResultExceptionReadme() + { + // create a client + string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; + ApiKeyCredential credential = new(key); + MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential); + + #region Snippet:ClientResultExceptionReadme + try + { + IPAddress ipAddress = IPAddress.Parse("2001:4898:80e8:b::189"); + ClientResult 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}'"); + } + #endregion + } + + [Test] + public async Task RequestOptionsReadme() + { + string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; + ApiKeyCredential credential = new ApiKeyCredential(key); + + MapsClient client = new MapsClient(new Uri("https://atlas.microsoft.com"), credential); + + // Dummy CancellationToken + CancellationToken cancellationToken = CancellationToken.None; + + try + { + IPAddress ipAddress = IPAddress.Parse("2001:4898:80e8:b::189"); + + #region 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); + #endregion + } + catch (ClientResultException e) + { + Assert.Fail($"Error: Response status code: '{e.Status}'"); + } + } +} diff --git a/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/CountryRegion.cs b/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/CountryRegion.cs index a5ae4b58b821..7f98d74b6376 100644 --- a/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/CountryRegion.cs +++ b/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/CountryRegion.cs @@ -25,7 +25,7 @@ internal static CountryRegion FromJson(JsonElement element) string? isoCode = default; - foreach (var property in element.EnumerateObject()) + foreach (JsonProperty property in element.EnumerateObject()) { if (property.NameEquals("isoCode"u8)) { @@ -47,13 +47,13 @@ public string GetFormatFromOptions(ModelReaderWriterOptions options) public CountryRegion Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) { - using var document = JsonDocument.ParseValue(ref reader); + using JsonDocument document = JsonDocument.ParseValue(ref reader); return FromJson(document.RootElement); } public CountryRegion Create(BinaryData data, ModelReaderWriterOptions options) { - using var document = JsonDocument.Parse(data.ToString()); + using JsonDocument document = JsonDocument.Parse(data.ToString()); return FromJson(document.RootElement); } diff --git a/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/IPAddressCountryPair.cs b/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/IPAddressCountryPair.cs index 395730bd2d7d..6f0e87f1c3cb 100644 --- a/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/IPAddressCountryPair.cs +++ b/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/IPAddressCountryPair.cs @@ -30,7 +30,7 @@ internal static IPAddressCountryPair FromJson(JsonElement element) CountryRegion? countryRegion = default; IPAddress? ipAddress = default; - foreach (var property in element.EnumerateObject()) + foreach (JsonProperty property in element.EnumerateObject()) { if (property.NameEquals("countryRegion"u8)) { @@ -70,7 +70,7 @@ internal static IPAddressCountryPair FromJson(JsonElement element) internal static IPAddressCountryPair FromResponse(PipelineResponse response) { - using var document = JsonDocument.Parse(response.Content); + using JsonDocument document = JsonDocument.Parse(response.Content); return FromJson(document.RootElement); } @@ -79,13 +79,13 @@ public string GetFormatFromOptions(ModelReaderWriterOptions options) public IPAddressCountryPair Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) { - using var document = JsonDocument.ParseValue(ref reader); + using JsonDocument document = JsonDocument.ParseValue(ref reader); return FromJson(document.RootElement); } public IPAddressCountryPair Create(BinaryData data, ModelReaderWriterOptions options) { - using var document = JsonDocument.Parse(data.ToString()); + using JsonDocument document = JsonDocument.Parse(data.ToString()); return FromJson(document.RootElement); } From 9dbf76ba8d7235e5e2f5a299a13120e358d7581b Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 4 Mar 2024 11:03:56 -0800 Subject: [PATCH 05/40] update snippets --- sdk/core/System.ClientModel/README.md | 62 +++++++++++++++++-- .../samples/Configuration.md | 36 ++++++----- 2 files changed, 80 insertions(+), 18 deletions(-) diff --git a/sdk/core/System.ClientModel/README.md b/sdk/core/System.ClientModel/README.md index 53e953e5393c..cf6241e916a9 100644 --- a/sdk/core/System.ClientModel/README.md +++ b/sdk/core/System.ClientModel/README.md @@ -47,6 +47,14 @@ Below, you will find sections explaining these shared concepts in more detail. `ClientPipelineOptions` allows overriding default client values for things like the network timeout used when sending a request or the maximum number of retries to send when a request fails. ```C# Snippet:ClientModelConfigurationReadme +MapsClientOptions options = new() +{ + NetworkTimeout = TimeSpan.FromSeconds(120), +}; + +string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; +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) @@ -62,6 +70,30 @@ _Service clients_ have methods that are used to call cloud services to invoke se **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` value that allows the client pipeline and the request to be configured for the duration of the call. ```C# Snippet:ClientResultTReadme +// create a client +string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; +ApiKeyCredential credential = new(key); +MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential); + +// call a service method, which returns ClientResult +IPAddress ipAddress = IPAddress.Parse("2001:4898:80e8:b::189"); +ClientResult result = await client.GetCountryCodeAsync(ipAddress); + +// ClientResult has two members: +// +// (1) A Value property to access the strongly-typed output +IPAddressCountryPair value = result.Value; +Console.WriteLine($"Country is {value.CountryRegion.IsoCode}."); + +// (2) A GetRawResponse method for accessing the details of the HTTP response +PipelineResponse response = result.GetRawResponse(); + +Console.WriteLine($"Response status code: '{response.Status}'."); +Console.WriteLine("Response headers:"); +foreach (KeyValuePair 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) @@ -77,6 +109,17 @@ For more information on client service methods, see [Client service method sampl When a service call fails, `System.ClientModel`-based clients throw a `ClientResultException`. The exception exposes the HTTP status code and the details of the service response if available. ```C# Snippet:ClientResultExceptionReadme +try +{ + IPAddress ipAddress = IPAddress.Parse("2001:4898:80e8:b::189"); + ClientResult 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}'"); +} ``` ### Customizing HTTP requests @@ -84,6 +127,17 @@ When a service call fails, `System.ClientModel`-based clients throw a `ClientRes `System.ClientModel`-based clients expose low-level _protocol methods_ that allow callers to customize the details of HTTP requests. Protocol methods take an optional `RequestOptions` value that allows callers to add a header to the request, or to add a policy to the client pipeline that can modify the request in any way before sending it to the service. `RequestOptions` also allows passing a `CancellationToken` to the method. ```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); ``` For more information on customizing request, see [Protocol method samples](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#protocol-methods) @@ -103,10 +157,10 @@ Example reading a model from json ```C# Snippet:Readme_Read_Simple string json = @"{ - ""x"": 1, - ""y"": 2, - ""z"": 3 -}"; + ""x"": 1, + ""y"": 2, + ""z"": 3 + }"; OutputModel? model = ModelReaderWriter.Read(BinaryData.FromString(json)); ``` diff --git a/sdk/core/System.ClientModel/samples/Configuration.md b/sdk/core/System.ClientModel/samples/Configuration.md index 05628f22827a..a8904d4073d5 100644 --- a/sdk/core/System.ClientModel/samples/Configuration.md +++ b/sdk/core/System.ClientModel/samples/Configuration.md @@ -7,6 +7,14 @@ To modify the retry policy, create a new instance of `ClientRetryPolicy` and set By default, clients are setup to retry 3 times using an exponential retry strategy with an initial delay of 0.8 sec, and a max delay of 1 minute. ```C# Snippet:ConfigurationCustomizeRetries +MapsClientOptions options = new() +{ + RetryPolicy = new ClientRetryPolicy(maxRetries: 5), +}; + +string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; +ApiKeyCredential credential = new(key); +MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential, options); ``` ## Add a custom policy to the pipeline @@ -16,14 +24,14 @@ Azure SDKs provides a way to add policies to the pipeline at three positions: - per-call policies are run once per request ```C# Snippet:ConfigurationAddPerCallPolicy -SecretClientOptions options = new SecretClientOptions(); -options.AddPolicy(new CustomRequestPolicy(), HttpPipelinePosition.PerCall); +MapsClientOptions options = new(); +options.AddPolicy(new StopwatchPolicy(), PipelinePosition.PerCall); ``` - per-try policies are run each time the request is tried ```C# Snippet:ConfigurationAddPerTryPolicy -options.AddPolicy(new StopwatchPolicy(), HttpPipelinePosition.PerRetry); +options.AddPolicy(new StopwatchPolicy(), PipelinePosition.PerTry); ``` - before-transport policies are run after other policies and before the request is sent @@ -31,7 +39,7 @@ options.AddPolicy(new StopwatchPolicy(), HttpPipelinePosition.PerRetry); Adding policies at this 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. ```C# Snippet:ConfigurationAddBeforeTransportPolicy -options.AddPolicy(new StopwatchPolicy(), HttpPipelinePosition.BeforeTransport); +options.AddPolicy(new StopwatchPolicy(), PipelinePosition.BeforeTransport); ``` ## Implement a custom policy @@ -39,26 +47,26 @@ options.AddPolicy(new StopwatchPolicy(), HttpPipelinePosition.BeforeTransport); To implement a policy create a class deriving from `HttpPipelinePolicy` and overide `ProcessAsync` and `Process` methods. Request can be accessed via `message.Request`. Response is accessible via `message.Response` but only after `ProcessNextAsync`/`ProcessNext` was called. ```C# Snippet:ConfigurationCustomPolicy -public class StopwatchPolicy : HttpPipelinePolicy +public class StopwatchPolicy : PipelinePolicy { - public override async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline) + public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) { - var stopwatch = new Stopwatch(); + Stopwatch stopwatch = new(); stopwatch.Start(); - await ProcessNextAsync(message, pipeline); + await ProcessNextAsync(message, pipeline, currentIndex); stopwatch.Stop(); Console.WriteLine($"Request to {message.Request.Uri} took {stopwatch.Elapsed}"); } - public override void Process(HttpMessage message, ReadOnlyMemory pipeline) + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) { - var stopwatch = new Stopwatch(); + Stopwatch stopwatch = new(); stopwatch.Start(); - ProcessNext(message, pipeline); + ProcessNext(message, pipeline, currentIndex); stopwatch.Stop(); @@ -72,10 +80,10 @@ public class StopwatchPolicy : HttpPipelinePolicy 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 create the custom `HttpClient` instance to its constructor. ```C# Snippet:ConfigurationCustomHttpClient -using HttpClient client = new HttpClient(); +using HttpClient client = new(); -SecretClientOptions options = new SecretClientOptions +MapsClientOptions options = new() { - Transport = new HttpClientTransport(client) + Transport = new HttpClientPipelineTransport(client) }; ``` From 433c3fc86ea9013eda92dd13048bef7b85655798 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 4 Mar 2024 11:22:34 -0800 Subject: [PATCH 06/40] readme updates --- sdk/core/System.ClientModel/README.md | 31 ++++++++++++--------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/sdk/core/System.ClientModel/README.md b/sdk/core/System.ClientModel/README.md index cf6241e916a9..95a86c5079b3 100644 --- a/sdk/core/System.ClientModel/README.md +++ b/sdk/core/System.ClientModel/README.md @@ -42,9 +42,8 @@ Below, you will find sections explaining these shared concepts in more detail. ### Configuring service clients -`System.ClientModel`-based 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` that can be used to configure the pipeline the client uses to send and receive HTTP requests and responses. - -`ClientPipelineOptions` allows overriding default client values for things like the network timeout used when sending a request or the maximum number of retries to send when a request fails. +`System.ClientModel`-based clients, or **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() @@ -61,7 +60,7 @@ For more information on client configuration, see [Client configuration samples] ### 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 have methods that are used to call cloud services to invoke service operations. These methods on a client are called **service methods**. `System.ClientModel`-based clients expose two types of service methods: _convenience methods_ and _protocol methods_. @@ -69,6 +68,8 @@ _Service clients_ have methods that are used to call cloud services to invoke se **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` value 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. + ```C# Snippet:ClientResultTReadme // create a client string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; @@ -96,17 +97,11 @@ foreach (KeyValuePair header in response.Headers) } ``` -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) - -// TODO: move the below to the detailed sample file - -**Convenience methods** are service methods that take a strongly-typed model representing schematized data needed to communicate with the service as input, and return a strongly-typed model representing the payload from the service response as output. Having strongly-typed models that represent service concepts provides a layer of convenience over working with the raw payload format. This is because these models unify the client user experience when cloud services differ in payload formats. That is, a client-user can learn the patterns for strongly-typed models that `System.ClientModel`-based clients provide, and use them together without having to reason about whether a cloud service represents resources using, for example, JSON or XML formats. - -**Protocol methods** are service methods that provide very little convenience over the raw HTTP APIs a cloud service exposes. They represent request and response message bodies using types that are very thin layers over raw JSON/binary/other formats. Users of client protocol methods must reference a service's API documentation directly, rather than relying on the client to provide developer conveniences via strongly-typing service schemas. +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, `System.ClientModel`-based clients throw a `ClientResultException`. The exception exposes the HTTP status code and the details of the service response if available. +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. ```C# Snippet:ClientResultExceptionReadme try @@ -124,7 +119,7 @@ catch (ClientResultException e) when (e.Status == 404) ### Customizing HTTP requests -`System.ClientModel`-based clients expose low-level _protocol methods_ that allow callers to customize the details of HTTP requests. Protocol methods take an optional `RequestOptions` value that allows callers to add a header to the request, or to add a policy to the client pipeline that can modify the request in any way before sending it to the service. `RequestOptions` also allows passing a `CancellationToken` to the method. +Service clients expose low-level _protocol methods_ that allow callers to customize the details of HTTP requests. Protocol methods take an optional `RequestOptions` value that allows callers to add a header to the request, or to add a policy to the client pipeline that can modify the request in any way before sending it to the service. `RequestOptions` also allows passing a `CancellationToken` to the method. ```C# Snippet:RequestOptionsReadme // Create RequestOptions instance @@ -140,20 +135,20 @@ options.AddHeader("CustomHeader", "CustomHeaderValue"); ClientResult output = await client.GetCountryCodeAsync(ipAddress.ToString(), options); ``` -For more information on customizing request, see [Protocol method samples](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#protocol-methods) +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` or `IJsonModel` which will give library users the ability to read and write your models. +Client library authors can implement the `IPersistableModel` or `IJsonModel` 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 = @"{ @@ -168,6 +163,8 @@ OutputModel? model = ModelReaderWriter.Read(BinaryData.FromString(j You can troubleshoot `System.ClientModel`-based 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 ## Contributing From a7934ed102b3592e1ef79b9a54e3374b3870ee97 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 4 Mar 2024 11:38:08 -0800 Subject: [PATCH 07/40] intermediate backup --- .../samples/ServiceMethods.md | 71 +++++++++---------- 1 file changed, 32 insertions(+), 39 deletions(-) diff --git a/sdk/core/System.ClientModel/samples/ServiceMethods.md b/sdk/core/System.ClientModel/samples/ServiceMethods.md index 92edb02e1331..69cc71bb0ed8 100644 --- a/sdk/core/System.ClientModel/samples/ServiceMethods.md +++ b/sdk/core/System.ClientModel/samples/ServiceMethods.md @@ -2,17 +2,21 @@ ## Introduction -_Service clients_ have methods that are used to call cloud services to invoke service operations. These methods on a client are called _service methods_. +`System.ClientModel`-based clients , or **service clients**, provide an interface to cloud services by translating library calls to HTTP requests. -`System.ClientModel`-based clients expose two types of service methods: _convenience methods_ and _protocol methods_. +In service clients, there are two ways to expose the schematized body in the request or response, known as the **message body**: -**Convenience methods** provide a convenient way to invoke a service operation. They are service methods that take a strongly-typed model representing schematized data sent to the service as input, and return a strongly-typed model representing the payload from the service response as output. Having strongly-typed models that represent service concepts provides a layer of convenience over working with the raw payload format. This is because these models unify the client user experience when cloud services differ in payload formats. That is, a client-user can learn the patterns for strongly-typed models that `System.ClientModel`-based clients provide, and use them together without having to reason about whether a cloud service represents resources using, for example, JSON or XML formats. +- Most service clients expose methods that take strongly-typed models as parameters, C# classes which map to the message body of the REST call. These methods are called **convenience methods**. -**Protocol methods** are service methods that provide very little convenience over the raw HTTP APIs a cloud service exposes. They represent request and response message bodies using types that are very thin layers over raw JSON/binary/other formats. Users of client protocol methods must reference a service's API documentation directly, rather than relying on the client to provide developer conveniences via strongly-typing service schemas. +- However, some clients expose methods that mirror the message body directly. Those methods are called here **protocol methods**, as they provide more direct access to the HTTP API protocol used by the service. ## Convenience methods -```C# Snippet:ResponseTHelloWorld +**Convenience methods** provide a convenient way to invoke a service operation. They are service methods that take a strongly-typed model representing schematized data sent to the service as input, and return a strongly-typed model representing the payload from the service response as output. Having strongly-typed models that represent service concepts provides a layer of convenience over working with the raw payload format. This is because these models unify the client user experience when cloud services differ in payload formats. That is, a client-user can learn the patterns for strongly-typed models that `System.ClientModel`-based clients provide, and use them together without having to reason about whether a cloud service represents resources using, for example, JSON or XML formats. + +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. + +```C# Snippet:ClientResultTReadme // create a client var client = new SecretClient(new Uri("http://example.com"), new DefaultAzureCredential()); @@ -36,45 +40,20 @@ foreach (HttpHeader header in http.Headers) } ``` -## Accessing HTTP response well-known headers - -You can access well known response headers via properties of `ResponseHeaders` object: - -```C# Snippet:ResponseHeaders -// call a service method, which returns Response -Response response = await client.GetSecretAsync("SecretName"); - -Response http = response.GetRawResponse(); - -Console.WriteLine("ETag " + http.Headers.ETag); -Console.WriteLine("Content-Length " + http.Headers.ContentLength); -Console.WriteLine("Content-Type " + http.Headers.ContentType); -``` +## Protocol methods -## Handling exceptions +In contrast to convenience methods, **protocol methods** are service methods that provide very little convenience over the raw HTTP APIs a cloud service exposes. They represent request and response message bodies using types that are very thin layers over raw JSON/binary/other formats. Users of client protocol methods must reference a service's API documentation directly, rather than relying on the client to provide developer conveniences via strongly-typing service schemas. -When a service call fails `Azure.RequestFailedException` would get thrown. The exception type provides a Status property with an HTTP status code an an ErrorCode property with a service-specific error code. +The following sample illustrates how to call a protocol method, including creating the request payload, accessing the details of the HTTP response. -```C# Snippet:RequestFailedException -try -{ - KeyVaultSecret secret = client.GetSecret("NonexistentSecret"); -} -// handle exception with status code 404 -catch (RequestFailedException e) when (e.Status == 404) -{ - // handle not found error - Console.WriteLine("ErrorCode " + e.ErrorCode); -} +```C# Snippet:ServiceMethodsProtocolMethod ``` -## Protocol methods - -### Pet's Example +### Protocol method concepts -To compare the two approaches, imagine a service that stores information about pets, with a pair of `GetPet` and `SetPet` operations. +To compare the convenience and protocol method approaches, let's look more closely at a `System.ClientModel`-based client implementation of a [Geolocation](https://learn.microsoft.com/rest/api/maps/geolocation/get-ip-to-location?view=rest-maps-2023-06-01&tabs=HTTP) service operation. -Pets are represented in the message body as a JSON object: +The [IpAddressToLocation]() operation result is Pets are represented in the message body as a JSON object: ```json { @@ -186,6 +165,20 @@ dynamic widget = response.Content.ToDynamicFromJson(); string name = widget.name; ``` -## Configuration And Customization -**Protocol methods** share the same configuration and customization as **convenience methods**. For details, see the [ReadMe](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/README.md). You can find more samples [here](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/samples/README.md). +## Handling exceptions + +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. + +```C# Snippet:ClientResultExceptionReadme +try +{ + KeyVaultSecret secret = client.GetSecret("NonexistentSecret"); +} +// handle exception with status code 404 +catch (RequestFailedException e) when (e.Status == 404) +{ + // handle not found error + Console.WriteLine("ErrorCode " + e.ErrorCode); +} +``` From f7adcbf00cdd2b34332652ce019e5d96d080807d Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 4 Mar 2024 12:54:04 -0800 Subject: [PATCH 08/40] updates --- .../samples/ServiceMethods.md | 189 ++++++------------ .../tests/Samples/ServiceMethodSamples.cs | 49 +++++ .../TestClients/MapsClient/MapsClient.cs | 19 ++ 3 files changed, 125 insertions(+), 132 deletions(-) diff --git a/sdk/core/System.ClientModel/samples/ServiceMethods.md b/sdk/core/System.ClientModel/samples/ServiceMethods.md index 69cc71bb0ed8..9e8cde03ebac 100644 --- a/sdk/core/System.ClientModel/samples/ServiceMethods.md +++ b/sdk/core/System.ClientModel/samples/ServiceMethods.md @@ -18,25 +18,28 @@ The following sample illustrates how to call a convenience method and access bot ```C# Snippet:ClientResultTReadme // create a client -var client = new SecretClient(new Uri("http://example.com"), new DefaultAzureCredential()); - -// call a service method, which returns Response -Response response = await client.GetSecretAsync("SecretName"); - -// Response has two main accessors. -// Value property for accessing the deserialized result of the call -KeyVaultSecret secret = response.Value; - -// .. and GetRawResponse method for accessing all the details of the HTTP response -Response http = response.GetRawResponse(); - -// for example, you can access HTTP status -int status = http.Status; - -// or the headers -foreach (HttpHeader header in http.Headers) +string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; +ApiKeyCredential credential = new(key); +MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential); + +// call a service method, which returns ClientResult +IPAddress ipAddress = IPAddress.Parse("2001:4898:80e8:b::189"); +ClientResult result = await client.GetCountryCodeAsync(ipAddress); + +// ClientResult has two members: +// +// (1) A Value property to access the strongly-typed output +IPAddressCountryPair value = result.Value; +Console.WriteLine($"Country is {value.CountryRegion.IsoCode}."); + +// (2) A GetRawResponse method for accessing the details of the HTTP response +PipelineResponse response = result.GetRawResponse(); + +Console.WriteLine($"Response status code: '{response.Status}'."); +Console.WriteLine("Response headers:"); +foreach (KeyValuePair header in response.Headers) { - Console.WriteLine($"{header.Name} {header.Value}"); + Console.WriteLine($"Name: '{header.Key}', Value: '{header.Value}'."); } ``` @@ -47,125 +50,46 @@ In contrast to convenience methods, **protocol methods** are service methods tha The following sample illustrates how to call a protocol method, including creating the request payload, accessing the details of the HTTP response. ```C# Snippet:ServiceMethodsProtocolMethod +// Create a BinaryData instance from a JSON string literal +BinaryData input = BinaryData.FromString(""" + { + "countryRegion": { + "isoCode": "US" + }, + } + """); + +// Call the protocol method +ClientResult result = await client.AddCountryCodeAsync(BinaryContent.Create(input)); + +// Obtain the output response content from the returned ClientResult +BinaryData output = result.GetRawResponse().Content; + +using JsonDocument outputAsJson = JsonDocument.Parse(output.ToString()); +string isoCode = outputAsJson.RootElement + .GetProperty("countryRegion") + .GetProperty("isoCode") + .GetString(); + +Console.WriteLine($"Code for added country is '{isoCode}'."); ``` -### Protocol method concepts - -To compare the convenience and protocol method approaches, let's look more closely at a `System.ClientModel`-based client implementation of a [Geolocation](https://learn.microsoft.com/rest/api/maps/geolocation/get-ip-to-location?view=rest-maps-2023-06-01&tabs=HTTP) service operation. - -The [IpAddressToLocation]() operation result is Pets are represented in the message body as a JSON object: - -```json -{ - "name": "snoopy", - "species": "beagle" -} -``` - -An API using model types could be: - -```csharp -// This is an example model class -public class Pet -{ - string Name { get; } - string Species { get; } -} - -Response GetPet(string dogName); -Response SetPet(Pet dog); -``` - -While the protocol methods version would be: - -```csharp -// Request: "id" in the context path, like "/pets/{id}" -// Response: { -// "name": "snoopy", -// "species": "beagle" -// } -Response GetPet(string id, RequestContext context = null) -// Request: { -// "name": "snoopy", -// "species": "beagle" -// } -// Response: { -// "name": "snoopy", -// "species": "beagle" -// } -Response SetPet(RequestContent requestBody, RequestContext context = null); -``` - -**[Note]**: This document is a general quickstart in using SDK Clients that expose '**protocol methods**'. - -## Usage - -The basic structure of calling protocol methods remains the same as that of convenience methods: - -1. [Initialize Your Client](#1-initialize-your-client "Initialize Your Client") - -2. [Create and Send a request](#2-create-and-send-a-request "Create and Send a Request") - -3. [Handle the Response](#3-handle-the-response "Handle the Response") - -### 1. Initialize Your Client - -The first step in interacting with a service via protocol methods is to create a client instance. - -```csharp -using System; -using Azure.Pets; -using Azure.Core; -using Azure.Identity; - -const string endpoint = "http://localhost:3000"; -var credential = new AzureKeyCredential(/*SERVICE-API-KEY*/); -var client = new PetStoreClient(new Uri(endpoint), credential, new PetStoreClientOptions()); -``` - -### 2. Create and Send a Request +Protocol methods take an optional `RequestOptions` value that allows callers to add a header to the request, or to add a policy to the client pipeline that can modify the request in any way before sending it to the service. `RequestOptions` also allows passing a `CancellationToken` to the method. -Protocol methods need a JSON object of the shape required by the schema of the service. +```C# Snippet:RequestOptionsReadme +// Create RequestOptions instance +RequestOptions options = new(); -See the specific service documentation for details, but as an example: +// Set CancellationToken +options.CancellationToken = cancellationToken; -```csharp -// anonymous class is serialized by System.Text.Json using runtime reflection -var data = new { - name = "snoopy", - species = "beagle" -}; -/* -{ - "name": "snoopy", - "species": "beagle" -} -*/ -client.SetPet(RequestContent.Create(data)); -``` - -### 3. Handle the Response - -Protocol methods all return a `Response` object that contains information returned from the service request. - -The most important field on Response contains the REST content returned from the service: +// Add a header to the request +options.AddHeader("CustomHeader", "CustomHeaderValue"); -```C# Snippet:GetPetAsync -Response response = await client.GetPetAsync("snoopy", new RequestContext()); - -var doc = JsonDocument.Parse(response.Content.ToMemory()); -var name = doc.RootElement.GetProperty("name").GetString(); +// Call protocol method to pass RequestOptions +ClientResult output = await client.GetCountryCodeAsync(ipAddress.ToString(), options); ``` -JSON properties can also be accessed using a dynamic layer. - -```C# Snippet:AzureCoreGetDynamicJsonProperty -Response response = client.GetWidget(); -dynamic widget = response.Content.ToDynamicFromJson(); -string name = widget.name; -``` - - ## Handling exceptions 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. @@ -173,12 +97,13 @@ When a service call fails, service clients throw a `ClientResultException`. The ```C# Snippet:ClientResultExceptionReadme try { - KeyVaultSecret secret = client.GetSecret("NonexistentSecret"); + IPAddress ipAddress = IPAddress.Parse("2001:4898:80e8:b::189"); + ClientResult result = await client.GetCountryCodeAsync(ipAddress); } // handle exception with status code 404 -catch (RequestFailedException e) when (e.Status == 404) +catch (ClientResultException e) when (e.Status == 404) { // handle not found error - Console.WriteLine("ErrorCode " + e.ErrorCode); + Console.Error.WriteLine($"Error: Response failed with status code: '{e.Status}'"); } ``` diff --git a/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs b/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs index e008ab1478e6..8a92530c83c9 100644 --- a/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs +++ b/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs @@ -4,9 +4,11 @@ using System.ClientModel.Primitives; using System.Collections.Generic; using System.Net; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Maps; +using Newtonsoft.Json.Linq; using NUnit.Framework; namespace System.ClientModel.Tests.Samples; @@ -101,4 +103,51 @@ public async Task RequestOptionsReadme() Assert.Fail($"Error: Response status code: '{e.Status}'"); } } + + [Test] + public async Task ServiceMethodsProtocolMethod() + { + string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; + ApiKeyCredential credential = new ApiKeyCredential(key); + + MapsClient client = new MapsClient(new Uri("https://atlas.microsoft.com"), credential); + + // Dummy CancellationToken + CancellationToken cancellationToken = CancellationToken.None; + + try + { +#nullable disable + #region Snippet:ServiceMethodsProtocolMethod + + // Create a BinaryData instance from a JSON string literal + BinaryData input = BinaryData.FromString(""" + { + "countryRegion": { + "isoCode": "US" + }, + } + """); + + // Call the protocol method + ClientResult result = await client.AddCountryCodeAsync(BinaryContent.Create(input)); + + // Obtain the output response content from the returned ClientResult + BinaryData output = result.GetRawResponse().Content; + + using JsonDocument outputAsJson = JsonDocument.Parse(output.ToString()); + string isoCode = outputAsJson.RootElement + .GetProperty("countryRegion") + .GetProperty("isoCode") + .GetString(); + + Console.WriteLine($"Code for added country is '{isoCode}'."); + #endregion +#nullable enable + } + catch (ClientResultException e) + { + Assert.Fail($"Error: Response status code: '{e.Status}'"); + } + } } diff --git a/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClient.cs b/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClient.cs index fd1f5085304a..5a499c597eb2 100644 --- a/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClient.cs +++ b/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClient.cs @@ -101,6 +101,25 @@ public virtual ClientResult GetCountryCode(string ipAddress, RequestOptions? opt return ClientResult.FromResponse(response); } + public virtual async Task AddCountryCodeAsync(BinaryContent content, RequestOptions? options = null) + { + // Fake method used to illlustrate creating input content in ClientModel samples. + // No such operation exists on the Azure Maps service, and this operation implementation + // will not succeed against a live service. + + if (content is null) throw new ArgumentNullException(nameof(content)); + + options ??= new RequestOptions(); + + using PipelineMessage message = CreateGetLocationRequest(string.Empty, options); + + _pipeline.Send(message); + + PipelineResponse response = message.Response!; + + return ClientResult.FromResponse(response); + } + private PipelineMessage CreateGetLocationRequest(string ipAddress, RequestOptions options) { PipelineMessage message = _pipeline.CreateMessage(); From 2d4ec41ab0d84fdaba02acda3d42fc4bbb3724f0 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 4 Mar 2024 12:55:36 -0800 Subject: [PATCH 09/40] fix --- .../tests/client/TestClients/MapsClient/MapsClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClient.cs b/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClient.cs index 5a499c597eb2..9e159cbd0529 100644 --- a/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClient.cs +++ b/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClient.cs @@ -113,7 +113,7 @@ public virtual async Task AddCountryCodeAsync(BinaryContent conten using PipelineMessage message = CreateGetLocationRequest(string.Empty, options); - _pipeline.Send(message); + await _pipeline.SendAsync(message).ConfigureAwait(false); PipelineResponse response = message.Response!; From b9e406f7d2241e8c5539048086b8e766130fb3ef Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 4 Mar 2024 13:20:20 -0800 Subject: [PATCH 10/40] updates --- eng/common/scripts/Test-SampleMetadata.ps1 | 1 + sdk/core/System.ClientModel/README.md | 14 +- .../samples/Configuration.md | 16 +-- .../samples/ProtocolMethods.md | 131 ------------------ .../samples/ServiceMethods.md | 16 +-- .../tests/Samples/ConfigurationSamples.cs | 2 + .../tests/Samples/ServiceMethodSamples.cs | 15 +- 7 files changed, 34 insertions(+), 161 deletions(-) delete mode 100644 sdk/core/System.ClientModel/samples/ProtocolMethods.md diff --git a/eng/common/scripts/Test-SampleMetadata.ps1 b/eng/common/scripts/Test-SampleMetadata.ps1 index 4a0000220fde..94bea6a25d33 100644 --- a/eng/common/scripts/Test-SampleMetadata.ps1 +++ b/eng/common/scripts/Test-SampleMetadata.ps1 @@ -429,6 +429,7 @@ begin { "return-to-workplace", "sql-server-2008", "surface-duo", + "system-clientmodel", "sway", "vs-app-center", "vs-code", diff --git a/sdk/core/System.ClientModel/README.md b/sdk/core/System.ClientModel/README.md index 95a86c5079b3..aa79894178c8 100644 --- a/sdk/core/System.ClientModel/README.md +++ b/sdk/core/System.ClientModel/README.md @@ -60,9 +60,7 @@ For more information on client configuration, see [Client configuration samples] ### 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**. - -`System.ClientModel`-based clients expose two types of service methods: _convenience methods_ and _protocol methods_. +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` that holds a strongly-typed representation of the service response. Details from the HTTP response can be obtained from the return value. @@ -71,12 +69,12 @@ Service clients have methods that are used to call cloud services to invoke serv 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. ```C# Snippet:ClientResultTReadme -// create a client +// Create a client string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; ApiKeyCredential credential = new(key); MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential); -// call a service method, which returns ClientResult +// Call a service method, which returns ClientResult IPAddress ipAddress = IPAddress.Parse("2001:4898:80e8:b::189"); ClientResult result = await client.GetCountryCodeAsync(ipAddress); @@ -109,10 +107,10 @@ try IPAddress ipAddress = IPAddress.Parse("2001:4898:80e8:b::189"); ClientResult result = await client.GetCountryCodeAsync(ipAddress); } -// handle exception with status code 404 +// Handle exception with status code 404 catch (ClientResultException e) when (e.Status == 404) { - // handle not found error + // Handle not found error Console.Error.WriteLine($"Error: Response failed with status code: '{e.Status}'"); } ``` @@ -161,7 +159,7 @@ OutputModel? model = ModelReaderWriter.Read(BinaryData.FromString(j ## Troubleshooting -You can troubleshoot `System.ClientModel`-based clients by inspecting the result of any `ClientResultException` thrown from a client's service 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). diff --git a/sdk/core/System.ClientModel/samples/Configuration.md b/sdk/core/System.ClientModel/samples/Configuration.md index a8904d4073d5..a7eb3d24bb72 100644 --- a/sdk/core/System.ClientModel/samples/Configuration.md +++ b/sdk/core/System.ClientModel/samples/Configuration.md @@ -4,7 +4,7 @@ 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 are setup to retry 3 times using an exponential retry strategy with an initial delay of 0.8 sec, and a max delay of 1 minute. +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. ```C# Snippet:ConfigurationCustomizeRetries MapsClientOptions options = new() @@ -19,24 +19,24 @@ MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential, opti ## Add a custom policy to the pipeline -Azure SDKs provides a way to add policies to the pipeline at three positions: +Azure SDKs provides a way to add policies to the pipeline at three positions, `PerCall`, `PerTry`, and `BeforeTranspor`. -- per-call policies are run once per request +- `PerCall` policies run once per request ```C# Snippet:ConfigurationAddPerCallPolicy MapsClientOptions options = new(); options.AddPolicy(new StopwatchPolicy(), PipelinePosition.PerCall); ``` -- per-try policies are run each time the request is tried +- `PerTry` policies run each time a request is tried ```C# Snippet:ConfigurationAddPerTryPolicy options.AddPolicy(new StopwatchPolicy(), PipelinePosition.PerTry); ``` -- before-transport policies are run after other policies and before the request is sent +- `BeforeTransport` policies run after all other policies in the pipeline and before the request is sent by the transport. -Adding policies at this 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. +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. ```C# Snippet:ConfigurationAddBeforeTransportPolicy options.AddPolicy(new StopwatchPolicy(), PipelinePosition.BeforeTransport); @@ -44,7 +44,7 @@ options.AddPolicy(new StopwatchPolicy(), PipelinePosition.BeforeTransport); ## Implement a custom policy -To implement a policy create a class deriving from `HttpPipelinePolicy` and overide `ProcessAsync` and `Process` methods. Request can be accessed via `message.Request`. Response is accessible via `message.Response` but only after `ProcessNextAsync`/`ProcessNext` was called. +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 @@ -77,7 +77,7 @@ public class StopwatchPolicy : PipelinePolicy ## 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 create the custom `HttpClient` instance to its constructor. +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 client = new(); diff --git a/sdk/core/System.ClientModel/samples/ProtocolMethods.md b/sdk/core/System.ClientModel/samples/ProtocolMethods.md deleted file mode 100644 index 633023823d58..000000000000 --- a/sdk/core/System.ClientModel/samples/ProtocolMethods.md +++ /dev/null @@ -1,131 +0,0 @@ -# C# Azure SDK Clients that Contain Protocol Methods - -## Introduction - -Azure SDK clients provide an interface to Azure services by translating library calls to REST requests. - -In Azure SDK clients, there are two ways to expose the schematized body in the request or response, known as the `message body`: - -- Most Azure SDK Clients expose methods that take ['model types'](https://azure.github.io/azure-sdk/dotnet_introduction.html#dotnet-model-types) as parameters, C# classes which map to the `message body` of the REST call. Those methods can be called here '**convenience methods**'. - -- However, some clients expose methods that mirror the message body directly. Those methods are called here '**protocol methods**', as they provide more direct access to the REST protocol used by the client library. - -### Pet's Example - -To compare the two approaches, imagine a service that stores information about pets, with a pair of `GetPet` and `SetPet` operations. - -Pets are represented in the message body as a JSON object: - -```json -{ - "name": "snoopy", - "species": "beagle" -} -``` - -An API using model types could be: - -```csharp -// This is an example model class -public class Pet -{ - string Name { get; } - string Species { get; } -} - -Response GetPet(string dogName); -Response SetPet(Pet dog); -``` - -While the protocol methods version would be: - -```csharp -// Request: "id" in the context path, like "/pets/{id}" -// Response: { -// "name": "snoopy", -// "species": "beagle" -// } -Response GetPet(string id, RequestContext context = null) -// Request: { -// "name": "snoopy", -// "species": "beagle" -// } -// Response: { -// "name": "snoopy", -// "species": "beagle" -// } -Response SetPet(RequestContent requestBody, RequestContext context = null); -``` - -**[Note]**: This document is a general quickstart in using SDK Clients that expose '**protocol methods**'. - -## Usage - -The basic structure of calling protocol methods remains the same as that of convenience methods: - -1. [Initialize Your Client](#1-initialize-your-client "Initialize Your Client") - -2. [Create and Send a request](#2-create-and-send-a-request "Create and Send a Request") - -3. [Handle the Response](#3-handle-the-response "Handle the Response") - -### 1. Initialize Your Client - -The first step in interacting with a service via protocol methods is to create a client instance. - -```csharp -using System; -using Azure.Pets; -using Azure.Core; -using Azure.Identity; - -const string endpoint = "http://localhost:3000"; -var credential = new AzureKeyCredential(/*SERVICE-API-KEY*/); -var client = new PetStoreClient(new Uri(endpoint), credential, new PetStoreClientOptions()); -``` - -### 2. Create and Send a Request - -Protocol methods need a JSON object of the shape required by the schema of the service. - -See the specific service documentation for details, but as an example: - -```csharp -// anonymous class is serialized by System.Text.Json using runtime reflection -var data = new { - name = "snoopy", - species = "beagle" -}; -/* -{ - "name": "snoopy", - "species": "beagle" -} -*/ -client.SetPet(RequestContent.Create(data)); -``` - -### 3. Handle the Response - -Protocol methods all return a `Response` object that contains information returned from the service request. - -The most important field on Response contains the REST content returned from the service: - -```C# Snippet:GetPetAsync -Response response = await client.GetPetAsync("snoopy", new RequestContext()); - -var doc = JsonDocument.Parse(response.Content.ToMemory()); -var name = doc.RootElement.GetProperty("name").GetString(); -``` - -JSON properties can also be accessed using a dynamic layer. - -```C# Snippet:AzureCoreGetDynamicJsonProperty -Response response = client.GetWidget(); -dynamic widget = response.Content.ToDynamicFromJson(); -string name = widget.name; -``` - -## Configuration And Customization - -**Protocol methods** share the same configuration and customization as **convenience methods**. For details, see the [ReadMe](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/README.md). You can find more samples [here](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/samples/README.md). diff --git a/sdk/core/System.ClientModel/samples/ServiceMethods.md b/sdk/core/System.ClientModel/samples/ServiceMethods.md index 9e8cde03ebac..aa385408e885 100644 --- a/sdk/core/System.ClientModel/samples/ServiceMethods.md +++ b/sdk/core/System.ClientModel/samples/ServiceMethods.md @@ -2,13 +2,13 @@ ## Introduction -`System.ClientModel`-based clients , or **service clients**, provide an interface to cloud services by translating library calls to HTTP requests. +`System.ClientModel`-based clients, or **service clients**, provide an interface to cloud services by translating library calls to HTTP requests. In service clients, there are two ways to expose the schematized body in the request or response, known as the **message body**: -- Most service clients expose methods that take strongly-typed models as parameters, C# classes which map to the message body of the REST call. These methods are called **convenience methods**. +- **Convenience methods** take strongly-typed models as parameters. These models are C# classes which map to the message body of the REST call. -- However, some clients expose methods that mirror the message body directly. Those methods are called here **protocol methods**, as they provide more direct access to the HTTP API protocol used by the service. +- **Protocol method** take primitive types as parameters and their `BinaryContent` input parameters mirror the message body directly. Protocol methods provide more direct access to the HTTP API protocol used by the service. ## Convenience methods @@ -17,12 +17,12 @@ In service clients, there are two ways to expose the schematized body in the req 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. ```C# Snippet:ClientResultTReadme -// create a client +// Create a client string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; ApiKeyCredential credential = new(key); MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential); -// call a service method, which returns ClientResult +// Call a service method, which returns ClientResult IPAddress ipAddress = IPAddress.Parse("2001:4898:80e8:b::189"); ClientResult result = await client.GetCountryCodeAsync(ipAddress); @@ -47,7 +47,7 @@ foreach (KeyValuePair header in response.Headers) In contrast to convenience methods, **protocol methods** are service methods that provide very little convenience over the raw HTTP APIs a cloud service exposes. They represent request and response message bodies using types that are very thin layers over raw JSON/binary/other formats. Users of client protocol methods must reference a service's API documentation directly, rather than relying on the client to provide developer conveniences via strongly-typing service schemas. -The following sample illustrates how to call a protocol method, including creating the request payload, accessing the details of the HTTP response. +The following sample illustrates how to call a protocol method, including creating the request payload and accessing the details of the HTTP response. ```C# Snippet:ServiceMethodsProtocolMethod // Create a BinaryData instance from a JSON string literal @@ -100,10 +100,10 @@ try IPAddress ipAddress = IPAddress.Parse("2001:4898:80e8:b::189"); ClientResult result = await client.GetCountryCodeAsync(ipAddress); } -// handle exception with status code 404 +// Handle exception with status code 404 catch (ClientResultException e) when (e.Status == 404) { - // handle not found error + // Handle not found error Console.Error.WriteLine($"Error: Response failed with status code: '{e.Status}'"); } ``` diff --git a/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs b/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs index 72836e6ad374..3a4ed7b36d23 100644 --- a/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs +++ b/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs @@ -14,6 +14,7 @@ namespace System.ClientModel.Tests.Samples; public class ConfigurationSamples { [Test] + [Ignore("Used for README")] public void ClientModelConfigurationReadme() { #region Snippet:ClientModelConfigurationReadme @@ -31,6 +32,7 @@ public void ClientModelConfigurationReadme() } [Test] + [Ignore("Used for README")] public void ConfigurationCustomizeRetries() { #region Snippet:ConfigurationCustomizeRetries diff --git a/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs b/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs index 8a92530c83c9..133ca8206bcc 100644 --- a/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs +++ b/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs @@ -8,7 +8,6 @@ using System.Threading; using System.Threading.Tasks; using Maps; -using Newtonsoft.Json.Linq; using NUnit.Framework; namespace System.ClientModel.Tests.Samples; @@ -16,15 +15,16 @@ namespace System.ClientModel.Tests.Samples; public class ServiceMethodSamples { [Test] + [Ignore("Used for README")] public async Task ClientResultTReadme() { #region Snippet:ClientResultTReadme - // create a client + // Create a client string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; ApiKeyCredential credential = new(key); MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential); - // call a service method, which returns ClientResult + // Call a service method, which returns ClientResult IPAddress ipAddress = IPAddress.Parse("2001:4898:80e8:b::189"); ClientResult result = await client.GetCountryCodeAsync(ipAddress); @@ -47,9 +47,10 @@ public async Task ClientResultTReadme() } [Test] + [Ignore("Used for README")] public async Task ClientResultExceptionReadme() { - // create a client + // Create a client string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; ApiKeyCredential credential = new(key); MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential); @@ -60,16 +61,17 @@ public async Task ClientResultExceptionReadme() IPAddress ipAddress = IPAddress.Parse("2001:4898:80e8:b::189"); ClientResult result = await client.GetCountryCodeAsync(ipAddress); } - // handle exception with status code 404 + // Handle exception with status code 404 catch (ClientResultException e) when (e.Status == 404) { - // handle not found error + // Handle not found error Console.Error.WriteLine($"Error: Response failed with status code: '{e.Status}'"); } #endregion } [Test] + [Ignore("Used for README")] public async Task RequestOptionsReadme() { string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; @@ -105,6 +107,7 @@ public async Task RequestOptionsReadme() } [Test] + [Ignore("Used for README")] public async Task ServiceMethodsProtocolMethod() { string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; From 8408cf30938b72784191cebe809c4cce64014500 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 4 Mar 2024 13:21:25 -0800 Subject: [PATCH 11/40] nit --- eng/common/scripts/Test-SampleMetadata.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/common/scripts/Test-SampleMetadata.ps1 b/eng/common/scripts/Test-SampleMetadata.ps1 index 94bea6a25d33..5ef9f96a1b95 100644 --- a/eng/common/scripts/Test-SampleMetadata.ps1 +++ b/eng/common/scripts/Test-SampleMetadata.ps1 @@ -429,8 +429,8 @@ begin { "return-to-workplace", "sql-server-2008", "surface-duo", - "system-clientmodel", "sway", + "system-clientmodel", "vs-app-center", "vs-code", "vs-mac", From b316e9cf58017a6db97426862c332b3841345763 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 4 Mar 2024 13:23:40 -0800 Subject: [PATCH 12/40] nit --- sdk/core/System.ClientModel/samples/Configuration.md | 4 ++-- .../System.ClientModel/tests/Samples/ConfigurationSamples.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/core/System.ClientModel/samples/Configuration.md b/sdk/core/System.ClientModel/samples/Configuration.md index a7eb3d24bb72..6c8826f3a9e1 100644 --- a/sdk/core/System.ClientModel/samples/Configuration.md +++ b/sdk/core/System.ClientModel/samples/Configuration.md @@ -80,10 +80,10 @@ public class StopwatchPolicy : PipelinePolicy 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 client = new(); +using HttpClient httpClient = new(); MapsClientOptions options = new() { - Transport = new HttpClientPipelineTransport(client) + Transport = new HttpClientPipelineTransport(httpClient) }; ``` diff --git a/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs b/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs index 3a4ed7b36d23..765f504a251d 100644 --- a/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs +++ b/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs @@ -100,11 +100,11 @@ public void ConfigurationCustomHttpClient() { #region Snippet:ConfigurationCustomHttpClient - using HttpClient client = new(); + using HttpClient httpClient = new(); MapsClientOptions options = new() { - Transport = new HttpClientPipelineTransport(client) + Transport = new HttpClientPipelineTransport(httpClient) }; #endregion From f0c2877554cf4394d0d2def462a2745504656af0 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 4 Mar 2024 16:24:09 -0800 Subject: [PATCH 13/40] fix links --- sdk/core/System.ClientModel/samples/README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sdk/core/System.ClientModel/samples/README.md b/sdk/core/System.ClientModel/samples/README.md index ae12cbb7fe6f..bb4748dcd082 100644 --- a/sdk/core/System.ClientModel/samples/README.md +++ b/sdk/core/System.ClientModel/samples/README.md @@ -11,6 +11,4 @@ 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) -- [Convenience Methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ConvenienceMethods.md) -- [Protocol Methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ProtocolMethods.md) -- [Client Pipeline](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/Pipeline.md) +- [Service Methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md) From 456c6b5bae96098ca832d9b6005cdf1c80f0222f Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 4 Mar 2024 16:38:09 -0800 Subject: [PATCH 14/40] updates from PR feedback --- sdk/core/System.ClientModel/README.md | 6 +++--- sdk/core/System.ClientModel/samples/Configuration.md | 2 +- sdk/core/System.ClientModel/samples/ServiceMethods.md | 2 +- .../tests/Samples/ServiceMethodSamples.cs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sdk/core/System.ClientModel/README.md b/sdk/core/System.ClientModel/README.md index aa79894178c8..1bb0435d7f02 100644 --- a/sdk/core/System.ClientModel/README.md +++ b/sdk/core/System.ClientModel/README.md @@ -62,9 +62,9 @@ For more information on client configuration, see [Client configuration samples] 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` that holds a strongly-typed representation of the service response. Details from the HTTP response can be obtained from the return value. +**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` 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` value that allows the client pipeline and the request to be configured for the duration of the call. +**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. @@ -117,7 +117,7 @@ catch (ClientResultException e) when (e.Status == 404) ### Customizing HTTP requests -Service clients expose low-level _protocol methods_ that allow callers to customize the details of HTTP requests. Protocol methods take an optional `RequestOptions` value that allows callers to add a header to the request, or to add a policy to the client pipeline that can modify the request in any way before sending it to the service. `RequestOptions` also allows passing a `CancellationToken` to the method. +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. ```C# Snippet:RequestOptionsReadme // Create RequestOptions instance diff --git a/sdk/core/System.ClientModel/samples/Configuration.md b/sdk/core/System.ClientModel/samples/Configuration.md index 6c8826f3a9e1..011f02803bef 100644 --- a/sdk/core/System.ClientModel/samples/Configuration.md +++ b/sdk/core/System.ClientModel/samples/Configuration.md @@ -19,7 +19,7 @@ MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential, opti ## Add a custom policy to the pipeline -Azure SDKs provides a way to add policies to the pipeline at three positions, `PerCall`, `PerTry`, and `BeforeTranspor`. +Azure SDKs provides a way to add policies to the pipeline at three positions, `PerCall`, `PerTry`, and `BeforeTransport`. - `PerCall` policies run once per request diff --git a/sdk/core/System.ClientModel/samples/ServiceMethods.md b/sdk/core/System.ClientModel/samples/ServiceMethods.md index aa385408e885..98cdf10d46a9 100644 --- a/sdk/core/System.ClientModel/samples/ServiceMethods.md +++ b/sdk/core/System.ClientModel/samples/ServiceMethods.md @@ -74,7 +74,7 @@ string isoCode = outputAsJson.RootElement Console.WriteLine($"Code for added country is '{isoCode}'."); ``` -Protocol methods take an optional `RequestOptions` value that allows callers to add a header to the request, or to add a policy to the client pipeline that can modify the request in any way before sending it to the service. `RequestOptions` also allows passing a `CancellationToken` to the method. +Protocol methods take an optional `RequestOptions` parameter. `RequestOptions` can be used to modify various aspects of the HTTP 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. ```C# Snippet:RequestOptionsReadme // Create RequestOptions instance diff --git a/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs b/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs index 133ca8206bcc..bd1f27c338a8 100644 --- a/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs +++ b/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs @@ -79,7 +79,7 @@ public async Task RequestOptionsReadme() MapsClient client = new MapsClient(new Uri("https://atlas.microsoft.com"), credential); - // Dummy CancellationToken + // CancellationToken used for snippet - doesn't need a real value. CancellationToken cancellationToken = CancellationToken.None; try From c0db6490e9af882d01d48979c60a07b38ac7c202 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 4 Mar 2024 16:39:19 -0800 Subject: [PATCH 15/40] revert engsys file --- eng/common/scripts/Test-SampleMetadata.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/eng/common/scripts/Test-SampleMetadata.ps1 b/eng/common/scripts/Test-SampleMetadata.ps1 index 5ef9f96a1b95..4a0000220fde 100644 --- a/eng/common/scripts/Test-SampleMetadata.ps1 +++ b/eng/common/scripts/Test-SampleMetadata.ps1 @@ -430,7 +430,6 @@ begin { "sql-server-2008", "surface-duo", "sway", - "system-clientmodel", "vs-app-center", "vs-code", "vs-mac", From 75d0161c3b3f92931c79850ba0a657e3eded1ff6 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Tue, 5 Mar 2024 08:57:33 -0800 Subject: [PATCH 16/40] update product --- sdk/core/System.ClientModel/samples/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/core/System.ClientModel/samples/README.md b/sdk/core/System.ClientModel/samples/README.md index bb4748dcd082..6df2237fdda1 100644 --- a/sdk/core/System.ClientModel/samples/README.md +++ b/sdk/core/System.ClientModel/samples/README.md @@ -3,7 +3,7 @@ page_type: sample languages: - csharp products: -- system.clientmodel +- dotnet-core name: System.ClientModel samples for .NET description: Samples for the System.ClientModel library --- From 262e923b7e5c794bafaaf193f641a4ca74e019e9 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Tue, 5 Mar 2024 12:02:42 -0800 Subject: [PATCH 17/40] add sample client implementation --- sdk/core/System.ClientModel/README.md | 120 ++++++++++++-- .../tests/Samples/ConfigurationSamples.cs | 4 +- .../tests/Samples/PipelineSamples.cs | 153 ++++++++++++++++++ 3 files changed, 264 insertions(+), 13 deletions(-) create mode 100644 sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs diff --git a/sdk/core/System.ClientModel/README.md b/sdk/core/System.ClientModel/README.md index 1bb0435d7f02..601776931d18 100644 --- a/sdk/core/System.ClientModel/README.md +++ b/sdk/core/System.ClientModel/README.md @@ -22,27 +22,125 @@ dotnet add package System.ClientModel None needed for `System.ClientModel`. -### Authenticate the client - -The `System.ClientModel` package provides an `ApiKeyCredential` type for authentication. - ## Key concepts +`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. + The main concepts in `System.ClientModel` include: -- Configuring service clients (`ClientPipelineOptions`). -- Accessing HTTP response details (`ClientResult`, `ClientResult`). -- Handling exceptions that result from failed requests (`ClientResultException`). -- Customizing HTTP requests (`RequestOptions`). +- Client pipeline to send and receive HTTP messages (`ClientPipeline`). +- Interfaces to read and write input and output models in client convenience APIs (`IPersistableModel` and `IJsonModel`). +- Options to configure service clients (`ClientPipelineOptions`). +- Results to enable access to service response and HTTP response details (`ClientResult`, `ClientResult`). +- Exceptions that result from failed requests (`ClientResultException`). +- Options to customize HTTP requests (`RequestOptions`). - Reading and writing models in different formats (`ModelReaderWriter`). Below, you will find sections explaining these shared concepts in more detail. ## Examples +### 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.Empty, + perTryPolicies: new PipelinePolicy[] { authenticationPolicy }, + beforeTransportPolicies: ReadOnlySpan.Empty); + } + + public ClientResult GetResource(string id) + { + PipelineMessage message = _pipeline.CreateMessage(); + message.ResponseClassifier = PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); + + PipelineRequest request = message.Request; + request.Method = "GET"; + request.Uri = new Uri("https://www.example.com/"); + request.Headers.Add("Accept", "application/json"); + + _pipeline.Send(message); + + PipelineResponse response = message.Response!; + + if (response.IsError) + { + throw new ClientResultException(response); + } + + SampleResource resource = ModelReaderWriter.Read(response.Content)!; + return ClientResult.FromValue(resource, 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` and `IJsonModel` 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 +{ + public SampleResource(string id) + { + Id = id; + } + + public string Id { get; init; } + + SampleResource IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) + => FromJson(reader); + + SampleResource IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) + => FromJson(new Utf8JsonReader(data)); + + string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) + => options.Format; + + void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) + => ToJson(writer); + + BinaryData IPersistableModel.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 -`System.ClientModel`-based clients, or **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`. +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 @@ -51,8 +149,8 @@ MapsClientOptions options = new() NetworkTimeout = TimeSpan.FromSeconds(120), }; -string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; -ApiKeyCredential credential = new(key); +string? key = Environment.GetEnvironmentVariable("MAPS_API_KEY"); +ApiKeyCredential credential = new(key!); MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential, options); ``` diff --git a/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs b/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs index 765f504a251d..02ddb677ee76 100644 --- a/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs +++ b/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs @@ -24,8 +24,8 @@ public void ClientModelConfigurationReadme() NetworkTimeout = TimeSpan.FromSeconds(120), }; - string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; - ApiKeyCredential credential = new(key); + string? key = Environment.GetEnvironmentVariable("MAPS_API_KEY"); + ApiKeyCredential credential = new(key!); MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential, options); #endregion diff --git a/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs b/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs new file mode 100644 index 000000000000..0714c1645551 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using System.IO; +using System.Text.Json; +using NUnit.Framework; + +namespace System.ClientModel.Tests.Samples; + +public class PipelineSamples +{ + [Test] + public void CanReadAndWriteSampleResource() + { + SampleResource resource = new("123"); + + IPersistableModel persistableModel = resource; + IJsonModel jsonModel = resource; + + BinaryData persistableModelData = persistableModel.Write(ModelReaderWriterOptions.Json); + SampleResource persistableModelResource = persistableModel.Create(persistableModelData, ModelReaderWriterOptions.Json); + + Assert.AreEqual("""{"id":"123"}""", persistableModelData.ToString()); + Assert.AreEqual("123", persistableModelResource.Id); + + using MemoryStream stream = new(); + using Utf8JsonWriter writer = new(stream); + jsonModel.Write(writer, ModelReaderWriterOptions.Json); + writer.Flush(); + + BinaryData jsonModelData = BinaryData.FromBytes(stream.ToArray()); + Utf8JsonReader reader = new(jsonModelData); + SampleResource jsonModelResource = jsonModel.Create(ref reader, ModelReaderWriterOptions.Json); + + Assert.AreEqual("""{"id":"123"}""", jsonModelData.ToString()); + Assert.AreEqual("123", persistableModelResource.Id); + } + + #region 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.Empty, + perTryPolicies: new PipelinePolicy[] { authenticationPolicy }, + beforeTransportPolicies: ReadOnlySpan.Empty); + } + + public ClientResult GetResource(string id) + { + PipelineMessage message = _pipeline.CreateMessage(); + message.ResponseClassifier = PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); + + PipelineRequest request = message.Request; + request.Method = "GET"; + request.Uri = new Uri("https://www.example.com/"); + request.Headers.Add("Accept", "application/json"); + + _pipeline.Send(message); + + PipelineResponse response = message.Response!; + + if (response.IsError) + { + throw new ClientResultException(response); + } + + SampleResource resource = ModelReaderWriter.Read(response.Content)!; + return ClientResult.FromValue(resource, response); + } + } + #endregion + + public class SampleClientOptions : ClientPipelineOptions + { + private const ServiceVersion LatestVersion = ServiceVersion.V1; + + internal string Version { get; } + + public enum ServiceVersion + { + V1 = 1 + } + + public SampleClientOptions(ServiceVersion version = LatestVersion) + { + Version = version switch + { + ServiceVersion.V1 => "1.0", + _ => throw new NotSupportedException() + }; + } + } + + #region Snippet:ReadmeSampleModel + public class SampleResource : IJsonModel + { + public SampleResource(string id) + { + Id = id; + } + + public string Id { get; init; } + + SampleResource IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) + => FromJson(reader); + + SampleResource IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) + => FromJson(new Utf8JsonReader(data)); + + string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) + => options.Format; + + void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) + => ToJson(writer); + + BinaryData IPersistableModel.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()!); + } + } + #endregion +} From 552b4857312b77cda59ac4f52e74531aa2b70dd8 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Tue, 5 Mar 2024 12:09:49 -0800 Subject: [PATCH 18/40] add input model to sample client method --- sdk/core/System.ClientModel/README.md | 11 ++++++----- .../tests/Samples/PipelineSamples.cs | 11 ++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/sdk/core/System.ClientModel/README.md b/sdk/core/System.ClientModel/README.md index 601776931d18..7272e30e2416 100644 --- a/sdk/core/System.ClientModel/README.md +++ b/sdk/core/System.ClientModel/README.md @@ -65,15 +65,16 @@ public class SampleClient beforeTransportPolicies: ReadOnlySpan.Empty); } - public ClientResult GetResource(string id) + public ClientResult UpdateResource(SampleResource resource) { PipelineMessage message = _pipeline.CreateMessage(); message.ResponseClassifier = PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); PipelineRequest request = message.Request; - request.Method = "GET"; - request.Uri = new Uri("https://www.example.com/"); + 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); @@ -84,8 +85,8 @@ public class SampleClient throw new ClientResultException(response); } - SampleResource resource = ModelReaderWriter.Read(response.Content)!; - return ClientResult.FromValue(resource, response); + SampleResource updated = ModelReaderWriter.Read(response.Content)!; + return ClientResult.FromValue(updated, response); } } ``` diff --git a/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs b/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs index 0714c1645551..d24d5944fb47 100644 --- a/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs +++ b/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs @@ -61,15 +61,16 @@ public SampleClient(Uri endpoint, ApiKeyCredential credential, SampleClientOptio beforeTransportPolicies: ReadOnlySpan.Empty); } - public ClientResult GetResource(string id) + public ClientResult UpdateResource(SampleResource resource) { PipelineMessage message = _pipeline.CreateMessage(); message.ResponseClassifier = PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); PipelineRequest request = message.Request; - request.Method = "GET"; - request.Uri = new Uri("https://www.example.com/"); + 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); @@ -80,8 +81,8 @@ public ClientResult GetResource(string id) throw new ClientResultException(response); } - SampleResource resource = ModelReaderWriter.Read(response.Content)!; - return ClientResult.FromValue(resource, response); + SampleResource updated = ModelReaderWriter.Read(response.Content)!; + return ClientResult.FromValue(updated, response); } } #endregion From d83555e05cb9539927cc050ce41fde82388f45bf Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Tue, 5 Mar 2024 12:14:08 -0800 Subject: [PATCH 19/40] change API key in samples --- sdk/core/System.ClientModel/README.md | 4 ++-- .../System.ClientModel/samples/Configuration.md | 4 ++-- .../System.ClientModel/samples/ServiceMethods.md | 4 ++-- .../tests/Samples/ConfigurationSamples.cs | 4 ++-- .../tests/Samples/ServiceMethodSamples.cs | 16 ++++++++-------- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/sdk/core/System.ClientModel/README.md b/sdk/core/System.ClientModel/README.md index 7272e30e2416..a0c893e97492 100644 --- a/sdk/core/System.ClientModel/README.md +++ b/sdk/core/System.ClientModel/README.md @@ -169,8 +169,8 @@ The following sample illustrates how to call a convenience method and access bot ```C# Snippet:ClientResultTReadme // Create a client -string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; -ApiKeyCredential credential = new(key); +string? key = Environment.GetEnvironmentVariable("MAPS_API_KEY"); +ApiKeyCredential credential = new(key!); MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential); // Call a service method, which returns ClientResult diff --git a/sdk/core/System.ClientModel/samples/Configuration.md b/sdk/core/System.ClientModel/samples/Configuration.md index 011f02803bef..dc71250e39a2 100644 --- a/sdk/core/System.ClientModel/samples/Configuration.md +++ b/sdk/core/System.ClientModel/samples/Configuration.md @@ -12,8 +12,8 @@ MapsClientOptions options = new() RetryPolicy = new ClientRetryPolicy(maxRetries: 5), }; -string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; -ApiKeyCredential credential = new(key); +string? key = Environment.GetEnvironmentVariable("MAPS_API_KEY"); +ApiKeyCredential credential = new(key!); MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential, options); ``` diff --git a/sdk/core/System.ClientModel/samples/ServiceMethods.md b/sdk/core/System.ClientModel/samples/ServiceMethods.md index 98cdf10d46a9..053ffe434a85 100644 --- a/sdk/core/System.ClientModel/samples/ServiceMethods.md +++ b/sdk/core/System.ClientModel/samples/ServiceMethods.md @@ -18,8 +18,8 @@ The following sample illustrates how to call a convenience method and access bot ```C# Snippet:ClientResultTReadme // Create a client -string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; -ApiKeyCredential credential = new(key); +string? key = Environment.GetEnvironmentVariable("MAPS_API_KEY"); +ApiKeyCredential credential = new(key!); MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential); // Call a service method, which returns ClientResult diff --git a/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs b/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs index 02ddb677ee76..30a909760a93 100644 --- a/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs +++ b/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs @@ -42,8 +42,8 @@ public void ConfigurationCustomizeRetries() RetryPolicy = new ClientRetryPolicy(maxRetries: 5), }; - string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; - ApiKeyCredential credential = new(key); + string? key = Environment.GetEnvironmentVariable("MAPS_API_KEY"); + ApiKeyCredential credential = new(key!); MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential, options); #endregion diff --git a/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs b/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs index bd1f27c338a8..30be4ecb5b10 100644 --- a/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs +++ b/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs @@ -20,8 +20,8 @@ public async Task ClientResultTReadme() { #region Snippet:ClientResultTReadme // Create a client - string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; - ApiKeyCredential credential = new(key); + string? key = Environment.GetEnvironmentVariable("MAPS_API_KEY"); + ApiKeyCredential credential = new(key!); MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential); // Call a service method, which returns ClientResult @@ -51,8 +51,8 @@ public async Task ClientResultTReadme() public async Task ClientResultExceptionReadme() { // Create a client - string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; - ApiKeyCredential credential = new(key); + string? key = Environment.GetEnvironmentVariable("MAPS_API_KEY"); + ApiKeyCredential credential = new(key!); MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential); #region Snippet:ClientResultExceptionReadme @@ -74,8 +74,8 @@ public async Task ClientResultExceptionReadme() [Ignore("Used for README")] public async Task RequestOptionsReadme() { - string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; - ApiKeyCredential credential = new ApiKeyCredential(key); + string? key = Environment.GetEnvironmentVariable("MAPS_API_KEY"); + ApiKeyCredential credential = new ApiKeyCredential(key!); MapsClient client = new MapsClient(new Uri("https://atlas.microsoft.com"), credential); @@ -110,8 +110,8 @@ public async Task RequestOptionsReadme() [Ignore("Used for README")] public async Task ServiceMethodsProtocolMethod() { - string key = Environment.GetEnvironmentVariable("MAPS_API_KEY") ?? string.Empty; - ApiKeyCredential credential = new ApiKeyCredential(key); + string? key = Environment.GetEnvironmentVariable("MAPS_API_KEY"); + ApiKeyCredential credential = new ApiKeyCredential(key!); MapsClient client = new MapsClient(new Uri("https://atlas.microsoft.com"), credential); From 6d99f19cb37ebf6761ae20a2759dca362077e6ab Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Tue, 5 Mar 2024 16:33:28 -0800 Subject: [PATCH 20/40] add inline comments to sample client and change defaults on HttpClient sample --- sdk/core/System.ClientModel/README.md | 35 +++++++++++++++++-- .../samples/Configuration.md | 11 +++++- .../tests/Samples/ConfigurationSamples.cs | 11 ++++-- .../tests/Samples/PipelineSamples.cs | 35 +++++++++++++++++-- 4 files changed, 85 insertions(+), 7 deletions(-) diff --git a/sdk/core/System.ClientModel/README.md b/sdk/core/System.ClientModel/README.md index a0c893e97492..415994c1b198 100644 --- a/sdk/core/System.ClientModel/README.md +++ b/sdk/core/System.ClientModel/README.md @@ -51,14 +51,23 @@ public class SampleClient private readonly ApiKeyCredential _credential; private readonly ClientPipeline _pipeline; + // Constructor takes service endpoint, credential used to authenticate + // with service, and options for configuring the client pipeline. public SampleClient(Uri endpoint, ApiKeyCredential credential, SampleClientOptions? options = default) { + // Default options are used if none are passed by the client user. options ??= new SampleClientOptions(); _endpoint = endpoint; _credential = credential; - ApiKeyAuthenticationPolicy authenticationPolicy = ApiKeyAuthenticationPolicy.CreateBearerAuthorizationPolicy(credential); + // Authentication policy instance is created from the user-provided + // credential and service authentication scheme. + ApiKeyAuthenticationPolicy authenticationPolicy + = ApiKeyAuthenticationPolicy.CreateBearerAuthorizationPolicy(credential); + + // Pipeline is created from user-provided options and policies + // specific to the service client implementation. _pipeline = ClientPipeline.Create(options, perCallPolicies: ReadOnlySpan.Empty, perTryPolicies: new PipelinePolicy[] { authenticationPolicy }, @@ -67,25 +76,45 @@ public class SampleClient public ClientResult UpdateResource(SampleResource resource) { + // Create the message that will be sent via the pipeline. PipelineMessage message = _pipeline.CreateMessage(); - message.ResponseClassifier = PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); + // Set a classifier that will decide whether the response is an + // error response based on the response status code. + message.ResponseClassifier + = PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); + + // Modify the request as needed for the service operation. 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"); + + // Add request content that will be written using methods defined + // by the IJsonModel interface. request.Content = BinaryContent.Create(resource); + // Send the message. _pipeline.Send(message); + // Obtain the response from the message Response property. + // The PipelineTransport ensures that the Response Value is set + // so that every policy in the pipeline can access the property. PipelineResponse response = message.Response!; + // If the response is considered an error response, throw an + // exception exposing the response details. if (response.IsError) { throw new ClientResultException(response); } + // Read the content from the response content and create an instance + // of a model from it, to return from this method. SampleResource updated = ModelReaderWriter.Read(response.Content)!; + + // Return a ClientResult holding the model instance and the + // HTTP response details. return ClientResult.FromValue(updated, response); } } @@ -120,6 +149,7 @@ public class SampleResource : IJsonModel BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => ModelReaderWriter.Write(this, options); + // Write the model JSON that will populate the HTTP request content. private void ToJson(Utf8JsonWriter writer) { writer.WriteStartObject(); @@ -128,6 +158,7 @@ public class SampleResource : IJsonModel writer.WriteEndObject(); } + // Read the JSON response content and create a model instance from it. private static SampleResource FromJson(Utf8JsonReader reader) { reader.Read(); // start object diff --git a/sdk/core/System.ClientModel/samples/Configuration.md b/sdk/core/System.ClientModel/samples/Configuration.md index dc71250e39a2..7924df24497e 100644 --- a/sdk/core/System.ClientModel/samples/Configuration.md +++ b/sdk/core/System.ClientModel/samples/Configuration.md @@ -80,7 +80,16 @@ public class StopwatchPolicy : PipelinePolicy 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(); +using HttpClientHandler handler = new() +{ + // Reduce the max connections per server, which defaults to 50. + MaxConnectionsPerServer = 25, + + // Preserve default System.ClientModel redirect behavior. + AllowAutoRedirect = false, +}; + +using HttpClient httpClient = new(handler); MapsClientOptions options = new() { diff --git a/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs b/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs index 30a909760a93..bfdcb974bb3b 100644 --- a/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs +++ b/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs @@ -99,14 +99,21 @@ public override void Process(PipelineMessage message, IReadOnlyList.Empty, perTryPolicies: new PipelinePolicy[] { authenticationPolicy }, @@ -63,25 +72,45 @@ public SampleClient(Uri endpoint, ApiKeyCredential credential, SampleClientOptio public ClientResult UpdateResource(SampleResource resource) { + // Create the message that will be sent via the pipeline. PipelineMessage message = _pipeline.CreateMessage(); - message.ResponseClassifier = PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); + // Set a classifier that will decide whether the response is an + // error response based on the response status code. + message.ResponseClassifier + = PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); + + // Modify the request as needed for the service operation. 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"); + + // Add request content that will be written using methods defined + // by the IJsonModel interface. request.Content = BinaryContent.Create(resource); + // Send the message. _pipeline.Send(message); + // Obtain the response from the message Response property. + // The PipelineTransport ensures that the Response Value is set + // so that every policy in the pipeline can access the property. PipelineResponse response = message.Response!; + // If the response is considered an error response, throw an + // exception exposing the response details. if (response.IsError) { throw new ClientResultException(response); } + // Read the content from the response content and create an instance + // of a model from it, to return from this method. SampleResource updated = ModelReaderWriter.Read(response.Content)!; + + // Return a ClientResult holding the model instance and the + // HTTP response details. return ClientResult.FromValue(updated, response); } } @@ -133,6 +162,7 @@ void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOp BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => ModelReaderWriter.Write(this, options); + // Write the model JSON that will populate the HTTP request content. private void ToJson(Utf8JsonWriter writer) { writer.WriteStartObject(); @@ -141,6 +171,7 @@ private void ToJson(Utf8JsonWriter writer) writer.WriteEndObject(); } + // Read the JSON response content and create a model instance from it. private static SampleResource FromJson(Utf8JsonReader reader) { reader.Read(); // start object From ee42d04688d74ffa533d20a1a3128861d59a54e9 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Tue, 5 Mar 2024 16:37:56 -0800 Subject: [PATCH 21/40] update impressions link --- sdk/core/System.ClientModel/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/core/System.ClientModel/README.md b/sdk/core/System.ClientModel/README.md index 415994c1b198..819e30cbf57c 100644 --- a/sdk/core/System.ClientModel/README.md +++ b/sdk/core/System.ClientModel/README.md @@ -303,7 +303,7 @@ When you submit a pull request, a CLA-bot will automatically determine whether y This project has adopted the [Microsoft Open Source Code of Conduct][code_of_conduct]. For more information see the [Code of Conduct FAQ][code_of_conduct_faq] or contact opencode@microsoft.com with any additional questions or comments. -![Impressions](https://azure-sdk-impressions.azurewebsites.net/api/impressions/azure-sdk-for-net%2Fsdk%2Fcore%2FAzure.Core%2FREADME.png) +![Impressions](https://azure-sdk-impressions.azurewebsites.net/api/impressions/azure-sdk-for-net%2Fsdk%2Fcore%2FSytem.ClientModel%2FREADME.png) [source]: https://github.com/Azure/azure-sdk-for-net/tree/main/sdk/core/System.ClientModel/src [package]: https://www.nuget.org/packages/System.ClientModel From e8b6e09129f3579e3d70ad793b3923fdeb058c94 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Tue, 5 Mar 2024 17:33:25 -0800 Subject: [PATCH 22/40] restructure to address PR feedback --- sdk/core/System.ClientModel/README.md | 105 ++++++++++-------- .../samples/Configuration.md | 52 ++++----- .../samples/ModelReaderWriter.md | 24 ++++ .../samples/ServiceMethods.md | 11 +- .../tests/Samples/ServiceMethodSamples.cs | 11 +- 5 files changed, 123 insertions(+), 80 deletions(-) create mode 100644 sdk/core/System.ClientModel/samples/ModelReaderWriter.md diff --git a/sdk/core/System.ClientModel/README.md b/sdk/core/System.ClientModel/README.md index 819e30cbf57c..bc305596a88b 100644 --- a/sdk/core/System.ClientModel/README.md +++ b/sdk/core/System.ClientModel/README.md @@ -24,23 +24,26 @@ None needed for `System.ClientModel`. ## Key concepts -`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. +`System.ClientModel` contains types in two major categories: those used to author service clients and those exposed in the public APIs of clients built using `System.ClientModel` types, and those used by the end-users of service clients to communicate with a cloud service. -The main concepts in `System.ClientModel` include: +Types used to author service clients appear in the `System.ClientModel.Primitives` namespace. Key concepts involving these types include: -- Client pipeline to send and receive HTTP messages (`ClientPipeline`). -- Interfaces to read and write input and output models in client convenience APIs (`IPersistableModel` and `IJsonModel`). -- Options to configure service clients (`ClientPipelineOptions`). -- Results to enable access to service response and HTTP response details (`ClientResult`, `ClientResult`). +- Client pipeline used to send and receive HTTP messages (`ClientPipeline`). +- Interfaces used to read and write input and output models exposed in client convenience APIs (`IPersistableModel` and `IJsonModel`). + +Service methods that end-users of clients call to invoke service operations fall into two categories: [convenience](https://devblogs.microsoft.com/dotnet/the-convenience-of-dotnet/) methods and lower-level protocol methods. Types used in clients' [convenience methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#convenience-methods) appear in the root `System.ClientModel` namespace. Types used in [protocol methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#protocol-methods) and other lower-level scenarios appear in the `System.ClientModel.Primitives` namespace. Key concepts involving these types include: + +- Results that provide access to the service response and the HTTP response details (`ClientResult`, `ClientResult`). - Exceptions that result from failed requests (`ClientResultException`). -- Options to customize HTTP requests (`RequestOptions`). -- Reading and writing models in different formats (`ModelReaderWriter`). +- Options used to configure the service client pipeline (`ClientPipelineOptions`). +- Options used to customize HTTP requests (`RequestOptions`). +- Content sent in an HTTP request body (`BinaryContent`). Below, you will find sections explaining these shared concepts in more detail. ## Examples -### Send a message using the ClientPipeline +### Send a message using 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. @@ -122,7 +125,7 @@ public class SampleClient ### 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` and `IJsonModel` 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. +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` and `IJsonModel` 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 [Send a message using the ClientPipeline](#send-a-message-using-clientpipeline). The following sample shows a minimal example of what a persistable model implementation might look like. ```C# Snippet:ReadmeSampleModel public class SampleResource : IJsonModel @@ -170,27 +173,11 @@ public class SampleResource : IJsonModel } ``` -### 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) +For more information on reading and writing persistable models, see [Model reader writer samples](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ModelReaderWriter.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_. +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](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#convenience-methods) and [protocol methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#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` that holds a strongly-typed representation of the service response. Details from the HTTP response may also be obtained from the return value. @@ -245,9 +232,27 @@ catch (ClientResultException e) when (e.Status == 404) } ``` +### 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) + ### 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. +Service clients expose low-level [protocol methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#protocol-methods) that allow callers to customize 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 allows an client user to pass a `CancellationToken` to the method. ```C# Snippet:RequestOptionsReadme // Create RequestOptions instance @@ -265,26 +270,36 @@ ClientResult output = await client.GetCountryCodeAsync(ipAddress.ToString(), opt 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 +### Provide request content + +In service clients' [protocol methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#protocol-methods), users pass the request content as an input parameter to the method as the `BinaryContent` type. The following example shows how an instance of `BinaryContent` can be created and passed to a protocol method. -Client library authors can implement the `IPersistableModel` or `IJsonModel` 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. +```C# Snippet:ServiceMethodsProtocolMethod +// Create a BinaryData instance from a JSON string literal. +BinaryData input = BinaryData.FromString(""" + { + "countryRegion": { + "isoCode": "US" + }, + } + """); -The example below shows how to write a persistable model to `BinaryData`. +// Create a BinaryContent instance to set as the HTTP request content. +BinaryContent requestContent = BinaryContent.Create(input); -```C# Snippet:Readme_Write_Simple -InputModel model = new InputModel(); -BinaryData data = ModelReaderWriter.Write(model); -``` +// Call the protocol method +ClientResult result = await client.AddCountryCodeAsync(requestContent); -The example below shows how to read JSON to create a strongly-typed model instance. +// Obtain the output response content from the returned ClientResult. +BinaryData output = result.GetRawResponse().Content; -```C# Snippet:Readme_Read_Simple -string json = @"{ - ""x"": 1, - ""y"": 2, - ""z"": 3 - }"; -OutputModel? model = ModelReaderWriter.Read(BinaryData.FromString(json)); +using JsonDocument outputAsJson = JsonDocument.Parse(output.ToString()); +string isoCode = outputAsJson.RootElement + .GetProperty("countryRegion") + .GetProperty("isoCode") + .GetString(); + +Console.WriteLine($"Code for added country is '{isoCode}'."); ``` ## Troubleshooting @@ -293,8 +308,6 @@ You can troubleshoot service clients by inspecting the result of any `ClientResu 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 - ## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.microsoft.com. diff --git a/sdk/core/System.ClientModel/samples/Configuration.md b/sdk/core/System.ClientModel/samples/Configuration.md index 7924df24497e..d9206d140d2d 100644 --- a/sdk/core/System.ClientModel/samples/Configuration.md +++ b/sdk/core/System.ClientModel/samples/Configuration.md @@ -17,34 +17,9 @@ 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. - -```C# Snippet:ConfigurationAddBeforeTransportPolicy -options.AddPolicy(new StopwatchPolicy(), PipelinePosition.BeforeTransport); -``` - ## Implement a custom policy -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. +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. ```C# Snippet:ConfigurationCustomPolicy public class StopwatchPolicy : PipelinePolicy @@ -75,6 +50,31 @@ public class StopwatchPolicy : PipelinePolicy } ``` +## 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. + +```C# Snippet:ConfigurationAddBeforeTransportPolicy +options.AddPolicy(new StopwatchPolicy(), PipelinePosition.BeforeTransport); +``` + ## 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. diff --git a/sdk/core/System.ClientModel/samples/ModelReaderWriter.md b/sdk/core/System.ClientModel/samples/ModelReaderWriter.md new file mode 100644 index 000000000000..78eeadc6f5fe --- /dev/null +++ b/sdk/core/System.ClientModel/samples/ModelReaderWriter.md @@ -0,0 +1,24 @@ + +# System.ClientModel-based ModelReaderWriter samples + +## Read and write persistable models + +Client library authors can implement the `IPersistableModel` or `IJsonModel` 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. + +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); +``` + +The example below shows how to read JSON to create a strongly-typed model instance. + +```C# Snippet:Readme_Read_Simple +string json = @"{ + ""x"": 1, + ""y"": 2, + ""z"": 3 + }"; +OutputModel? model = ModelReaderWriter.Read(BinaryData.FromString(json)); +``` diff --git a/sdk/core/System.ClientModel/samples/ServiceMethods.md b/sdk/core/System.ClientModel/samples/ServiceMethods.md index 053ffe434a85..2cbe17829ba5 100644 --- a/sdk/core/System.ClientModel/samples/ServiceMethods.md +++ b/sdk/core/System.ClientModel/samples/ServiceMethods.md @@ -50,8 +50,8 @@ In contrast to convenience methods, **protocol methods** are service methods tha The following sample illustrates how to call a protocol method, including creating the request payload and accessing the details of the HTTP response. ```C# Snippet:ServiceMethodsProtocolMethod -// Create a BinaryData instance from a JSON string literal -BinaryData input = BinaryData.FromString(""" +// Create a BinaryData instance from a JSON string literal. +BinaryData input = BinaryData.FromString(""" { "countryRegion": { "isoCode": "US" @@ -59,10 +59,13 @@ BinaryData input = BinaryData.FromString(""" } """); +// Create a BinaryContent instance to set as the HTTP request content. +BinaryContent requestContent = BinaryContent.Create(input); + // Call the protocol method -ClientResult result = await client.AddCountryCodeAsync(BinaryContent.Create(input)); +ClientResult result = await client.AddCountryCodeAsync(requestContent); -// Obtain the output response content from the returned ClientResult +// Obtain the output response content from the returned ClientResult. BinaryData output = result.GetRawResponse().Content; using JsonDocument outputAsJson = JsonDocument.Parse(output.ToString()); diff --git a/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs b/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs index 30be4ecb5b10..6f1a7d06b944 100644 --- a/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs +++ b/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs @@ -123,8 +123,8 @@ public async Task ServiceMethodsProtocolMethod() #nullable disable #region Snippet:ServiceMethodsProtocolMethod - // Create a BinaryData instance from a JSON string literal - BinaryData input = BinaryData.FromString(""" + // Create a BinaryData instance from a JSON string literal. + BinaryData input = BinaryData.FromString(""" { "countryRegion": { "isoCode": "US" @@ -132,10 +132,13 @@ public async Task ServiceMethodsProtocolMethod() } """); + // Create a BinaryContent instance to set as the HTTP request content. + BinaryContent requestContent = BinaryContent.Create(input); + // Call the protocol method - ClientResult result = await client.AddCountryCodeAsync(BinaryContent.Create(input)); + ClientResult result = await client.AddCountryCodeAsync(requestContent); - // Obtain the output response content from the returned ClientResult + // Obtain the output response content from the returned ClientResult. BinaryData output = result.GetRawResponse().Content; using JsonDocument outputAsJson = JsonDocument.Parse(output.ToString()); From 81eae19140e5d889fd5364f49ceb99d97e110d46 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Tue, 5 Mar 2024 17:37:28 -0800 Subject: [PATCH 23/40] nits --- sdk/core/System.ClientModel/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/core/System.ClientModel/README.md b/sdk/core/System.ClientModel/README.md index bc305596a88b..ff45157e72fb 100644 --- a/sdk/core/System.ClientModel/README.md +++ b/sdk/core/System.ClientModel/README.md @@ -24,7 +24,7 @@ None needed for `System.ClientModel`. ## Key concepts -`System.ClientModel` contains types in two major categories: those used to author service clients and those exposed in the public APIs of clients built using `System.ClientModel` types, and those used by the end-users of service clients to communicate with a cloud service. +`System.ClientModel` contains types in two major categories: (1) those used to author service clients, and (2) those exposed in the public APIs of clients built using `System.ClientModel` types. The latter are intended for use by the end-users of service clients to communicate with cloud services. Types used to author service clients appear in the `System.ClientModel.Primitives` namespace. Key concepts involving these types include: From 095324c2540b53f6fa7dba15522d0efd6e935ef7 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Tue, 5 Mar 2024 17:45:06 -0800 Subject: [PATCH 24/40] nits --- sdk/core/System.ClientModel/README.md | 29 +++++++++---------- .../tests/Samples/PipelineSamples.cs | 29 +++++++++---------- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/sdk/core/System.ClientModel/README.md b/sdk/core/System.ClientModel/README.md index ff45157e72fb..032508849fde 100644 --- a/sdk/core/System.ClientModel/README.md +++ b/sdk/core/System.ClientModel/README.md @@ -55,10 +55,10 @@ public class SampleClient private readonly ClientPipeline _pipeline; // Constructor takes service endpoint, credential used to authenticate - // with service, and options for configuring the client pipeline. + // with the service, and options for configuring the client pipeline. public SampleClient(Uri endpoint, ApiKeyCredential credential, SampleClientOptions? options = default) { - // Default options are used if none are passed by the client user. + // Default options are used if none are passed by the client's user. options ??= new SampleClientOptions(); _endpoint = endpoint; @@ -66,8 +66,7 @@ public class SampleClient // Authentication policy instance is created from the user-provided // credential and service authentication scheme. - ApiKeyAuthenticationPolicy authenticationPolicy - = ApiKeyAuthenticationPolicy.CreateBearerAuthorizationPolicy(credential); + ApiKeyAuthenticationPolicy authenticationPolicy = ApiKeyAuthenticationPolicy.CreateBearerAuthorizationPolicy(credential); // Pipeline is created from user-provided options and policies // specific to the service client implementation. @@ -79,45 +78,45 @@ public class SampleClient public ClientResult UpdateResource(SampleResource resource) { - // Create the message that will be sent via the pipeline. + // Create a message that can be sent via the client pipeline. PipelineMessage message = _pipeline.CreateMessage(); - // Set a classifier that will decide whether the response is an + // Set a classifier that will determine whether the response is an // error response based on the response status code. message.ResponseClassifier = PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); - // Modify the request as needed for the service operation. + // Modify the request as needed to invoke the service operation. 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"); - // Add request content that will be written using methods defined - // by the IJsonModel interface. + // Add request body content that will be written using methods + // defined by the model's implementation of the IJsonModel interface. request.Content = BinaryContent.Create(resource); // Send the message. _pipeline.Send(message); // Obtain the response from the message Response property. - // The PipelineTransport ensures that the Response Value is set + // The PipelineTransport ensures that the Response value is set // so that every policy in the pipeline can access the property. PipelineResponse response = message.Response!; // If the response is considered an error response, throw an - // exception exposing the response details. + // exception that exposes the response details. if (response.IsError) { throw new ClientResultException(response); } - // Read the content from the response content and create an instance - // of a model from it, to return from this method. + // Read the content from the response body and create an instance of + // a model from it, to include in the type returned by this method. SampleResource updated = ModelReaderWriter.Read(response.Content)!; - // Return a ClientResult holding the model instance and the - // HTTP response details. + // Return a ClientResult holding the model instance and the HTTP + // response details. return ClientResult.FromValue(updated, response); } } diff --git a/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs b/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs index 8d1922c08b71..29f0d184ccf2 100644 --- a/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs +++ b/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs @@ -48,10 +48,10 @@ public class SampleClient private readonly ClientPipeline _pipeline; // Constructor takes service endpoint, credential used to authenticate - // with service, and options for configuring the client pipeline. + // with the service, and options for configuring the client pipeline. public SampleClient(Uri endpoint, ApiKeyCredential credential, SampleClientOptions? options = default) { - // Default options are used if none are passed by the client user. + // Default options are used if none are passed by the client's user. options ??= new SampleClientOptions(); _endpoint = endpoint; @@ -59,8 +59,7 @@ public SampleClient(Uri endpoint, ApiKeyCredential credential, SampleClientOptio // Authentication policy instance is created from the user-provided // credential and service authentication scheme. - ApiKeyAuthenticationPolicy authenticationPolicy - = ApiKeyAuthenticationPolicy.CreateBearerAuthorizationPolicy(credential); + ApiKeyAuthenticationPolicy authenticationPolicy = ApiKeyAuthenticationPolicy.CreateBearerAuthorizationPolicy(credential); // Pipeline is created from user-provided options and policies // specific to the service client implementation. @@ -72,45 +71,45 @@ ApiKeyAuthenticationPolicy authenticationPolicy public ClientResult UpdateResource(SampleResource resource) { - // Create the message that will be sent via the pipeline. + // Create a message that can be sent via the client pipeline. PipelineMessage message = _pipeline.CreateMessage(); - // Set a classifier that will decide whether the response is an + // Set a classifier that will determine whether the response is an // error response based on the response status code. message.ResponseClassifier = PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); - // Modify the request as needed for the service operation. + // Modify the request as needed to invoke the service operation. 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"); - // Add request content that will be written using methods defined - // by the IJsonModel interface. + // Add request body content that will be written using methods + // defined by the model's implementation of the IJsonModel interface. request.Content = BinaryContent.Create(resource); // Send the message. _pipeline.Send(message); // Obtain the response from the message Response property. - // The PipelineTransport ensures that the Response Value is set + // The PipelineTransport ensures that the Response value is set // so that every policy in the pipeline can access the property. PipelineResponse response = message.Response!; // If the response is considered an error response, throw an - // exception exposing the response details. + // exception that exposes the response details. if (response.IsError) { throw new ClientResultException(response); } - // Read the content from the response content and create an instance - // of a model from it, to return from this method. + // Read the content from the response body and create an instance of + // a model from it, to include in the type returned by this method. SampleResource updated = ModelReaderWriter.Read(response.Content)!; - // Return a ClientResult holding the model instance and the - // HTTP response details. + // Return a ClientResult holding the model instance and the HTTP + // response details. return ClientResult.FromValue(updated, response); } } From 47a47caf65f55ee31a4f3a312f995f01557895d5 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Tue, 5 Mar 2024 17:50:45 -0800 Subject: [PATCH 25/40] nits --- sdk/core/System.ClientModel/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/core/System.ClientModel/README.md b/sdk/core/System.ClientModel/README.md index 032508849fde..6a1dbba63636 100644 --- a/sdk/core/System.ClientModel/README.md +++ b/sdk/core/System.ClientModel/README.md @@ -124,7 +124,7 @@ public class SampleClient ### Reading and writing model content to HTTP messages -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` and `IJsonModel` 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 [Send a message using the ClientPipeline](#send-a-message-using-clientpipeline). The following sample shows a minimal example of what a persistable model implementation might look like. +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` and `IJsonModel` 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 [Send a message using the ClientPipeline](#send-a-message-using-clientpipeline). The following sample shows a minimal example of what a persistable model implementation might look like. ```C# Snippet:ReadmeSampleModel public class SampleResource : IJsonModel @@ -247,7 +247,7 @@ 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) +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). ### Customizing HTTP requests @@ -267,11 +267,11 @@ options.AddHeader("CustomHeader", "CustomHeaderValue"); ClientResult output = await client.GetCountryCodeAsync(ipAddress.ToString(), options); ``` -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) +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). ### Provide request content -In service clients' [protocol methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#protocol-methods), users pass the request content as an input parameter to the method as the `BinaryContent` type. The following example shows how an instance of `BinaryContent` can be created and passed to a protocol method. +In service clients' [protocol methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#protocol-methods), users pass the request content as an input parameter to the method as an instance of `BinaryContent`. The following example shows how to create a `BinaryContent` instance to pass to a protocol method. ```C# Snippet:ServiceMethodsProtocolMethod // Create a BinaryData instance from a JSON string literal. From 91d322397ce62c7941618bd8906211fd20d1141d Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Wed, 6 Mar 2024 11:47:05 -0800 Subject: [PATCH 26/40] small updates from PR feedback --- sdk/core/System.ClientModel/README.md | 7 +------ .../System.ClientModel/tests/Samples/PipelineSamples.cs | 5 ----- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/sdk/core/System.ClientModel/README.md b/sdk/core/System.ClientModel/README.md index 6a1dbba63636..76cb44b4cae7 100644 --- a/sdk/core/System.ClientModel/README.md +++ b/sdk/core/System.ClientModel/README.md @@ -24,7 +24,7 @@ None needed for `System.ClientModel`. ## Key concepts -`System.ClientModel` contains types in two major categories: (1) those used to author service clients, and (2) those exposed in the public APIs of clients built using `System.ClientModel` types. The latter are intended for use by the end-users of service clients to communicate with cloud services. +`System.ClientModel` contains two major categories of types: (1) types used to author service clients, and (2) types exposed in the public APIs of clients built using `System.ClientModel` types. The latter are intended for use by the end-users of service clients to communicate with cloud services. Types used to author service clients appear in the `System.ClientModel.Primitives` namespace. Key concepts involving these types include: @@ -81,11 +81,6 @@ public class SampleClient // Create a message that can be sent via the client pipeline. PipelineMessage message = _pipeline.CreateMessage(); - // Set a classifier that will determine whether the response is an - // error response based on the response status code. - message.ResponseClassifier - = PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); - // Modify the request as needed to invoke the service operation. PipelineRequest request = message.Request; request.Method = "PATCH"; diff --git a/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs b/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs index 29f0d184ccf2..351426c34641 100644 --- a/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs +++ b/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs @@ -74,11 +74,6 @@ public ClientResult UpdateResource(SampleResource resource) // Create a message that can be sent via the client pipeline. PipelineMessage message = _pipeline.CreateMessage(); - // Set a classifier that will determine whether the response is an - // error response based on the response status code. - message.ResponseClassifier - = PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); - // Modify the request as needed to invoke the service operation. PipelineRequest request = message.Request; request.Method = "PATCH"; From 863cec65455e1f54fa9d3131c779d1f1c6072174 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Wed, 6 Mar 2024 17:56:31 -0800 Subject: [PATCH 27/40] add comment --- sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs b/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs index 351426c34641..760594018b25 100644 --- a/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs +++ b/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs @@ -69,6 +69,9 @@ public SampleClient(Uri endpoint, ApiKeyCredential credential, SampleClientOptio beforeTransportPolicies: ReadOnlySpan.Empty); } + // Service method takes an input model representing a service resource + // and returns `ClientResult` holding an output model representing + // the value returned in the service response. public ClientResult UpdateResource(SampleResource resource) { // Create a message that can be sent via the client pipeline. From 7a02ba253396e0143f02f679c82733f0b424a5e6 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Thu, 7 Mar 2024 10:52:00 -0800 Subject: [PATCH 28/40] rework convenience methods section in README --- sdk/core/System.ClientModel/README.md | 28 ++++++++++--------- .../samples/ServiceMethods.md | 19 ++++++------- .../tests/Samples/ServiceMethodSamples.cs | 13 +++++---- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/sdk/core/System.ClientModel/README.md b/sdk/core/System.ClientModel/README.md index 76cb44b4cae7..e4ad3abf59ae 100644 --- a/sdk/core/System.ClientModel/README.md +++ b/sdk/core/System.ClientModel/README.md @@ -76,6 +76,9 @@ public class SampleClient beforeTransportPolicies: ReadOnlySpan.Empty); } + // Service method takes an input model representing a service resource + // and returns `ClientResult` holding an output model representing + // the value returned in the service response. public ClientResult UpdateResource(SampleResource resource) { // Create a message that can be sent via the client pipeline. @@ -169,33 +172,32 @@ public class SampleResource : IJsonModel For more information on reading and writing persistable models, see [Model reader writer samples](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ModelReaderWriter.md). -### Accessing HTTP response details +### Accessing the service response -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](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#convenience-methods) and [protocol methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#protocol-methods). +Service clients have methods that are used to call cloud services to invoke service operations. These methods on a client are called **service methods**, and they send a request to the service and return a representation of its response to the caller. Service clients expose two types of service methods: [convenience methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#convenience-methods) and [protocol methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#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` that holds a strongly-typed representation of the service response. Details from the HTTP response may also be obtained from the return value. +**Convenience methods** provide a [convenient](https://devblogs.microsoft.com/dotnet/the-convenience-of-dotnet/) way to invoke a service operation. They are methods that take a strongly-typed model as input and return a `ClientResult` 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. +The following sample illustrates how to call a convenience method and access the strongly-typed output model from the service response. -```C# Snippet:ClientResultTReadme -// Create a client -string? key = Environment.GetEnvironmentVariable("MAPS_API_KEY"); -ApiKeyCredential credential = new(key!); +```C# Snippet:ReadmeClientResultT MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential); -// Call a service method, which returns ClientResult +// Call a convenience method, which returns ClientResult IPAddress ipAddress = IPAddress.Parse("2001:4898:80e8:b::189"); ClientResult result = await client.GetCountryCodeAsync(ipAddress); -// ClientResult has two members: -// -// (1) A Value property to access the strongly-typed output +// Access the output model from the service response. IPAddressCountryPair value = result.Value; Console.WriteLine($"Country is {value.CountryRegion.IsoCode}."); +``` + +If needed, callers can obtain the details of the HTTP response by calling the result's `GetRawResponse` method. -// (2) A GetRawResponse method for accessing the details of the HTTP response +```C# Snippet:ReadmeGetRawResponse +// Access the HTTP response details. PipelineResponse response = result.GetRawResponse(); Console.WriteLine($"Response status code: '{response.Status}'."); diff --git a/sdk/core/System.ClientModel/samples/ServiceMethods.md b/sdk/core/System.ClientModel/samples/ServiceMethods.md index 2cbe17829ba5..1fbbd1893e7f 100644 --- a/sdk/core/System.ClientModel/samples/ServiceMethods.md +++ b/sdk/core/System.ClientModel/samples/ServiceMethods.md @@ -14,25 +14,24 @@ In service clients, there are two ways to expose the schematized body in the req **Convenience methods** provide a convenient way to invoke a service operation. They are service methods that take a strongly-typed model representing schematized data sent to the service as input, and return a strongly-typed model representing the payload from the service response as output. Having strongly-typed models that represent service concepts provides a layer of convenience over working with the raw payload format. This is because these models unify the client user experience when cloud services differ in payload formats. That is, a client-user can learn the patterns for strongly-typed models that `System.ClientModel`-based clients provide, and use them together without having to reason about whether a cloud service represents resources using, for example, JSON or XML formats. -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. +The following sample illustrates how to call a convenience method and access the strongly-typed output model from the service response. -```C# Snippet:ClientResultTReadme -// Create a client -string? key = Environment.GetEnvironmentVariable("MAPS_API_KEY"); -ApiKeyCredential credential = new(key!); +```C# Snippet:ReadmeClientResultT MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential); -// Call a service method, which returns ClientResult +// Call a convenience method, which returns ClientResult IPAddress ipAddress = IPAddress.Parse("2001:4898:80e8:b::189"); ClientResult result = await client.GetCountryCodeAsync(ipAddress); -// ClientResult has two members: -// -// (1) A Value property to access the strongly-typed output +// Access the output model from the service response. IPAddressCountryPair value = result.Value; Console.WriteLine($"Country is {value.CountryRegion.IsoCode}."); +``` + +If needed, callers can obtain the details of the HTTP response by calling the result's `GetRawResponse` method. -// (2) A GetRawResponse method for accessing the details of the HTTP response +```C# Snippet:ReadmeGetRawResponse +// Access the HTTP response details. PipelineResponse response = result.GetRawResponse(); Console.WriteLine($"Response status code: '{response.Status}'."); diff --git a/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs b/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs index 6f1a7d06b944..d300c8221096 100644 --- a/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs +++ b/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs @@ -18,23 +18,24 @@ public class ServiceMethodSamples [Ignore("Used for README")] public async Task ClientResultTReadme() { - #region Snippet:ClientResultTReadme // Create a client string? key = Environment.GetEnvironmentVariable("MAPS_API_KEY"); ApiKeyCredential credential = new(key!); + + #region Snippet:ReadmeClientResultT MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential); - // Call a service method, which returns ClientResult + // Call a convenience method, which returns ClientResult IPAddress ipAddress = IPAddress.Parse("2001:4898:80e8:b::189"); ClientResult result = await client.GetCountryCodeAsync(ipAddress); - // ClientResult has two members: - // - // (1) A Value property to access the strongly-typed output + // Access the output model from the service response. IPAddressCountryPair value = result.Value; Console.WriteLine($"Country is {value.CountryRegion.IsoCode}."); + #endregion - // (2) A GetRawResponse method for accessing the details of the HTTP response + #region Snippet:ReadmeGetRawResponse + // Access the HTTP response details. PipelineResponse response = result.GetRawResponse(); Console.WriteLine($"Response status code: '{response.Status}'."); From c9fc7d0c9ac0f55ae37c2df115d2195d29bd28a0 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Thu, 7 Mar 2024 14:07:35 -0800 Subject: [PATCH 29/40] more updates; add dotnet-api slug --- eng/common/scripts/Test-SampleMetadata.ps1 | 1 + sdk/core/System.ClientModel/README.md | 4 +++- sdk/core/System.ClientModel/samples/ClientImplementation.md | 6 ++++++ sdk/core/System.ClientModel/samples/README.md | 2 +- sdk/core/System.ClientModel/samples/ServiceMethods.md | 4 ++-- 5 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 sdk/core/System.ClientModel/samples/ClientImplementation.md diff --git a/eng/common/scripts/Test-SampleMetadata.ps1 b/eng/common/scripts/Test-SampleMetadata.ps1 index 4a0000220fde..9e50fa1dce03 100644 --- a/eng/common/scripts/Test-SampleMetadata.ps1 +++ b/eng/common/scripts/Test-SampleMetadata.ps1 @@ -330,6 +330,7 @@ begin { "blazor-webassembly", "common-data-service", "customer-voice", + "dotnet-api", "dotnet-core", "dotnet-standard", "document-intelligence", diff --git a/sdk/core/System.ClientModel/README.md b/sdk/core/System.ClientModel/README.md index e4ad3abf59ae..e078d357ac58 100644 --- a/sdk/core/System.ClientModel/README.md +++ b/sdk/core/System.ClientModel/README.md @@ -120,6 +120,8 @@ public class SampleClient } ``` +For more information on authoring clients, see [Client implementation samples](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ClientImplementation.md). + ### Reading and writing model content to HTTP messages 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` and `IJsonModel` 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 [Send a message using the ClientPipeline](#send-a-message-using-clientpipeline). The following sample shows a minimal example of what a persistable model implementation might look like. @@ -180,7 +182,7 @@ Service clients have methods that are used to call cloud services to invoke serv **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 the strongly-typed output model from the service response. +The following sample illustrates how to call a convenience method and access the output model created from the service response. ```C# Snippet:ReadmeClientResultT MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential); diff --git a/sdk/core/System.ClientModel/samples/ClientImplementation.md b/sdk/core/System.ClientModel/samples/ClientImplementation.md new file mode 100644 index 000000000000..185065d0f99e --- /dev/null +++ b/sdk/core/System.ClientModel/samples/ClientImplementation.md @@ -0,0 +1,6 @@ +# System.ClientModel-based client implementation samples + +## Introduction + +`System.ClientModel`-based clients, or **service clients** are built using types provided in the `System.ClientModel` library. + diff --git a/sdk/core/System.ClientModel/samples/README.md b/sdk/core/System.ClientModel/samples/README.md index 6df2237fdda1..01901f59ed48 100644 --- a/sdk/core/System.ClientModel/samples/README.md +++ b/sdk/core/System.ClientModel/samples/README.md @@ -3,7 +3,7 @@ page_type: sample languages: - csharp products: -- dotnet-core +- dotnet-api name: System.ClientModel samples for .NET description: Samples for the System.ClientModel library --- diff --git a/sdk/core/System.ClientModel/samples/ServiceMethods.md b/sdk/core/System.ClientModel/samples/ServiceMethods.md index 1fbbd1893e7f..9127fa23dddf 100644 --- a/sdk/core/System.ClientModel/samples/ServiceMethods.md +++ b/sdk/core/System.ClientModel/samples/ServiceMethods.md @@ -1,4 +1,4 @@ -# System.ClientModel-based client service methods +# System.ClientModel-based client service method samples ## Introduction @@ -14,7 +14,7 @@ In service clients, there are two ways to expose the schematized body in the req **Convenience methods** provide a convenient way to invoke a service operation. They are service methods that take a strongly-typed model representing schematized data sent to the service as input, and return a strongly-typed model representing the payload from the service response as output. Having strongly-typed models that represent service concepts provides a layer of convenience over working with the raw payload format. This is because these models unify the client user experience when cloud services differ in payload formats. That is, a client-user can learn the patterns for strongly-typed models that `System.ClientModel`-based clients provide, and use them together without having to reason about whether a cloud service represents resources using, for example, JSON or XML formats. -The following sample illustrates how to call a convenience method and access the strongly-typed output model from the service response. +The following sample illustrates how to call a convenience method and access the output model created from the service response. ```C# Snippet:ReadmeClientResultT MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential); From f459730ac323f0b52ce8d1bc40539e89f81cc6df Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Thu, 7 Mar 2024 16:44:15 -0800 Subject: [PATCH 30/40] Add sample showing response classifier --- .../samples/ClientImplementation.md | 143 ++++++++++++++++++ .../tests/Samples/PipelineSamples.cs | 15 ++ 2 files changed, 158 insertions(+) diff --git a/sdk/core/System.ClientModel/samples/ClientImplementation.md b/sdk/core/System.ClientModel/samples/ClientImplementation.md index 185065d0f99e..b9f5099f1135 100644 --- a/sdk/core/System.ClientModel/samples/ClientImplementation.md +++ b/sdk/core/System.ClientModel/samples/ClientImplementation.md @@ -4,3 +4,146 @@ `System.ClientModel`-based clients, or **service clients** are built using types provided in the `System.ClientModel` library. +## Basic client implementation + +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; + + // Constructor takes service endpoint, credential used to authenticate + // with the service, and options for configuring the client pipeline. + public SampleClient(Uri endpoint, ApiKeyCredential credential, SampleClientOptions? options = default) + { + // Default options are used if none are passed by the client's user. + options ??= new SampleClientOptions(); + + _endpoint = endpoint; + _credential = credential; + + // Authentication policy instance is created from the user-provided + // credential and service authentication scheme. + ApiKeyAuthenticationPolicy authenticationPolicy = ApiKeyAuthenticationPolicy.CreateBearerAuthorizationPolicy(credential); + + // Pipeline is created from user-provided options and policies + // specific to the service client implementation. + _pipeline = ClientPipeline.Create(options, + perCallPolicies: ReadOnlySpan.Empty, + perTryPolicies: new PipelinePolicy[] { authenticationPolicy }, + beforeTransportPolicies: ReadOnlySpan.Empty); + } + + // Service method takes an input model representing a service resource + // and returns `ClientResult` holding an output model representing + // the value returned in the service response. + public ClientResult UpdateResource(SampleResource resource) + { + // Create a message that can be sent via the client pipeline. + PipelineMessage message = _pipeline.CreateMessage(); + + // Modify the request as needed to invoke the service operation. + 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"); + + // Add request body content that will be written using methods + // defined by the model's implementation of the IJsonModel interface. + request.Content = BinaryContent.Create(resource); + + // Send the message. + _pipeline.Send(message); + + // Obtain the response from the message Response property. + // The PipelineTransport ensures that the Response value is set + // so that every policy in the pipeline can access the property. + PipelineResponse response = message.Response!; + + // If the response is considered an error response, throw an + // exception that exposes the response details. + if (response.IsError) + { + throw new ClientResultException(response); + } + + // Read the content from the response body and create an instance of + // a model from it, to include in the type returned by this method. + SampleResource updated = ModelReaderWriter.Read(response.Content)!; + + // Return a ClientResult holding the model instance and the HTTP + // response details. + 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 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` and `IJsonModel` 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. + +```C# Snippet:ReadmeSampleModel +public class SampleResource : IJsonModel +{ + public SampleResource(string id) + { + Id = id; + } + + public string Id { get; init; } + + SampleResource IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) + => FromJson(reader); + + SampleResource IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) + => FromJson(new Utf8JsonReader(data)); + + string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) + => options.Format; + + void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) + => ToJson(writer); + + BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) + => ModelReaderWriter.Write(this, options); + + // Write the model JSON that will populate the HTTP request content. + private void ToJson(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + writer.WritePropertyName("id"); + writer.WriteStringValue(Id); + writer.WriteEndObject(); + } + + // Read the JSON response content and create a model instance from it. + private static SampleResource FromJson(Utf8JsonReader reader) + { + reader.Read(); // start object + reader.Read(); // property name + reader.Read(); // id value + + return new SampleResource(reader.GetString()!); + } +} +``` + +### Modifying error classification + +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` to send and receive messages sets the response's `IsError` property. To classify the response, the transport uses the `PipelineMessageClassifier` value on the `PipelineMessage.ResponseClassifier` property. 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). + +By default, the transport will set `IsError` to `true` for responses with an 4xx or 5xx HTTP status code. Clients can override the default behavior by setting the `ResponseClassifier` property on `PipelineMessage` when a request is created in a service method. Typically, a client will create a classifier that sets `response.IsError` to `false` only when the response status code is one that the service API definition has listed as a success response for a given service operation. This type of status code-based classifier can be created using `PipelineMessageClassifier.Create` factory method and passing the list of success status codes as shown in the sample below. + +```C# Snippet:ClientStatusCodeClassifier +// Create a message that can be sent via the client pipeline. +PipelineMessage message = _pipeline.CreateMessage(); + +// Set a classifier that will categorize only responses with status codes +// indicating success for the service operation as non-error responses. +message.ResponseClassifier = PipelineMessageClassifier.Create(stackalloc ushort[] { 200, 202 }); +``` + +Client authors can also create types derived from `PipelineMessageClassifier` to provide custom implementations that classify a response based on any value available from `PipelineMessage`. diff --git a/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs b/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs index 760594018b25..e87052e5c94e 100644 --- a/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs +++ b/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs @@ -40,6 +40,21 @@ public void CanReadAndWriteSampleResource() Assert.AreEqual("123", persistableModelResource.Id); } + [Test] + public void ClientStatusCodeClassifier() + { + ClientPipeline _pipeline = ClientPipeline.Create(); + + #region Snippet:ClientStatusCodeClassifier + // Create a message that can be sent via the client pipeline. + PipelineMessage message = _pipeline.CreateMessage(); + + // Set a classifier that will categorize only responses with status codes + // indicating success for the service operation as non-error responses. + message.ResponseClassifier = PipelineMessageClassifier.Create(stackalloc ushort[] { 200, 202 }); + #endregion + } + #region Snippet:ReadmeSampleClient public class SampleClient { From 3c75fa9cf9d44c2e6df2532c8c2b7c552f6ebf30 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Thu, 7 Mar 2024 17:02:09 -0800 Subject: [PATCH 31/40] updates: --- .../System.ClientModel/samples/ClientImplementation.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sdk/core/System.ClientModel/samples/ClientImplementation.md b/sdk/core/System.ClientModel/samples/ClientImplementation.md index b9f5099f1135..c86bd592c29c 100644 --- a/sdk/core/System.ClientModel/samples/ClientImplementation.md +++ b/sdk/core/System.ClientModel/samples/ClientImplementation.md @@ -2,7 +2,7 @@ ## Introduction -`System.ClientModel`-based clients, or **service clients** are built using types provided in the `System.ClientModel` library. +`System.ClientModel`-based clients, or **service clients**, are built using types provided in the `System.ClientModel` library. ## Basic client implementation @@ -131,11 +131,11 @@ public class SampleResource : IJsonModel } ``` -### Modifying error classification +### Configuring error response classification -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` to send and receive messages sets the response's `IsError` property. To classify the response, the transport uses the `PipelineMessageClassifier` value on the `PipelineMessage.ResponseClassifier` property. 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). +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). -By default, the transport will set `IsError` to `true` for responses with an 4xx or 5xx HTTP status code. Clients can override the default behavior by setting the `ResponseClassifier` property on `PipelineMessage` when a request is created in a service method. Typically, a client will create a classifier that sets `response.IsError` to `false` only when the response status code is one that the service API definition has listed as a success response for a given service operation. This type of status code-based classifier can be created using `PipelineMessageClassifier.Create` factory method and passing the list of success status codes as shown in the sample below. +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 an `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. ```C# Snippet:ClientStatusCodeClassifier // Create a message that can be sent via the client pipeline. @@ -146,4 +146,4 @@ PipelineMessage message = _pipeline.CreateMessage(); message.ResponseClassifier = PipelineMessageClassifier.Create(stackalloc ushort[] { 200, 202 }); ``` -Client authors can also create types derived from `PipelineMessageClassifier` to provide custom implementations that classify a response based on any value available from `PipelineMessage`. +Client authors can also create custom classifiers derived from `PipelineMessageClassifier`. From 80401054ad17d97504e3ec615caef1cfd160623e Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Fri, 8 Mar 2024 09:34:16 -0800 Subject: [PATCH 32/40] reference error response configuration sample from README --- sdk/core/System.ClientModel/README.md | 4 +++- sdk/core/System.ClientModel/samples/ClientImplementation.md | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/sdk/core/System.ClientModel/README.md b/sdk/core/System.ClientModel/README.md index e078d357ac58..93ea9a266552 100644 --- a/sdk/core/System.ClientModel/README.md +++ b/sdk/core/System.ClientModel/README.md @@ -230,6 +230,8 @@ catch (ClientResultException e) when (e.Status == 404) } ``` +Whether or not a response is considered an error by the client is determined by the `PipelineMessageClassifier` held by a message when it is sent through the client pipeline. For more information on how client authors can customize error classification, see [Configuring error response classification samples](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ClientImplementation.md#configuring-error-response-classification). + ### 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`. @@ -250,7 +252,7 @@ For more information on client configuration, see [Client configuration samples] ### Customizing HTTP requests -Service clients expose low-level [protocol methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#protocol-methods) that allow callers to customize 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 allows an client user to pass a `CancellationToken` to the method. +Service clients expose low-level [protocol methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#protocol-methods) that allow callers to customize 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 allows a client user to pass a `CancellationToken` to the method. ```C# Snippet:RequestOptionsReadme // Create RequestOptions instance diff --git a/sdk/core/System.ClientModel/samples/ClientImplementation.md b/sdk/core/System.ClientModel/samples/ClientImplementation.md index c86bd592c29c..cca72b6c9893 100644 --- a/sdk/core/System.ClientModel/samples/ClientImplementation.md +++ b/sdk/core/System.ClientModel/samples/ClientImplementation.md @@ -135,15 +135,15 @@ public class SampleResource : IJsonModel 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). -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 an `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. +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. ```C# Snippet:ClientStatusCodeClassifier // Create a message that can be sent via the client pipeline. PipelineMessage message = _pipeline.CreateMessage(); // Set a classifier that will categorize only responses with status codes -// indicating success for the service operation as non-error responses. +// that indicate success for the service operation as non-error responses. message.ResponseClassifier = PipelineMessageClassifier.Create(stackalloc ushort[] { 200, 202 }); ``` -Client authors can also create custom classifiers derived from `PipelineMessageClassifier`. +Client authors can also customize classifier logic by creating a custom classifier type derived from `PipelineMessageClassifier`. From a49b27bf7fa7c5c02d61e77da3155abcacf8b413 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Fri, 8 Mar 2024 09:45:25 -0800 Subject: [PATCH 33/40] update samples README --- sdk/core/System.ClientModel/samples/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sdk/core/System.ClientModel/samples/README.md b/sdk/core/System.ClientModel/samples/README.md index 01901f59ed48..fd510000cfbb 100644 --- a/sdk/core/System.ClientModel/samples/README.md +++ b/sdk/core/System.ClientModel/samples/README.md @@ -10,5 +10,7 @@ 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) +- [Implementing service clients](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ClientImplementation.md) +- [Configuring service clients](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/Configuration.md) +- [Client service methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md) +- [Reading and writing client models](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ModelReaderWriter.md) From 5f00c79f6273c9cd3974a66d0128e783240a44ca Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Fri, 8 Mar 2024 10:42:39 -0800 Subject: [PATCH 34/40] update md files --- sdk/core/System.ClientModel/samples/ClientImplementation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/core/System.ClientModel/samples/ClientImplementation.md b/sdk/core/System.ClientModel/samples/ClientImplementation.md index cca72b6c9893..76f466aaf5b8 100644 --- a/sdk/core/System.ClientModel/samples/ClientImplementation.md +++ b/sdk/core/System.ClientModel/samples/ClientImplementation.md @@ -142,7 +142,7 @@ To classify the response, the transport uses the `PipelineMessageClassifier` val PipelineMessage message = _pipeline.CreateMessage(); // Set a classifier that will categorize only responses with status codes -// that indicate success for the service operation as non-error responses. +// indicating success for the service operation as non-error responses. message.ResponseClassifier = PipelineMessageClassifier.Create(stackalloc ushort[] { 200, 202 }); ``` From 49896a623cd7c0a7fc25a7ad0e584283fbae38cb Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Fri, 8 Mar 2024 10:53:29 -0800 Subject: [PATCH 35/40] show creation of BinaryContent from model in RequestOptions sample --- sdk/core/System.ClientModel/README.md | 16 +++++++++++----- .../System.ClientModel/samples/ServiceMethods.md | 16 +++++++++++----- .../tests/Samples/ServiceMethodSamples.cs | 16 +++++++++++----- .../TestClients/MapsClient/CountryRegion.cs | 2 +- 4 files changed, 34 insertions(+), 16 deletions(-) diff --git a/sdk/core/System.ClientModel/README.md b/sdk/core/System.ClientModel/README.md index 93ea9a266552..da6d5afb4ce1 100644 --- a/sdk/core/System.ClientModel/README.md +++ b/sdk/core/System.ClientModel/README.md @@ -255,17 +255,23 @@ For more information on client configuration, see [Client configuration samples] Service clients expose low-level [protocol methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#protocol-methods) that allow callers to customize 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 allows a client user to pass a `CancellationToken` to the method. ```C# Snippet:RequestOptionsReadme -// Create RequestOptions instance +// Create RequestOptions instance. RequestOptions options = new(); -// Set CancellationToken +// Set the CancellationToken. options.CancellationToken = cancellationToken; -// Add a header to the request +// Add a header to the request. options.AddHeader("CustomHeader", "CustomHeaderValue"); -// Call protocol method to pass RequestOptions -ClientResult output = await client.GetCountryCodeAsync(ipAddress.ToString(), options); +// Create an instance of a model that implements the IJsonModel interface. +CountryRegion region = new("US"); + +// Create BinaryContent from the input model. +BinaryContent content = BinaryContent.Create(region); + +// Call the protocol method, passing the content and options. +ClientResult output = await client.AddCountryCodeAsync(content, options); ``` 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). diff --git a/sdk/core/System.ClientModel/samples/ServiceMethods.md b/sdk/core/System.ClientModel/samples/ServiceMethods.md index 9127fa23dddf..608c9d396326 100644 --- a/sdk/core/System.ClientModel/samples/ServiceMethods.md +++ b/sdk/core/System.ClientModel/samples/ServiceMethods.md @@ -79,17 +79,23 @@ Console.WriteLine($"Code for added country is '{isoCode}'."); Protocol methods take an optional `RequestOptions` parameter. `RequestOptions` can be used to modify various aspects of the HTTP 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. ```C# Snippet:RequestOptionsReadme -// Create RequestOptions instance +// Create RequestOptions instance. RequestOptions options = new(); -// Set CancellationToken +// Set the CancellationToken. options.CancellationToken = cancellationToken; -// Add a header to the request +// Add a header to the request. options.AddHeader("CustomHeader", "CustomHeaderValue"); -// Call protocol method to pass RequestOptions -ClientResult output = await client.GetCountryCodeAsync(ipAddress.ToString(), options); +// Create an instance of a model that implements the IJsonModel interface. +CountryRegion region = new("US"); + +// Create BinaryContent from the input model. +BinaryContent content = BinaryContent.Create(region); + +// Call the protocol method, passing the content and options. +ClientResult output = await client.AddCountryCodeAsync(content, options); ``` ## Handling exceptions diff --git a/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs b/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs index d300c8221096..30cf0cfce856 100644 --- a/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs +++ b/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs @@ -88,17 +88,23 @@ public async Task RequestOptionsReadme() IPAddress ipAddress = IPAddress.Parse("2001:4898:80e8:b::189"); #region Snippet:RequestOptionsReadme - // Create RequestOptions instance + // Create RequestOptions instance. RequestOptions options = new(); - // Set CancellationToken + // Set the CancellationToken. options.CancellationToken = cancellationToken; - // Add a header to the request + // Add a header to the request. options.AddHeader("CustomHeader", "CustomHeaderValue"); - // Call protocol method to pass RequestOptions - ClientResult output = await client.GetCountryCodeAsync(ipAddress.ToString(), options); + // Create an instance of a model that implements the IJsonModel interface. + CountryRegion region = new("US"); + + // Create BinaryContent from the input model. + BinaryContent content = BinaryContent.Create(region); + + // Call the protocol method, passing the content and options. + ClientResult output = await client.AddCountryCodeAsync(content, options); #endregion } catch (ClientResultException e) diff --git a/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/CountryRegion.cs b/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/CountryRegion.cs index 7f98d74b6376..12c92dc3c4d6 100644 --- a/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/CountryRegion.cs +++ b/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/CountryRegion.cs @@ -9,7 +9,7 @@ namespace Maps; public class CountryRegion : IJsonModel { - internal CountryRegion(string isoCode) + public CountryRegion(string isoCode) { IsoCode = isoCode; } From 81e2e2049c2f11af07993d0bc9d95d685da8fcd1 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Fri, 8 Mar 2024 11:33:02 -0800 Subject: [PATCH 36/40] add examples of different way to create BinaryContent --- sdk/core/System.ClientModel/README.md | 60 ++++++++++++++- .../samples/ServiceMethods.md | 59 +++++++++------ .../tests/Samples/ServiceMethodSamples.cs | 74 ++++++++++++++++++- 3 files changed, 164 insertions(+), 29 deletions(-) diff --git a/sdk/core/System.ClientModel/README.md b/sdk/core/System.ClientModel/README.md index da6d5afb4ce1..57e21fc44c16 100644 --- a/sdk/core/System.ClientModel/README.md +++ b/sdk/core/System.ClientModel/README.md @@ -271,14 +271,21 @@ CountryRegion region = new("US"); BinaryContent content = BinaryContent.Create(region); // Call the protocol method, passing the content and options. -ClientResult output = await client.AddCountryCodeAsync(content, options); +ClientResult result = await client.AddCountryCodeAsync(content, options); ``` 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). ### Provide request content -In service clients' [protocol methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#protocol-methods), users pass the request content as an input parameter to the method as an instance of `BinaryContent`. The following example shows how to create a `BinaryContent` instance to pass to a protocol method. +In service clients' [protocol methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#protocol-methods), users pass the request content as a `BinaryContent` parameter. There are a variety of ways to create a `BinaryContent` instance: + +1. From `BinaryData`, which can be created from a string, a stream, an object, or from a byte array containing the serialized UTF-8 bytes +1. From a model type that implements the `IPersistableModel` or `IJsonModel` interfaces. + +The following examples illustrate some of the different ways to create `BinaryContent` and pass it to a protocol method. + +#### From a string literal ```C# Snippet:ServiceMethodsProtocolMethod // Create a BinaryData instance from a JSON string literal. @@ -293,7 +300,7 @@ BinaryData input = BinaryData.FromString(""" // Create a BinaryContent instance to set as the HTTP request content. BinaryContent requestContent = BinaryContent.Create(input); -// Call the protocol method +// Call the protocol method. ClientResult result = await client.AddCountryCodeAsync(requestContent); // Obtain the output response content from the returned ClientResult. @@ -308,6 +315,53 @@ string isoCode = outputAsJson.RootElement Console.WriteLine($"Code for added country is '{isoCode}'."); ``` +#### From an anonymous type + +```C# Snippet:ServiceMethodsBinaryContentAnonymous +// Create a BinaryData instance from an anonymous object representing +// the JSON the service expects for the service operation. +BinaryData input = BinaryData.FromObjectAsJson(new +{ + countryRegion = new + { + isoCode = "US" + } +}); + +// Create the BinaryContent instance to pass to the protocol method. +BinaryContent content = BinaryContent.Create(input); + +// Call the protocol method. +ClientResult result = await client.AddCountryCodeAsync(content); +``` + +#### From an input stream + +```C# Snippet:ServiceMethodsBinaryContentStream +// Create a BinaryData instance from a file stream +FileStream stream = File.OpenRead(@"c:\path\to\file.txt"); +BinaryData input = BinaryData.FromStream(stream); + +// Create the BinaryContent instance to pass to the protocol method. +BinaryContent content = BinaryContent.Create(input); + +// Call the protocol method. +ClientResult result = await client.AddCountryCodeAsync(content); +``` + +#### From a model type + +```C# Snippet:ServiceMethodsBinaryContentModel +// Create an instance of a model that implements the IJsonModel interface. +CountryRegion region = new("US"); + +// Create BinaryContent from the input model. +BinaryContent content = BinaryContent.Create(region); + +// Call the protocol method, passing the content and options. +ClientResult result = await client.AddCountryCodeAsync(content); +``` + ## Troubleshooting You can troubleshoot service clients by inspecting the result of any `ClientResultException` thrown from a client's service method. diff --git a/sdk/core/System.ClientModel/samples/ServiceMethods.md b/sdk/core/System.ClientModel/samples/ServiceMethods.md index 608c9d396326..97bc397edb4e 100644 --- a/sdk/core/System.ClientModel/samples/ServiceMethods.md +++ b/sdk/core/System.ClientModel/samples/ServiceMethods.md @@ -46,7 +46,40 @@ foreach (KeyValuePair header in response.Headers) In contrast to convenience methods, **protocol methods** are service methods that provide very little convenience over the raw HTTP APIs a cloud service exposes. They represent request and response message bodies using types that are very thin layers over raw JSON/binary/other formats. Users of client protocol methods must reference a service's API documentation directly, rather than relying on the client to provide developer conveniences via strongly-typing service schemas. -The following sample illustrates how to call a protocol method, including creating the request payload and accessing the details of the HTTP response. +### Customizing HTTP requests + +Service clients expose low-level [protocol methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#protocol-methods) that allow callers to customize 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 allows a client user to pass a `CancellationToken` to the method. + +```C# Snippet:RequestOptionsReadme +// Create RequestOptions instance. +RequestOptions options = new(); + +// Set the CancellationToken. +options.CancellationToken = cancellationToken; + +// Add a header to the request. +options.AddHeader("CustomHeader", "CustomHeaderValue"); + +// Create an instance of a model that implements the IJsonModel interface. +CountryRegion region = new("US"); + +// Create BinaryContent from the input model. +BinaryContent content = BinaryContent.Create(region); + +// Call the protocol method, passing the content and options. +ClientResult result = await client.AddCountryCodeAsync(content, options); +``` + +### Provide request content + +In service clients' [protocol methods](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#protocol-methods), users pass the request content as a `BinaryContent` parameter. There are a variety of ways to create a `BinaryContent` instance: + +1. From `BinaryData`, which can be created from a string, a stream, an object, or from a byte array containing the serialized UTF-8 bytes +1. From a model type that implements the `IPersistableModel` or `IJsonModel` interfaces. + +The following examples illustrate some of the different ways to create `BinaryContent` and pass it to a protocol method. + +#### From a string literal ```C# Snippet:ServiceMethodsProtocolMethod // Create a BinaryData instance from a JSON string literal. @@ -61,7 +94,7 @@ BinaryData input = BinaryData.FromString(""" // Create a BinaryContent instance to set as the HTTP request content. BinaryContent requestContent = BinaryContent.Create(input); -// Call the protocol method +// Call the protocol method. ClientResult result = await client.AddCountryCodeAsync(requestContent); // Obtain the output response content from the returned ClientResult. @@ -76,28 +109,6 @@ string isoCode = outputAsJson.RootElement Console.WriteLine($"Code for added country is '{isoCode}'."); ``` -Protocol methods take an optional `RequestOptions` parameter. `RequestOptions` can be used to modify various aspects of the HTTP 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. - -```C# Snippet:RequestOptionsReadme -// Create RequestOptions instance. -RequestOptions options = new(); - -// Set the CancellationToken. -options.CancellationToken = cancellationToken; - -// Add a header to the request. -options.AddHeader("CustomHeader", "CustomHeaderValue"); - -// Create an instance of a model that implements the IJsonModel interface. -CountryRegion region = new("US"); - -// Create BinaryContent from the input model. -BinaryContent content = BinaryContent.Create(region); - -// Call the protocol method, passing the content and options. -ClientResult output = await client.AddCountryCodeAsync(content, options); -``` - ## Handling exceptions 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. diff --git a/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs b/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs index 30cf0cfce856..7479ec1f4c0f 100644 --- a/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs +++ b/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs @@ -3,11 +3,13 @@ using System.ClientModel.Primitives; using System.Collections.Generic; +using System.IO; using System.Net; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Maps; +using Microsoft.Extensions.Options; using NUnit.Framework; namespace System.ClientModel.Tests.Samples; @@ -104,7 +106,7 @@ public async Task RequestOptionsReadme() BinaryContent content = BinaryContent.Create(region); // Call the protocol method, passing the content and options. - ClientResult output = await client.AddCountryCodeAsync(content, options); + ClientResult result = await client.AddCountryCodeAsync(content, options); #endregion } catch (ClientResultException e) @@ -142,7 +144,7 @@ public async Task ServiceMethodsProtocolMethod() // Create a BinaryContent instance to set as the HTTP request content. BinaryContent requestContent = BinaryContent.Create(input); - // Call the protocol method + // Call the protocol method. ClientResult result = await client.AddCountryCodeAsync(requestContent); // Obtain the output response content from the returned ClientResult. @@ -163,4 +165,72 @@ public async Task ServiceMethodsProtocolMethod() Assert.Fail($"Error: Response status code: '{e.Status}'"); } } + + [Test] + [Ignore("Used for README")] + public async Task ServiceMethodsBinaryContentAnonymous() + { + string? key = Environment.GetEnvironmentVariable("MAPS_API_KEY"); + ApiKeyCredential credential = new ApiKeyCredential(key!); + MapsClient client = new MapsClient(new Uri("https://atlas.microsoft.com"), credential); + + #region Snippet:ServiceMethodsBinaryContentAnonymous + // Create a BinaryData instance from an anonymous object representing + // the JSON the service expects for the service operation. + BinaryData input = BinaryData.FromObjectAsJson(new + { + countryRegion = new + { + isoCode = "US" + } + }); + + // Create the BinaryContent instance to pass to the protocol method. + BinaryContent content = BinaryContent.Create(input); + + // Call the protocol method. + ClientResult result = await client.AddCountryCodeAsync(content); + #endregion + } + + [Test] + [Ignore("Used for README")] + public async Task ServiceMethodsBinaryContentStream() + { + string? key = Environment.GetEnvironmentVariable("MAPS_API_KEY"); + ApiKeyCredential credential = new ApiKeyCredential(key!); + MapsClient client = new MapsClient(new Uri("https://atlas.microsoft.com"), credential); + + #region Snippet:ServiceMethodsBinaryContentStream + // Create a BinaryData instance from a file stream + FileStream stream = File.OpenRead(@"c:\path\to\file.txt"); + BinaryData input = BinaryData.FromStream(stream); + + // Create the BinaryContent instance to pass to the protocol method. + BinaryContent content = BinaryContent.Create(input); + + // Call the protocol method. + ClientResult result = await client.AddCountryCodeAsync(content); + #endregion + } + + [Test] + [Ignore("Used for README")] + public async Task ServiceMethodsBinaryContentModel() + { + string? key = Environment.GetEnvironmentVariable("MAPS_API_KEY"); + ApiKeyCredential credential = new ApiKeyCredential(key!); + MapsClient client = new MapsClient(new Uri("https://atlas.microsoft.com"), credential); + + #region Snippet:ServiceMethodsBinaryContentModel + // Create an instance of a model that implements the IJsonModel interface. + CountryRegion region = new("US"); + + // Create BinaryContent from the input model. + BinaryContent content = BinaryContent.Create(region); + + // Call the protocol method, passing the content and options. + ClientResult result = await client.AddCountryCodeAsync(content); + #endregion + } } From 3aed2729e2036ecc18d192eb01d67d012458b156 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Fri, 8 Mar 2024 12:23:17 -0800 Subject: [PATCH 37/40] show protocol method implementation and message.Apply(options) --- sdk/core/System.ClientModel/README.md | 2 +- .../samples/ClientImplementation.md | 130 +++++++++++++++- ...ples.cs => ClientImplementationSamples.cs} | 4 +- .../TestClients/MapsClient/MapsClient.cs | 139 +++++++++++++++--- 4 files changed, 252 insertions(+), 23 deletions(-) rename sdk/core/System.ClientModel/tests/Samples/{PipelineSamples.cs => ClientImplementationSamples.cs} (98%) diff --git a/sdk/core/System.ClientModel/README.md b/sdk/core/System.ClientModel/README.md index 57e21fc44c16..34e5412406d0 100644 --- a/sdk/core/System.ClientModel/README.md +++ b/sdk/core/System.ClientModel/README.md @@ -113,7 +113,7 @@ public class SampleClient // a model from it, to include in the type returned by this method. SampleResource updated = ModelReaderWriter.Read(response.Content)!; - // Return a ClientResult holding the model instance and the HTTP + // Return a ClientResult holding the model instance and the HTTP // response details. return ClientResult.FromValue(updated, response); } diff --git a/sdk/core/System.ClientModel/samples/ClientImplementation.md b/sdk/core/System.ClientModel/samples/ClientImplementation.md index 76f466aaf5b8..1643439951e2 100644 --- a/sdk/core/System.ClientModel/samples/ClientImplementation.md +++ b/sdk/core/System.ClientModel/samples/ClientImplementation.md @@ -74,7 +74,7 @@ public class SampleClient // a model from it, to include in the type returned by this method. SampleResource updated = ModelReaderWriter.Read(response.Content)!; - // Return a ClientResult holding the model instance and the HTTP + // Return a ClientResult holding the model instance and the HTTP // response details. return ClientResult.FromValue(updated, response); } @@ -131,6 +131,134 @@ public class SampleResource : IJsonModel } ``` +### Implementing protocol methods + +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, it takes a model type parameter as input and returns a `ClientResult` holding an output model type. + +In contrast, client [protocol method](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#protocol-methods), takes `BinaryContent` as input and an optional `RequestOptions` parameter holding options used to configure the request and the client pipeline for the duration of the service call. The following sample shows a minimal example of what it might look like to implement both convenience methods and protocol methods on a client. + +#### Convenience method + +The client's convenience method converts model types to request content and from response content, and calls through to the protocol method. + +```C# Snippet:ClientImplementationConvenienceMethod +// A convenience method takes a model type and returns a ClientResult. +public virtual async Task> AddCountryCodeAsync(CountryRegion country) +{ + // Validate input parameters. + if (country is null) + throw new ArgumentNullException(nameof(country)); + + // Create the request body content to pass to the protocol method. + // The content will be written using methods defined by the model's + // implementation of the IJsonModel interface. + BinaryContent content = BinaryContent.Create(country); + + // Call the protocol method. + ClientResult result = await AddCountryCodeAsync(content).ConfigureAwait(false); + + // Obtain the response from the ClientResult. + PipelineResponse response = result.GetRawResponse(); + + // Create an instance of the model type representing the service response. + CountryRegion value = ModelReaderWriter.Read(response.Content)!; + + // Create the instance of ClientResult to return from the convenience method. + return ClientResult.FromValue(value, response); +} +``` + +#### Protocol method + +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. + +```C# Snippet:ClientImplementationProtocolMethod +// Protocol method. +public virtual async Task AddCountryCodeAsync(BinaryContent country, RequestOptions? options = null) +{ + // Validate input parameters. + if (country is null) + throw new ArgumentNullException(nameof(country)); + + // Use default RequestOptions if none were provided by the caller. + options ??= new RequestOptions(); + + // Create a message that can be sent through the client pipeline. + using PipelineMessage message = CreateAddCountryCodeRequest(country, options); + + // Send the message. + _pipeline.Send(message); + + // Obtain the response from the message Response property. + // The PipelineTransport ensures that the Response value is set + // so that every policy in the pipeline can access the property. + PipelineResponse response = message.Response!; + + // If the response is considered an error response, throw an + // exception that exposes the response details. The protocol method + // caller can change the default exception behavior by setting error + // options differently. + if (response.IsError && options.ErrorOptions == ClientErrorBehaviors.Default) + { + throw await ClientResultException.CreateAsync(response).ConfigureAwait(false); + } + + // Return a ClientResult holding the HTTP response details. + return ClientResult.FromResponse(response); +} +``` + +For more information on response classification, see [Configuring error response classification](#configuring-error-response-classification). + +#### Request creation helper method + +```C# Snippet:ClientImplementationRequestHelper +private PipelineMessage CreateAddCountryCodeRequest(BinaryContent content, RequestOptions options) +{ + // Create an instance of the message to send through the pipeline. + PipelineMessage message = _pipeline.CreateMessage(); + + // Set a response classifier with known success codes for the operation. + message.ResponseClassifier = PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); + + // Obtain the request to set its values directly. + PipelineRequest request = message.Request; + + // Set the request method. + request.Method = "PATCH"; + + // Create the URI to set on the request. + UriBuilder uriBuilder = new(_endpoint.ToString()); + + StringBuilder path = new(); + path.Append("countries"); + uriBuilder.Path += path.ToString(); + + StringBuilder query = new(); + query.Append("api-version="); + query.Append(Uri.EscapeDataString(_apiVersion)); + uriBuilder.Query = query.ToString(); + + // Set the URI on the request. + request.Uri = uriBuilder.Uri; + + // Add headers to the request. + request.Headers.Add("Accept", "application/json"); + + // Set the request content. + request.Content = content; + + // Apply the RequestOptions to the method. This sets properties on the + // message that the client pipeline will use during processing, + // including CancellationToken and any headers provided via calls to + // AddHeader or SetHeader. It also stores policies that will be added + // to the client pipeline before the first policy processes the message. + message.Apply(options); + + return message; +} +``` + ### Configuring error response classification 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). diff --git a/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs b/sdk/core/System.ClientModel/tests/Samples/ClientImplementationSamples.cs similarity index 98% rename from sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs rename to sdk/core/System.ClientModel/tests/Samples/ClientImplementationSamples.cs index e87052e5c94e..ee7542439c40 100644 --- a/sdk/core/System.ClientModel/tests/Samples/PipelineSamples.cs +++ b/sdk/core/System.ClientModel/tests/Samples/ClientImplementationSamples.cs @@ -11,7 +11,7 @@ namespace System.ClientModel.Tests.Samples; -public class PipelineSamples +public class ClientImplementationSamples { [Test] public void CanReadAndWriteSampleResource() @@ -121,7 +121,7 @@ public ClientResult UpdateResource(SampleResource resource) // a model from it, to include in the type returned by this method. SampleResource updated = ModelReaderWriter.Read(response.Content)!; - // Return a ClientResult holding the model instance and the HTTP + // Return a ClientResult holding the model instance and the HTTP // response details. return ClientResult.FromValue(updated, response); } diff --git a/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClient.cs b/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClient.cs index 9e159cbd0529..be75f90fa93d 100644 --- a/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClient.cs +++ b/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClient.cs @@ -19,8 +19,10 @@ public class MapsClient public MapsClient(Uri endpoint, ApiKeyCredential credential, MapsClientOptions? options = default) { - if (endpoint is null) throw new ArgumentNullException(nameof(endpoint)); - if (credential is null) throw new ArgumentNullException(nameof(credential)); + if (endpoint is null) + throw new ArgumentNullException(nameof(endpoint)); + if (credential is null) + throw new ArgumentNullException(nameof(credential)); options ??= new MapsClientOptions(); @@ -71,7 +73,8 @@ public virtual async Task GetCountryCodeAsync(string ipAddress, Re public virtual ClientResult GetCountryCode(IPAddress ipAddress) { - if (ipAddress is null) throw new ArgumentNullException(nameof(ipAddress)); + if (ipAddress is null) + throw new ArgumentNullException(nameof(ipAddress)); ClientResult result = GetCountryCode(ipAddress.ToString()); @@ -83,7 +86,8 @@ public virtual ClientResult GetCountryCode(IPAddress ipAdd public virtual ClientResult GetCountryCode(string ipAddress, RequestOptions? options = null) { - if (ipAddress is null) throw new ArgumentNullException(nameof(ipAddress)); + if (ipAddress is null) + throw new ArgumentNullException(nameof(ipAddress)); options ??= new RequestOptions(); @@ -101,55 +105,152 @@ public virtual ClientResult GetCountryCode(string ipAddress, RequestOptions? opt return ClientResult.FromResponse(response); } - public virtual async Task AddCountryCodeAsync(BinaryContent content, RequestOptions? options = null) + private PipelineMessage CreateGetLocationRequest(string ipAddress, RequestOptions options) { - // Fake method used to illlustrate creating input content in ClientModel samples. - // No such operation exists on the Azure Maps service, and this operation implementation - // will not succeed against a live service. + // Create an instance of the message to send through the pipeline. + PipelineMessage message = _pipeline.CreateMessage(); + + // Set a response classifier with known success codes for the operation. + message.ResponseClassifier = PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); - if (content is null) throw new ArgumentNullException(nameof(content)); + // Set request values needed by the service. + PipelineRequest request = message.Request; + request.Method = "GET"; + + UriBuilder uriBuilder = new(_endpoint.ToString()); + StringBuilder path = new(); + path.Append("geolocation/ip"); + path.Append("/json"); + uriBuilder.Path += path.ToString(); + + StringBuilder query = new(); + query.Append("api-version="); + query.Append(Uri.EscapeDataString(_apiVersion)); + query.Append("&ip="); + query.Append(Uri.EscapeDataString(ipAddress)); + uriBuilder.Query = query.ToString(); + + request.Uri = uriBuilder.Uri; + + request.Headers.Add("Accept", "application/json"); + + message.Apply(options); + + return message; + } + + // Fake method used to illlustrate creating input content in ClientModel + // samples. No such operation exists on the Azure Maps service, and this + // operation implementation will not succeed against a live service. + + #region Snippet:ClientImplementationConvenienceMethod + // A convenience method takes a model type and returns a ClientResult. + public virtual async Task> AddCountryCodeAsync(CountryRegion country) + { + // Validate input parameters. + if (country is null) + throw new ArgumentNullException(nameof(country)); + + // Create the request body content to pass to the protocol method. + // The content will be written using methods defined by the model's + // implementation of the IJsonModel interface. + BinaryContent content = BinaryContent.Create(country); + + // Call the protocol method. + ClientResult result = await AddCountryCodeAsync(content).ConfigureAwait(false); + + // Obtain the response from the ClientResult. + PipelineResponse response = result.GetRawResponse(); + + // Create an instance of the model type representing the service response. + CountryRegion value = ModelReaderWriter.Read(response.Content)!; + + // Create the instance of ClientResult to return from the convenience method. + return ClientResult.FromValue(value, response); + } + #endregion + + #region Snippet:ClientImplementationProtocolMethod + // Protocol method. + public virtual async Task AddCountryCodeAsync(BinaryContent country, RequestOptions? options = null) + { + // Validate input parameters. + if (country is null) + throw new ArgumentNullException(nameof(country)); + + // Use default RequestOptions if none were provided by the caller. options ??= new RequestOptions(); - using PipelineMessage message = CreateGetLocationRequest(string.Empty, options); + // Create a message that can be sent through the client pipeline. + using PipelineMessage message = CreateAddCountryCodeRequest(country, options); - await _pipeline.SendAsync(message).ConfigureAwait(false); + // Send the message. + _pipeline.Send(message); + // Obtain the response from the message Response property. + // The PipelineTransport ensures that the Response value is set + // so that every policy in the pipeline can access the property. PipelineResponse response = message.Response!; + // If the response is considered an error response, throw an + // exception that exposes the response details. The protocol method + // caller can change the default exception behavior by setting error + // options differently. + if (response.IsError && options.ErrorOptions == ClientErrorBehaviors.Default) + { + throw await ClientResultException.CreateAsync(response).ConfigureAwait(false); + } + + // Return a ClientResult holding the HTTP response details. return ClientResult.FromResponse(response); } + #endregion - private PipelineMessage CreateGetLocationRequest(string ipAddress, RequestOptions options) + #region Snippet:ClientImplementationRequestHelper + private PipelineMessage CreateAddCountryCodeRequest(BinaryContent content, RequestOptions options) { + // Create an instance of the message to send through the pipeline. PipelineMessage message = _pipeline.CreateMessage(); + + // Set a response classifier with known success codes for the operation. message.ResponseClassifier = PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); + // Obtain the request to set its values directly. PipelineRequest request = message.Request; - request.Method = "GET"; + // Set the request method. + request.Method = "PATCH"; + + // Create the URI to set on the request. UriBuilder uriBuilder = new(_endpoint.ToString()); StringBuilder path = new(); - path.Append("geolocation/ip"); - path.Append("/json"); + path.Append("countries"); uriBuilder.Path += path.ToString(); StringBuilder query = new(); query.Append("api-version="); query.Append(Uri.EscapeDataString(_apiVersion)); - query.Append("&ip="); - query.Append(Uri.EscapeDataString(ipAddress)); uriBuilder.Query = query.ToString(); + // Set the URI on the request. request.Uri = uriBuilder.Uri; + // Add headers to the request. request.Headers.Add("Accept", "application/json"); - // Note: due to addition of SetHeader method on RequestOptions, we now - // need to apply options at the end of the CreateRequest routine. + // Set the request content. + request.Content = content; + + // Apply the RequestOptions to the method. This sets properties on the + // message that the client pipeline will use during processing, + // including CancellationToken and any headers provided via calls to + // AddHeader or SetHeader. It also stores policies that will be added + // to the client pipeline before the first policy processes the message. message.Apply(options); return message; } + #endregion } From d7a36c70b41c9f6ad279c5f1c262549a7913207e Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Fri, 8 Mar 2024 12:29:42 -0800 Subject: [PATCH 38/40] updates --- .../samples/ClientImplementation.md | 12 ++++++------ .../client/TestClients/MapsClient/MapsClient.cs | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/sdk/core/System.ClientModel/samples/ClientImplementation.md b/sdk/core/System.ClientModel/samples/ClientImplementation.md index 1643439951e2..81f5279eb50a 100644 --- a/sdk/core/System.ClientModel/samples/ClientImplementation.md +++ b/sdk/core/System.ClientModel/samples/ClientImplementation.md @@ -133,9 +133,9 @@ public class SampleResource : IJsonModel ### Implementing protocol methods -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, it takes a model type parameter as input and returns a `ClientResult` holding an output model type. +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 as a single service method that takes a model type parameter as input and returns a `ClientResult` holding an output model type. -In contrast, client [protocol method](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/samples/ServiceMethods.md#protocol-methods), takes `BinaryContent` as input and an optional `RequestOptions` parameter holding options used to configure the request and the client pipeline for the duration of the service call. The following sample shows a minimal example of what it might look like to implement both convenience methods and protocol methods on a client. +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. #### Convenience method @@ -146,8 +146,7 @@ The client's convenience method converts model types to request content and from public virtual async Task> AddCountryCodeAsync(CountryRegion country) { // Validate input parameters. - if (country is null) - throw new ArgumentNullException(nameof(country)); + if (country is null) throw new ArgumentNullException(nameof(country)); // Create the request body content to pass to the protocol method. // The content will be written using methods defined by the model's @@ -177,8 +176,7 @@ The client's protocol method calls a helper method to create the message and req public virtual async Task AddCountryCodeAsync(BinaryContent country, RequestOptions? options = null) { // Validate input parameters. - if (country is null) - throw new ArgumentNullException(nameof(country)); + if (country is null) throw new ArgumentNullException(nameof(country)); // Use default RequestOptions if none were provided by the caller. options ??= new RequestOptions(); @@ -200,6 +198,8 @@ public virtual async Task AddCountryCodeAsync(BinaryContent countr // options differently. if (response.IsError && options.ErrorOptions == ClientErrorBehaviors.Default) { + // Use the CreateAsync factory method to create an exception instance + // in an async context. In a sync method, the exception constructor can be used. throw await ClientResultException.CreateAsync(response).ConfigureAwait(false); } diff --git a/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClient.cs b/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClient.cs index be75f90fa93d..323cd9b7da1a 100644 --- a/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClient.cs +++ b/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClient.cs @@ -149,8 +149,7 @@ private PipelineMessage CreateGetLocationRequest(string ipAddress, RequestOption public virtual async Task> AddCountryCodeAsync(CountryRegion country) { // Validate input parameters. - if (country is null) - throw new ArgumentNullException(nameof(country)); + if (country is null) throw new ArgumentNullException(nameof(country)); // Create the request body content to pass to the protocol method. // The content will be written using methods defined by the model's @@ -176,8 +175,7 @@ public virtual async Task> AddCountryCodeAsync(Count public virtual async Task AddCountryCodeAsync(BinaryContent country, RequestOptions? options = null) { // Validate input parameters. - if (country is null) - throw new ArgumentNullException(nameof(country)); + if (country is null) throw new ArgumentNullException(nameof(country)); // Use default RequestOptions if none were provided by the caller. options ??= new RequestOptions(); @@ -199,6 +197,8 @@ public virtual async Task AddCountryCodeAsync(BinaryContent countr // options differently. if (response.IsError && options.ErrorOptions == ClientErrorBehaviors.Default) { + // Use the CreateAsync factory method to create an exception instance + // in an async context. In a sync method, the exception constructor can be used. throw await ClientResultException.CreateAsync(response).ConfigureAwait(false); } From 6c56e7cf5d89e7c0c745899546e5371b8c06f74b Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Fri, 8 Mar 2024 13:49:37 -0800 Subject: [PATCH 39/40] nits --- sdk/core/System.ClientModel/samples/ClientImplementation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/core/System.ClientModel/samples/ClientImplementation.md b/sdk/core/System.ClientModel/samples/ClientImplementation.md index 81f5279eb50a..c6349f6a5ed3 100644 --- a/sdk/core/System.ClientModel/samples/ClientImplementation.md +++ b/sdk/core/System.ClientModel/samples/ClientImplementation.md @@ -133,7 +133,7 @@ public class SampleResource : IJsonModel ### Implementing protocol methods -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 as a single service method that takes a model type parameter as input and returns a `ClientResult` holding an output model type. +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` holding an output model type. 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. From 2719c618a98044c45175fe5e5df2e45f27e9780d Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Fri, 8 Mar 2024 14:00:59 -0800 Subject: [PATCH 40/40] nits --- sdk/core/System.ClientModel/README.md | 2 +- .../samples/ClientImplementation.md | 2 +- .../tests/Samples/ClientImplementationSamples.cs | 2 +- .../tests/Samples/ServiceMethodSamples.cs | 2 -- .../client/TestClients/MapsClient/MapsClient.cs | 12 ++++-------- 5 files changed, 7 insertions(+), 13 deletions(-) diff --git a/sdk/core/System.ClientModel/README.md b/sdk/core/System.ClientModel/README.md index 34e5412406d0..09b64e2d18dc 100644 --- a/sdk/core/System.ClientModel/README.md +++ b/sdk/core/System.ClientModel/README.md @@ -82,7 +82,7 @@ public class SampleClient public ClientResult UpdateResource(SampleResource resource) { // Create a message that can be sent via the client pipeline. - PipelineMessage message = _pipeline.CreateMessage(); + using PipelineMessage message = _pipeline.CreateMessage(); // Modify the request as needed to invoke the service operation. PipelineRequest request = message.Request; diff --git a/sdk/core/System.ClientModel/samples/ClientImplementation.md b/sdk/core/System.ClientModel/samples/ClientImplementation.md index c6349f6a5ed3..405ed90450fe 100644 --- a/sdk/core/System.ClientModel/samples/ClientImplementation.md +++ b/sdk/core/System.ClientModel/samples/ClientImplementation.md @@ -43,7 +43,7 @@ public class SampleClient public ClientResult UpdateResource(SampleResource resource) { // Create a message that can be sent via the client pipeline. - PipelineMessage message = _pipeline.CreateMessage(); + using PipelineMessage message = _pipeline.CreateMessage(); // Modify the request as needed to invoke the service operation. PipelineRequest request = message.Request; diff --git a/sdk/core/System.ClientModel/tests/Samples/ClientImplementationSamples.cs b/sdk/core/System.ClientModel/tests/Samples/ClientImplementationSamples.cs index ee7542439c40..dc0231062d3f 100644 --- a/sdk/core/System.ClientModel/tests/Samples/ClientImplementationSamples.cs +++ b/sdk/core/System.ClientModel/tests/Samples/ClientImplementationSamples.cs @@ -90,7 +90,7 @@ public SampleClient(Uri endpoint, ApiKeyCredential credential, SampleClientOptio public ClientResult UpdateResource(SampleResource resource) { // Create a message that can be sent via the client pipeline. - PipelineMessage message = _pipeline.CreateMessage(); + using PipelineMessage message = _pipeline.CreateMessage(); // Modify the request as needed to invoke the service operation. PipelineRequest request = message.Request; diff --git a/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs b/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs index 7479ec1f4c0f..ad8c4b017ddb 100644 --- a/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs +++ b/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs @@ -9,7 +9,6 @@ using System.Threading; using System.Threading.Tasks; using Maps; -using Microsoft.Extensions.Options; using NUnit.Framework; namespace System.ClientModel.Tests.Samples; @@ -131,7 +130,6 @@ public async Task ServiceMethodsProtocolMethod() { #nullable disable #region Snippet:ServiceMethodsProtocolMethod - // Create a BinaryData instance from a JSON string literal. BinaryData input = BinaryData.FromString(""" { diff --git a/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClient.cs b/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClient.cs index 323cd9b7da1a..ec660d4563bb 100644 --- a/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClient.cs +++ b/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClient.cs @@ -39,8 +39,7 @@ public MapsClient(Uri endpoint, ApiKeyCredential credential, MapsClientOptions? public virtual async Task> GetCountryCodeAsync(IPAddress ipAddress) { - if (ipAddress is null) - throw new ArgumentNullException(nameof(ipAddress)); + if (ipAddress is null) throw new ArgumentNullException(nameof(ipAddress)); ClientResult result = await GetCountryCodeAsync(ipAddress.ToString()).ConfigureAwait(false); @@ -52,8 +51,7 @@ public virtual async Task> GetCountryCodeAsyn public virtual async Task GetCountryCodeAsync(string ipAddress, RequestOptions? options = null) { - if (ipAddress is null) - throw new ArgumentNullException(nameof(ipAddress)); + if (ipAddress is null) throw new ArgumentNullException(nameof(ipAddress)); options ??= new RequestOptions(); @@ -73,8 +71,7 @@ public virtual async Task GetCountryCodeAsync(string ipAddress, Re public virtual ClientResult GetCountryCode(IPAddress ipAddress) { - if (ipAddress is null) - throw new ArgumentNullException(nameof(ipAddress)); + if (ipAddress is null) throw new ArgumentNullException(nameof(ipAddress)); ClientResult result = GetCountryCode(ipAddress.ToString()); @@ -86,8 +83,7 @@ public virtual ClientResult GetCountryCode(IPAddress ipAdd public virtual ClientResult GetCountryCode(string ipAddress, RequestOptions? options = null) { - if (ipAddress is null) - throw new ArgumentNullException(nameof(ipAddress)); + if (ipAddress is null) throw new ArgumentNullException(nameof(ipAddress)); options ??= new RequestOptions();