API Implementation
This section describes guidelines for implementing Azure SDK client libraries. Please note that some of these guidelines are automatically enforced by code generation tools.
The Service Client
TODO: add a brief mention of the approach to implementing service clients.
Service Methods
TODO: Briefly introduce that service methods are implemented via an HttpPipeline
instance. Mention that much of this is done for you using code generation.
Using HttpPipeline
The following example shows a typical way of using HttpPipeline
to implement a service call method. The HttpPipeline
will handle common HTTP requirements such as the user agent, logging, distributed tracing, retries, and proxy configuration.
public virtual async Task<Response<ConfigurationSetting>> AddAsync(ConfigurationSetting setting, CancellationToken cancellationToken = default)
{
if (setting == null) throw new ArgumentNullException(nameof(setting));
... // validate other preconditions
// Use HttpPipeline _pipeline filed of the client type to create new HTTP request
using (Request request = _pipeline.CreateRequest()) {
// specify HTTP request line
request.Method = RequestMethod.Put;
request.Uri.Reset(_endpoint);
request.Uri.AppendPath(KvRoute, escape: false);
requast.Uri.AppendPath(key);
// add headers
request.Headers.Add(IfNoneMatchWildcard);
request.Headers.Add(MediaTypeKeyValueApplicationHeader);
request.Headers.Add(HttpHeader.Common.JsonContentType);
request.Headers.Add(HttpHeader.Common.CreateContentLength(content.Length));
// add content
ReadOnlyMemory<byte> content = Serialize(setting);
request.Content = HttpPipelineRequestContent.Create(content);
// send the request
var response = await Pipeline.SendRequestAsync(request).ConfigureAwait(false);
if (response.Status == 200) {
// deserialize content
Response<ConfigurationSetting> result = await CreateResponse(response, cancellationToken);
}
else
{
throw await response.CreateRequestFailedExceptionAsync(message);
}
}
}
TODO: do we still want this code sample now that we’re encouraging moving to Code Gen?
For a more complete example, see the configuration client implementation.
Using HttpPipelinePolicy
The HTTP pipeline includes a number of policies that all requests pass through. Examples of policies include setting required headers, authentication, generating a request ID, and implementing proxy authentication. HttpPipelinePolicy
is the base type of all policies (plugins) of the HttpPipeline
. This section describes guidelines for designing custom policies.
✅ DO inherit from HttpPipelinePolicy
if the policy implementation calls asynchronous APIs.
See an example here.
✅ DO inherit from HttpPipelineSynchronousPolicy
if the policy implementation calls only synchronous APIs.
See an example here.
✅ DO ensure ProcessAsync
and Process
methods are thread safe.
HttpMessage
, Request
, and Response
don’t have to be thread-safe.
Service Method Parameters
Parameter validation
In addition to general parameter validation guidelines:
⛔️ DO NOT define your own parameter validation class.
Use the Argument
class in Azure.Core or even “extend” it (it’s a partial class included as source) with project-specific argument assertions.
Just add the following to your project to include it:
<!-- Import Azure.Core properties if not already imported. -->
<Import Project="$(MSBuildThisFileDirectory)..\..\..\core\Azure.Core\src\Azure.Core.props" />
<ItemGroup>
<Compile Include="$(AzureCoreSharedSources)Argument.cs" />
</ItemGroup>
See remarks on the Argument
class for more detail.
Supporting Types
Serialization
JSON Serialization
✅ DO use System.Text.Json
package to write and read JSON content.
☑️ YOU SHOULD use Utf8JsonWriter
to write JSON payloads:
var json = new Utf8JsonWriter(writer);
json.WriteStartObject();
json.WriteString("value", setting.Value);
json.WriteString("content_type", setting.ContentType);
json.WriteEndObject();
json.Flush();
written = (int)json.BytesWritten;
☑️ YOU SHOULD use JsonDocument
to read JSON payloads:
using (JsonDocument json = await JsonDocument.ParseAsync(content, default, cancellationToken).ConfigureAwait(false))
{
JsonElement root = json.RootElement;
var setting = new ConfigurationSetting();
// required property
setting.Key = root.GetProperty("key").GetString();
// optional property
if (root.TryGetProperty("last_modified", out var lastModified)) {
if(lastModified.Type == JsonValueType.Null) {
setting.LastModified = null;
}
else {
setting.LastModified = DateTimeOffset.Parse(lastModified.GetString());
}
}
...
return setting;
}
☑️ YOU SHOULD consider using Utf8JsonReader
to read JSON payloads.
Utf8JsonReader
is faster than JsonDocument
but much less convenient to use.
✅ DO make your serialization and deserialization code version resilient.
Optional JSON properties should be deserialized into nullable model properties.
Enumeration-like structures
As described in general enumeration guidelines, you should use enum
types whenever passing or deserializing a well-known set of values to or from the service.
There may be times, however, where a readonly struct
is necessary to capture an extensible value from the service even if well-known values are defined,
or to pass back to the service those same or other user-supplied values:
- The value is retrieved and deserialized from service, and may contain a value not supported by the client library.
- The value is roundtripped: the value is retrieved and deserialized from the service, and may later be serialized and sent back to the server.
In those cases, you should define a structure that:
- Is
readonly
. - Implements
IEquatable<T>
that compares only the string value. - Stores only the string value.
- Defines
static
properties with well-known values. - Defines equality, inequality, and an implicit cast-from-string operators.
- Overrides
Equals
,GetHashCode
, andToString
methods. Equals(object)
andGetHashCode
should be attributed withEditorBrowsable(EditorBrowsableState.Never)
.
Type of value comparison should be selected on per-service basis, if the service is inconsistent with how string values are returned the case-insensitive comparison is allowed.
The following example implements these requirements and should be used as a template:
/// <summary>
/// An algorithm used for encryption and decryption
/// </summary>
public partial readonly struct EncryptionAlgorithm : IEquatable<EncryptionAlgorithm>
{
internal const string Rsa15Value = "RSA1_5";
internal const string RsaOaepValue = "RSA-OAEP";
internal const string RsaOaep256Value = "RSA-OAEP-256";
private readonly string _value;
/// <summary>
/// Initializes a new instance of the <see cref="EncryptionAlgorithm"/> structure.
/// </summary>
/// <param name="value">The string value of the instance.</param>
public EncryptionAlgorithm(string value)
{
_value = value ?? throw new ArgumentNullException(nameof(value));
}
/// <summary>
/// RSA1_5
/// </summary>
public static EncryptionAlgorithm Rsa15 { get; } = new EncryptionAlgorithm(Rsa15Value);
/// <summary>
/// RSA-OAEP
/// </summary>
public static EncryptionAlgorithm RsaOaep { get; } = new EncryptionAlgorithm(RsaOaepValue);
/// <summary>
/// RSA-OAEP256
/// </summary>
public static EncryptionAlgorithm RsaOaep256 { get; } = new EncryptionAlgorithm(RsaOaep256Value);
/// <summary>
/// Determines if two <see cref="EncryptionAlgorithm"/> values are the same.
/// </summary>
/// <param name="left">The first <see cref="EncryptionAlgorithm"/> to compare.</param>
/// <param name="right">The second <see cref="EncryptionAlgorithm"/> to compare.</param>
/// <returns>True if <paramref name="left"/> and <paramref name="right"/> are the same; otherwise, false.</returns>
public static bool operator ==(EncryptionAlgorithm left, EncryptionAlgorithm right) => left.Equals(right);
/// <summary>
/// Determines if two <see cref="EncryptionAlgorithm"/> values are different.
/// </summary>
/// <param name="left">The first <see cref="EncryptionAlgorithm"/> to compare.</param>
/// <param name="right">The second <see cref="EncryptionAlgorithm"/> to compare.</param>
/// <returns>True if <paramref name="left"/> and <paramref name="right"/> are different; otherwise, false.</returns>
public static bool operator !=(EncryptionAlgorithm left, EncryptionAlgorithm right) => !left.Equals(right);
/// <summary>
/// Converts a string to a <see cref="EncryptionAlgorithm"/>.
/// </summary>
/// <param name="value">The string value to convert.</param>
public static implicit operator EncryptionAlgorithm(string value) => new EncryptionAlgorithm(value);
/// <inheritdoc/>
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals(object obj) => obj is EncryptionAlgorithm other && Equals(other);
/// <inheritdoc/>
public bool Equals(EncryptionAlgorithm other) => string.Equals(_value, other._value, StringComparison.Ordinal);
/// <inheritdoc/>
[EditorBrowsable(EditorBrowsableState.Never)]
public override int GetHashCode() => _value?.GetHashCode() ?? 0;
/// <inheritdoc/>
public override string ToString() => _value;
// Attach extra information to to structs using factory methods:
internal RSAEncryptionPadding GetRsaEncryptionPadding() => _value switch
{
Rsa15Value => RSAEncryptionPadding.Pkcs1,
RsaOaepValue => RSAEncryptionPadding.OaepSHA1,
RsaOaep256Value => RSAEncryptionPadding.OaepSHA256,
_ => null,
};
}
Constant values for enumeration-like structures
☑️ YOU SHOULD define a nested static class named Values
with public constants if and only if extensible enum values must be used as constant expressions, for example:
- Attribute values
- Default parameter values
- Switch statements and expressions
public partial readonly struct EncryptionAlgorithm : IEquatable<EncryptionAlgorithm>
{
/// <summary>
/// The values of all declared <see cref="EncryptionAlgorithm"/> properties as string constants.
/// </summary>
public static class Values
{
/// <summary>
/// RSA1_5
/// </summary>
public const string Rsa15 = EncryptionAlgorithm.Rsa15Value;
/// <summary>
/// RSA-OAEP
/// </summary>
public const string RsaOaep = EncryptionAlgorithm.RsaOaepValue;
/// <summary>
/// RSA-OAEP256
/// </summary>
public const string RsaOaep256 = EncryptionAlgorithm.RsaOaep256Value;
}
}
✅ DO define tests to ensure extensible enum properties and defined Values
constants declare the same names and define the same values. See here for an example.
Using Azure Core Types
Implementing Subtypes of Operation<T>
Subtypes of Operation<T>
are returned from service client methods invoking long running operations (LROs), as described in Methods Invoking Long Running Operations.
✅ DO check the value of HasCompleted
in subclass implementations of UpdateStatus
and UpdateStatusAsync
and immediately return the result of GetRawResponse
if it is true.
public override Response UpdateStatus(
CancellationToken cancellationToken = default) =>
UpdateStatusAsync(false, cancellationToken).EnsureCompleted();
public override async ValueTask<Response> UpdateStatusAsync(
CancellationToken cancellationToken = default) =>
await UpdateStatusAsync(true, cancellationToken).ConfigureAwait(false);
private async Task<Response> UpdateStatusAsync(bool async, CancellationToken cancellationToken)
{
// Short-circuit when already completed (which improves mocking scenarios that won't have a client).
if (HasCompleted)
{
return GetRawResponse();
}
// some service operation call here, which the above check avoids if the operation has already completed.
...
✅ DO throw from methods on Operation<T>
subclasses in the following scenarios.
- If an underlying service operation call from
UpdateStatus
,WaitForCompletion
, orWaitForCompletionAsync
throws, re-throwRequestFailedException
or its subtype. - If the operation completes with a non-success result, throw
RequestFailedException
or its subtype fromUpdateStatus
,WaitForCompletion
, orWaitForCompletionAsync
.- Include any relevant error state information in the exception details.
private async ValueTask<Response> UpdateStatusAsync(bool async, CancellationToken cancellationToken)
{
...
try
{
Response<ExampleOperationResult> update = async
? await _serviceClient.GetExampleResultAsync(new Guid(Id), cancellationToken).ConfigureAwait(false)
: _serviceClient.GetExampleResult(new Guid(Id), cancellationToken);
_response = update.GetRawResponse();
if (update.Value.Status == OperationStatus.Succeeded)
{
// we need to first assign a value and then mark the operation as completed to avoid race conditions
_value = ConvertValue(update.Value.ExampleResult.PageResults, update.Value.ExampleResult.ReadResults);
_hasCompleted = true;
}
else if (update.Value.Status == OperationStatus.Failed)
{
_requestFailedException = await ClientCommon
.CreateExceptionForFailedOperationAsync(async, _diagnostics, _response, update.Value.AnalyzeResult.Errors)
.ConfigureAwait(false);
_hasCompleted = true;
// the operation didn't throw, but it completed with a non-success result.
throw _requestFailedException;
}
}
catch (Exception e)
{
scope.Failed(e);
throw;
}
}
return GetRawResponse();
}
- If the
Value
property is evaluated after the operation failed (HasValue
is false andHasCompleted
is true) throw the same exception as the one thrown when the operation failed.
public override FooResult Value
{
get
{
if (HasCompleted && !HasValue)
#pragma warning disable CA1065 // Do not raise exceptions in unexpected locations
// throw the exception captured when the operation failed.
throw _requestFailedException;
#pragma warning restore CA1065 // Do not raise exceptions in unexpected locations
else
return OperationHelpers.GetValue(ref _value);
}
}
- If the
Value
property is evaluated before the operation is complete (HasCompleted
is false) throwInvalidOperationException
.- The exception message should be: “The operation has not yet completed.”
// start the operation.
ExampleOperation operation = await client.StartExampleOperationAsync(someParameter);
if (!operation.HasCompleted)
{
// attempt to get the Value property before the operation has completed.
ExampleOperationResult myResult = operation.Value; //throws InvalidOperationException.
}
else if (!operation.HasValue)
{
// attempt to get the Value property after the operation has completed unsuccessfully.
ExampleOperationResult myResult = operation.Value; //throws RequestFailedException.
}
SDK Feature Implementation
Configuration
TODO: Add discussion on configuration environment variables to parallel that of other languages
Logging
Request logging will be done automatically by the HttpPipeline
. If a client library needs to add custom logging, follow the same guidelines and mechanisms as the pipeline logging mechanism. If a client library wants to do custom logging, the designer of the library must ensure that the logging mechanism is pluggable in the same way as the HttpPipeline
logging policy.
✅ DO follow the logging section of the Azure SDK General Guidelines if logging directly (as opposed to through the HttpPipeline
).
EventSource
Azure SDKs use EventSource library as a logging mechanism.
Most Azure SDKs with client methods that make a service request and deserialize the response won’t need to define EventSource of their own as Azure.Core
would provide HTTP request logging, retry logging etc. and the failures would be surfaced to customers as Exceptions
.
You should only log information that is not possible to infer from the result returned by the client.
For example:
- ✔ GOOD
Azure.Identity
logging which credential type was selected when usingDefaultAzureCredential
- it’s impossible to determine from the returnedAccessToken
with credential was used to retrieve it. - ✔ GOOD
Azure.Security.KeyVault
logging whether an encryption operation was performed locally or remotely - the client can decide to run encryption locally or remotely depending on key properties, the decision is invisible to consumer. - ❌BAD Logging properties of the request or response -
Azure-Core
already does this as part of the pipeline. - ❌BAD Logging exception details before throwing - customers would be able to decide if they want the exception to be logged or not.
✅ DO use EventSource library to produce diagnostic events.
✅ DO use the package name with EventSource
suffix for the class name (i.e. . AzureCoreEventSource
for Azure.Core
package).
✅ DO use the AzureEventSource
as the base class.
✅ DO follow the logging guidelines when implementing an [Package]EventSource
.
✅ DO have a single [Package]EventSource
type per library.
✅ DO define [Package]EventSource
class as internal sealed
.
✅ DO define and use a singleton instance of [Package]EventSource
:
✅ DO set [Package]EventSource
name to package name replacing .
with -
(i.e. . Azure-Core
for Azure.Core
package)
✅ DO have Message
property of EventAttribute set for all events.
✅ DO treat [Package]EventSource
name, guid, event id and parameters as public API and follow the appropriate versioning rules.
☑️ YOU SHOULD check IsEnabled property before doing expensive work (formatting parameters, calling ToString, allocations etc.)
⛔️ DO NOT define events with Exception
parameters as they are not supported by EventSource
.
✅ DO have a test that asserts [Package]EventSource
name, guid and generates the manifest to verify that event source is correctly defined.
✅ DO test that expected events are produced when appropriate. TestEventListener
class can be used to collect events. Make sure you mark the test as [NonParallelizable]
.
☑️ YOU SHOULD avoid logging exceptions that are going to get thrown to user code anyway.
☑️ YOU SHOULD be aware of event size limit of 64K.
[Test]
public void MatchesNameAndGuid()
{
// Arrange & Act
var eventSourceType = typeof(AzureCoreEventSource);
// Assert
Assert.NotNull(eventSourceType);
Assert.AreEqual("Azure-Core", EventSource.GetName(eventSourceType));
Assert.AreEqual(Guid.Parse("1015ab6c-4cd8-53d6-aec3-9b937011fa95"), EventSource.GetGuid(eventSourceType));
Assert.IsNotEmpty(EventSource.GenerateManifest(eventSourceType, "assemblyPathToIncludeInManifest"));
}
Sample EventSource
declaration:
[EventSource(Name = EventSourceName)]
internal sealed class AzureCoreEventSource : AzureEventSource
{
private const string EventSourceName = "Azure-Core";
// Having event ids defined as const makes it easy to keep track of them
private const int MessageSentEventId = 0;
private const int ClientClosingEventId = 1;
public static AzureCoreEventSource Shared { get; } = new AzureCoreEventSource();
private AzureCoreEventSource() : base(EventSourceName) { }
[NonEvent]
public void MessageSent(Guid clientId, string messageBody)
{
// We are calling Guid.ToString make sure anyone is listening before spending resources
if (IsEnabled(EventLevel.Informational, EventKeywords.None))
{
MessageSent(clientId.ToString("N"), messageBody);
}
}
// In this example we don't do any expensive parameter formatting so we can avoid extra method and IsEnabled check
[Event(ClientClosingEventId, Level = EventLevel.Informational, Message = "Client {0} is closing the connection.")]
public void ClientClosing(string clientId)
{
WriteEvent(ClientClosingEventId, clientId);
}
[Event(MessageSentEventId, Level = EventLevel.Informational, Message = "Client {0} sent message with body '{1}'")]
private void MessageSent(string clientId, string messageBody)
{
WriteEvent(MessageSentEventId, clientId, messageBody);
}
}
Distributed Tracing
TODO: Add guidance for distributed tracing implementation
Telemetry
TODO: Add guidance regarding user agent strings
Ecosystem Integration
Integration with ASP.NET Core
All Azure client libraries ship with a set of extension methods that provide integration with ASP.NET Core applications by registering clients with DependencyInjection container, flowing Azure SDK logs to ASP.NET Core logging subsystem and providing ability to use configuration subsystem for client configuration (for more examples see https://github.com/Azure/azure-sdk-for-net/tree/main/sdk/core/Microsoft.Extensions.Azure)
✅ DO provide a single *ClientBuilderExtensions
class for every Azure SDK client library that contains client types. Name of the type should use the same prefix as the *ClientOptions
class used across the library. For example: SecretClientBuilderExtensions
, BlobClientBuilderExtensions
✅ DO use Microsoft.Extensions.Azure
as a namespace.
✅ DO provide integration extension methods for every top level client type users are expected to start with in the main namespace. Do not include integration extension methods for secondary clients, child clients, or clients in advanced namespaces.
✅ DO name extension methods as Add[ClientName]
for example. Add AddSecretsClient
, AddBlobServiceClient
.
✅ DO provide an overload for every set of constructor parameters.
✅ DO provide extension method for IAzureClientFactoryBuilder
interface for constructors that don’t take TokenCredentials
. Extension method should take same set of parameters as constructor and call into builder.RegisterClientFactory
Sample implementation:
public static IAzureClientBuilder<ConfigurationClient, ConfigurationClientOptions> AddConfigurationClient<TBuilder>(this TBuilder builder, string connectionString)
where TBuilder : IAzureClientFactoryBuilder
{
return builder.RegisterClientFactory<ConfigurationClient, ConfigurationClientOptions>(options => new ConfigurationClient(connectionString, options));
}
✅ DO provide extension method for IAzureClientFactoryBuilderWithCredential
interface for constructors that take TokenCredentials
. Extension method should take same set of parameters as constructor except the TokenCredential
and call into builder.RegisterClientFactory
overload that provides the token credential as part of factory lambda.
Sample implementation:
public static IAzureClientBuilder<SecretClient, SecretClientOptions> AddSecretClient<TBuilder>(this TBuilder builder, Uri vaultUri)
where TBuilder : IAzureClientFactoryBuilderWithCredential
{
return builder.RegisterClientFactory<SecretClient, SecretClientOptions>((options, cred) => new SecretClient(vaultUri, cred, options));
}
✅ DO provide extension method for IAzureClientFactoryBuilderWithConfiguration<TConfiguration>
that takes TConfiguration configuration
. This overload would allow customers to pass in a IConfiguration
object and create client dynamically based on configuration values.
Sample implementation:
public static IAzureClientBuilder<SecretClient, SecretClientOptions> AddSecretClient<TBuilder, TConfiguration>(this TBuilder builder, TConfiguration configuration)
where TBuilder : IAzureClientFactoryBuilderWithConfiguration<TConfiguration>
{
return builder.RegisterClientFactory<SecretClient, SecretClientOptions>(configuration);
}