From 1d8a46c6191a4f26a41a2e34ebeda5cfde476406 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Fri, 8 Mar 2024 14:32:25 -0800 Subject: [PATCH] ClientModel: Add samples for System.ClientModel library (#42369) * Samples - WIP * README updates * Configuration samples * updates before generating snippets * update snippets * readme updates * intermediate backup * updates * fix * updates * nit * nit * fix links * updates from PR feedback * revert engsys file * update product * add sample client implementation * add input model to sample client method * change API key in samples * add inline comments to sample client and change defaults on HttpClient sample * update impressions link * restructure to address PR feedback * nits * nits * nits * small updates from PR feedback * add comment * rework convenience methods section in README * more updates; add dotnet-api slug * Add sample showing response classifier * updates: * reference error response configuration sample from README * update samples README * update md files * show creation of BinaryContent from model in RequestOptions sample * add examples of different way to create BinaryContent * show protocol method implementation and message.Apply(options) * updates * nits * nits --- sdk/core/System.ClientModel/README.md | 365 +++++++++++++++--- .../samples/ClientImplementation.md | 277 +++++++++++++ .../samples/Configuration.md | 98 +++++ .../samples/ModelReaderWriter.md | 24 ++ sdk/core/System.ClientModel/samples/README.md | 16 + .../samples/ServiceMethods.md | 128 ++++++ .../Samples/ClientImplementationSamples.cs | 197 ++++++++++ .../tests/Samples/ConfigurationSamples.cs | 119 ++++++ .../tests/Samples/ModelReaderWriterSamples.cs | 86 +++++ .../tests/Samples/ReadmeModelReaderWriter.cs | 87 ----- .../tests/Samples/ServiceMethodSamples.cs | 234 +++++++++++ .../TestClients/MapsClient/CountryRegion.cs | 69 ++++ .../MapsClient/IPAddressCountryPair.cs | 101 +++++ .../TestClients/MapsClient/MapsClient.cs | 252 ++++++++++++ .../MapsClient/MapsClientOptions.cs | 28 ++ 15 files changed, 1951 insertions(+), 130 deletions(-) create mode 100644 sdk/core/System.ClientModel/samples/ClientImplementation.md create mode 100644 sdk/core/System.ClientModel/samples/Configuration.md create mode 100644 sdk/core/System.ClientModel/samples/ModelReaderWriter.md create mode 100644 sdk/core/System.ClientModel/samples/README.md create mode 100644 sdk/core/System.ClientModel/samples/ServiceMethods.md create mode 100644 sdk/core/System.ClientModel/tests/Samples/ClientImplementationSamples.cs create mode 100644 sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs create mode 100644 sdk/core/System.ClientModel/tests/Samples/ModelReaderWriterSamples.cs delete mode 100644 sdk/core/System.ClientModel/tests/Samples/ReadmeModelReaderWriter.cs create mode 100644 sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.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 c3429ab13d843..09b64e2d18dc0 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 @@ -23,71 +22,351 @@ dotnet add package System.ClientModel None needed for `System.ClientModel`. -### Authenticate the client +## Key concepts -The `System.ClientModel` package provides a `KeyCredential` type for authentication. +`System.ClientModel` contains two 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. -## Key concepts +Types used to author service clients appear in the `System.ClientModel.Primitives` namespace. Key concepts involving these types include: + +- 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`). -The main shared concepts of `System.ClientModel` include: +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: -- 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`). +- Results that provide access to the service response and the HTTP response details (`ClientResult`, `ClientResult`). +- Exceptions that result from failed requests (`ClientResultException`). +- 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 MessagePipeline +### 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. + +```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. + using 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); + } +} +``` + +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. + +```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()!); + } +} +``` + +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 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**, 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](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 the output model created from the service response. + +```C# Snippet:ReadmeClientResultT +MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential); + +// Call a convenience method, which returns ClientResult +IPAddress ipAddress = IPAddress.Parse("2001:4898:80e8:b::189"); +ClientResult result = await client.GetCountryCodeAsync(ipAddress); + +// 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. + +```C# Snippet:ReadmeGetRawResponse +// Access the HTTP response details. +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). -A very basic client implementation might use the following approach: +### Handling exceptions that result from failed requests -```csharp -ApiKeyCredential credential = new ApiKeyCredential(key); -ApiKeyAuthenticationPolicy authenticationPolicy = ApiKeyAuthenticationPolicy.CreateBearerAuthorizationPolicy(credential); -ClientPipeline pipeline = ClientPipeline.Create(pipelineOptions, authenticationPolicy); +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. -PipelineMessage message = pipeline.CreateMessage(); -message.Apply(requestOptions); -message.MessageClassifier = PipelineMessageClassifier.Create(stackalloc ushort[] { 200 }); +```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}'"); +} +``` + +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`. +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](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); +``` -PipelineRequest request = message.Request; -request.Method = "GET"; -request.Uri = new Uri("https://www.example.com/"); -request.Headers.Add("Accept", "application/json"); +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). -pipeline.Send(message); -Console.WriteLine(message.Response.Status); +### 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. +BinaryData input = BinaryData.FromString(""" + { + "countryRegion": { + "isoCode": "US" + }, + } + """); + +// 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(requestContent); + +// 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}'."); +``` + +#### 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); ``` -### Read and write persistable models +#### From an input stream -As a library author you can implement `IPersistableModel` or `IJsonModel` which will give library users the ability to read and write your models. +```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); -Example writing an instance of a model. +// Create the BinaryContent instance to pass to the protocol method. +BinaryContent content = 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(content); ``` -Example reading a model from json +#### 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); -```C# Snippet:Readme_Read_Simple -string json = @"{ - ""x"": 1, - ""y"": 2, - ""z"": 3 -}"; -OutputModel? model = ModelReaderWriter.Read(BinaryData.FromString(json)); +// Call the protocol method, passing the content and options. +ClientResult result = await client.AddCountryCodeAsync(content); ``` ## Troubleshooting -You can troubleshoot `System.ClientModel`-based clients by inspecting the result of any `ClientResultException` thrown from a pipeline's `Send` method. +You can troubleshoot service clients by inspecting the result of any `ClientResultException` thrown from a client's service method. -## Next steps +For more information on client service method errors, see [Handling exceptions that result from failed requests](#handling-exceptions-that-result-from-failed-requests). ## Contributing @@ -97,7 +376,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 diff --git a/sdk/core/System.ClientModel/samples/ClientImplementation.md b/sdk/core/System.ClientModel/samples/ClientImplementation.md new file mode 100644 index 0000000000000..405ed90450fe6 --- /dev/null +++ b/sdk/core/System.ClientModel/samples/ClientImplementation.md @@ -0,0 +1,277 @@ +# System.ClientModel-based client implementation samples + +## Introduction + +`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. + using 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()!); + } +} +``` + +### 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 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. + +#### 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) + { + // 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); + } + + // 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). + +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. +message.ResponseClassifier = PipelineMessageClassifier.Create(stackalloc ushort[] { 200, 202 }); +``` + +Client authors can also customize classifier logic by creating a custom classifier type derived from `PipelineMessageClassifier`. diff --git a/sdk/core/System.ClientModel/samples/Configuration.md b/sdk/core/System.ClientModel/samples/Configuration.md new file mode 100644 index 0000000000000..d9206d140d2df --- /dev/null +++ b/sdk/core/System.ClientModel/samples/Configuration.md @@ -0,0 +1,98 @@ +# System.ClientModel-based client configuration samples + +## Configuring retries + +To modify the retry policy, create a new instance of `ClientRetryPolicy` and set it on the `ClientPipelineOptions` passed to the client constructor. + +By default, clients will retry a request three times using an exponential retry strategy with an initial delay of 0.8 seconds and a maximum delay of one minute. + +```C# Snippet:ConfigurationCustomizeRetries +MapsClientOptions options = new() +{ + RetryPolicy = new ClientRetryPolicy(maxRetries: 5), +}; + +string? key = Environment.GetEnvironmentVariable("MAPS_API_KEY"); +ApiKeyCredential credential = new(key!); +MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential, options); +``` + +## Implement a custom policy + +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 +{ + public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + Stopwatch stopwatch = new(); + stopwatch.Start(); + + await ProcessNextAsync(message, pipeline, currentIndex); + + stopwatch.Stop(); + + Console.WriteLine($"Request to {message.Request.Uri} took {stopwatch.Elapsed}"); + } + + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + Stopwatch stopwatch = new(); + stopwatch.Start(); + + ProcessNext(message, pipeline, currentIndex); + + stopwatch.Stop(); + + Console.WriteLine($"Request to {message.Request.Uri} took {stopwatch.Elapsed}"); + } +} +``` + +## 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. + +```C# Snippet:ConfigurationCustomHttpClient +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() +{ + Transport = new HttpClientPipelineTransport(httpClient) +}; +``` diff --git a/sdk/core/System.ClientModel/samples/ModelReaderWriter.md b/sdk/core/System.ClientModel/samples/ModelReaderWriter.md new file mode 100644 index 0000000000000..78eeadc6f5feb --- /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/README.md b/sdk/core/System.ClientModel/samples/README.md new file mode 100644 index 0000000000000..fd510000cfbb5 --- /dev/null +++ b/sdk/core/System.ClientModel/samples/README.md @@ -0,0 +1,16 @@ +--- +page_type: sample +languages: +- csharp +products: +- dotnet-api +name: System.ClientModel samples for .NET +description: Samples for the System.ClientModel library +--- + +# System.ClientModel Samples + +- [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) diff --git a/sdk/core/System.ClientModel/samples/ServiceMethods.md b/sdk/core/System.ClientModel/samples/ServiceMethods.md new file mode 100644 index 0000000000000..97bc397edb4ec --- /dev/null +++ b/sdk/core/System.ClientModel/samples/ServiceMethods.md @@ -0,0 +1,128 @@ +# System.ClientModel-based client service method samples + +## Introduction + +`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**: + +- **Convenience methods** take strongly-typed models as parameters. These models are C# classes which map to the message body of the REST call. + +- **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 + +**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 output model created from the service response. + +```C# Snippet:ReadmeClientResultT +MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential); + +// Call a convenience method, which returns ClientResult +IPAddress ipAddress = IPAddress.Parse("2001:4898:80e8:b::189"); +ClientResult result = await client.GetCountryCodeAsync(ipAddress); + +// 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. + +```C# Snippet:ReadmeGetRawResponse +// Access the HTTP response details. +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}'."); +} +``` + +## Protocol methods + +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. + +### 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. +BinaryData input = BinaryData.FromString(""" + { + "countryRegion": { + "isoCode": "US" + }, + } + """); + +// 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(requestContent); + +// 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}'."); +``` + +## 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 +{ + 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}'"); +} +``` diff --git a/sdk/core/System.ClientModel/tests/Samples/ClientImplementationSamples.cs b/sdk/core/System.ClientModel/tests/Samples/ClientImplementationSamples.cs new file mode 100644 index 0000000000000..dc0231062d3f3 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/Samples/ClientImplementationSamples.cs @@ -0,0 +1,197 @@ +// 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 ClientImplementationSamples +{ + [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); + } + + [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 + { + 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. + using 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); + } + } + #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); + + // 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()!); + } + } + #endregion +} 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 0000000000000..bfdcb974bb3b0 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/Samples/ConfigurationSamples.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net.Http; +using System.Threading.Tasks; +using Maps; +using NUnit.Framework; + +namespace System.ClientModel.Tests.Samples; + +public class ConfigurationSamples +{ + [Test] + [Ignore("Used for README")] + public void ClientModelConfigurationReadme() + { + #region 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); + + #endregion + } + + [Test] + [Ignore("Used for README")] + public void ConfigurationCustomizeRetries() + { + #region Snippet:ConfigurationCustomizeRetries + + MapsClientOptions options = new() + { + RetryPolicy = new ClientRetryPolicy(maxRetries: 5), + }; + + string? key = Environment.GetEnvironmentVariable("MAPS_API_KEY"); + ApiKeyCredential credential = new(key!); + MapsClient client = new(new Uri("https://atlas.microsoft.com"), credential, options); + + #endregion + } + + [Test] + public void ConfigurationAddPolicies() + { + #region Snippet:ConfigurationAddPerCallPolicy + MapsClientOptions options = new(); + options.AddPolicy(new StopwatchPolicy(), PipelinePosition.PerCall); + #endregion + + #region Snippet:ConfigurationAddPerTryPolicy + options.AddPolicy(new StopwatchPolicy(), PipelinePosition.PerTry); + #endregion + + #region Snippet:ConfigurationAddBeforeTransportPolicy + options.AddPolicy(new StopwatchPolicy(), PipelinePosition.BeforeTransport); + #endregion + } + + #region Snippet:ConfigurationCustomPolicy + public class StopwatchPolicy : PipelinePolicy + { + public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + Stopwatch stopwatch = new(); + stopwatch.Start(); + + await ProcessNextAsync(message, pipeline, currentIndex); + + stopwatch.Stop(); + + Console.WriteLine($"Request to {message.Request.Uri} took {stopwatch.Elapsed}"); + } + + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + Stopwatch stopwatch = new(); + stopwatch.Start(); + + ProcessNext(message, pipeline, currentIndex); + + stopwatch.Stop(); + + Console.WriteLine($"Request to {message.Request.Uri} took {stopwatch.Elapsed}"); + } + } + #endregion + + [Test] + public void ConfigurationCustomHttpClient() + { + #region Snippet:ConfigurationCustomHttpClient + 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() + { + Transport = new HttpClientPipelineTransport(httpClient) + }; + #endregion + } +} diff --git a/sdk/core/System.ClientModel/tests/Samples/ModelReaderWriterSamples.cs b/sdk/core/System.ClientModel/tests/Samples/ModelReaderWriterSamples.cs new file mode 100644 index 0000000000000..774bc2dc6c2cc --- /dev/null +++ b/sdk/core/System.ClientModel/tests/Samples/ModelReaderWriterSamples.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using System.Text.Json; + +namespace System.ClientModel.Tests.Samples; + +internal class ModelReaderWriterSamples +{ + public void Write_Simple() + { + #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 = @"{ + ""x"": 1, + ""y"": 2, + ""z"": 3 + }"; + OutputModel? model = ModelReaderWriter.Read(BinaryData.FromString(json)); + #endregion + } + + private class OutputModel : IJsonModel + { + OutputModel IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } + + OutputModel IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } + + string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } + + void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } + + BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } + } + + private class InputModel : IJsonModel + { + void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } + + InputModel IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } + + BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } + + InputModel IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } + + string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } + } +} diff --git a/sdk/core/System.ClientModel/tests/Samples/ReadmeModelReaderWriter.cs b/sdk/core/System.ClientModel/tests/Samples/ReadmeModelReaderWriter.cs deleted file mode 100644 index b094c68db351b..0000000000000 --- a/sdk/core/System.ClientModel/tests/Samples/ReadmeModelReaderWriter.cs +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.ClientModel.Primitives; -using System.Text.Json; - -namespace System.ClientModel.Tests.Samples -{ - internal class ReadmeModelReaderWriter - { - public void Write_Simple() - { - #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 = @"{ - ""x"": 1, - ""y"": 2, - ""z"": 3 - }"; - OutputModel? model = ModelReaderWriter.Read(BinaryData.FromString(json)); - #endregion - } - - private class OutputModel : IJsonModel - { - OutputModel IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) - { - throw new NotImplementedException(); - } - - OutputModel IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) - { - throw new NotImplementedException(); - } - - string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) - { - throw new NotImplementedException(); - } - - void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) - { - throw new NotImplementedException(); - } - - BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) - { - throw new NotImplementedException(); - } - } - - private class InputModel : IJsonModel - { - void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) - { - throw new NotImplementedException(); - } - - InputModel IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) - { - throw new NotImplementedException(); - } - - BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) - { - throw new NotImplementedException(); - } - - InputModel IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) - { - throw new NotImplementedException(); - } - - string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) - { - throw new NotImplementedException(); - } - } - } -} 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 0000000000000..ad8c4b017ddb8 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/Samples/ServiceMethodSamples.cs @@ -0,0 +1,234 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +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 NUnit.Framework; + +namespace System.ClientModel.Tests.Samples; + +public class ServiceMethodSamples +{ + [Test] + [Ignore("Used for README")] + public async Task 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 convenience method, which returns ClientResult + IPAddress ipAddress = IPAddress.Parse("2001:4898:80e8:b::189"); + ClientResult result = await client.GetCountryCodeAsync(ipAddress); + + // Access the output model from the service response. + IPAddressCountryPair value = result.Value; + Console.WriteLine($"Country is {value.CountryRegion.IsoCode}."); + #endregion + + #region Snippet:ReadmeGetRawResponse + // Access the HTTP response details. + 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] + [Ignore("Used for README")] + public async Task ClientResultExceptionReadme() + { + // Create a client + string? key = Environment.GetEnvironmentVariable("MAPS_API_KEY"); + 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] + [Ignore("Used for README")] + public async Task RequestOptionsReadme() + { + string? key = Environment.GetEnvironmentVariable("MAPS_API_KEY"); + ApiKeyCredential credential = new ApiKeyCredential(key!); + + MapsClient client = new MapsClient(new Uri("https://atlas.microsoft.com"), credential); + + // CancellationToken used for snippet - doesn't need a real value. + CancellationToken cancellationToken = CancellationToken.None; + + try + { + IPAddress ipAddress = IPAddress.Parse("2001:4898:80e8:b::189"); + + #region 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); + #endregion + } + catch (ClientResultException e) + { + Assert.Fail($"Error: Response status code: '{e.Status}'"); + } + } + + [Test] + [Ignore("Used for README")] + public async Task ServiceMethodsProtocolMethod() + { + string? key = Environment.GetEnvironmentVariable("MAPS_API_KEY"); + 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" + }, + } + """); + + // 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(requestContent); + + // 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}'"); + } + } + + [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 + } +} 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 0000000000000..12c92dc3c4d6f --- /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 +{ + public 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 (JsonProperty 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 JsonDocument document = JsonDocument.ParseValue(ref reader); + return FromJson(document.RootElement); + } + + public CountryRegion Create(BinaryData data, ModelReaderWriterOptions options) + { + using JsonDocument 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 0000000000000..6f0e87f1c3cb8 --- /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 (JsonProperty 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 JsonDocument document = JsonDocument.Parse(response.Content); + return FromJson(document.RootElement); + } + + public string GetFormatFromOptions(ModelReaderWriterOptions options) + => "J"; + + public IPAddressCountryPair Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) + { + using JsonDocument document = JsonDocument.ParseValue(ref reader); + return FromJson(document.RootElement); + } + + public IPAddressCountryPair Create(BinaryData data, ModelReaderWriterOptions options) + { + using JsonDocument 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 0000000000000..ec660d4563bb5 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/client/TestClients/MapsClient/MapsClient.cs @@ -0,0 +1,252 @@ +// 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) + { + // 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 }); + + // 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(); + + // 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) + { + // 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); + } + + // Return a ClientResult holding the HTTP response details. + return ClientResult.FromResponse(response); + } + #endregion + + #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; + + // 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; + } + #endregion +} 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 0000000000000..ba4a287ec2bd3 --- /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() + }; + } +}