Introduction

The following document describes .NET specific guidelines for designing Azure SDK client libraries. These guidelines complement general .NET Framework Design Guidelines with design considerations specific to the Azure SDK. These guidelines also expand on and simplify language-independent General Azure SDK Guidelines. More specific guidelines take precedence over more general guidelines.

Throughout this document, we’ll use the client library for the Azure Application Configuration service to illustrate various design concepts.

Design Principles

The main value of the Azure SDK is productivity building applications with Azure services. Other qualities, such as completeness, extensibility, and performance are important but secondary. We ensure our customers can be highly productive when using our libraries by ensuring these libraries are:

Idiomatic

  • Azure SDK libraries follow .NET Framework Design Guidelines.
  • Azure SDK libraries feel like designed by the designers of the .NET Standard libraries.
  • Azure SDK libraries version just like the .NET Standard libraries.

We are not trying to fix bad parts of the language ecosystem; we embrace the ecosystem with its strengths and its flaws.

Consistent

  • The Azure SDK feels like a single product of a single team, not a set of unrelated NuGet packages.
  • Users learn common concepts once; apply the knowledge across all SDK components.
  • All differences from the guidelines must have good reasons.

Approachable

  • Small number of steps to get started; power knobs for advanced users
  • Small number of concepts; small number of types; small number of members
  • Approachable by our users, not only by engineers designing the SDK components
  • Easy to find great getting started guides and samples
  • Easy to acquire

Dependable

  • 100% backward compatible
  • Great logging, tracing, and error messages
  • Predictable support lifecycle, feature coverage, and quality

General Guidelines

DO follow the official .NET Framework Design Guidelines.

At the end of this document, you can find a section with the most commonly overlooked guidelines in existing Azure SDK libraries.

DO follow the General Azure SDK Guidelines.

The guidelines provide a robust methodology for communicating with Azure services. The easiest way to ensure that your component meets these requirements is to use the Azure.Core package to call Azure services. Details of these helper APIs and their usage are described in the Using HttpPipeline section.

DO use HttpPipeline to implement all methods that call Azure REST services.

The pipeline can be found in the Azure.Core package, and it takes care of many General Azure SDK Guidelines. Details of the pipeline design and usage are described in section Using HttpPipeline below. If you can’t use the pipeline, you must implement all the general requirements of Azure SDK manually.

Support for non-HTTP Protocols

This document contains guidelines developed primarily for typical Azure REST services, i.e. stateless services with request-response based interaction model. Many of the guidelines in this document are more broadly applicable, but some might be specific to such REST services.

Azure SDK API Design

Azure services are exposed to .NET developers as one or more service client types and a set of supporting types. The supporting types may include various subclients, which give structure to the API by organizing groups of related service operations, and model types, which represent resources on the service.

The Service Client

Service clients are the main starting points for developers calling Azure services with the Azure SDK. Each client library should have at least one client in its main namespace, so it’s easy to discover. The guidelines in this section describe patterns for the design of a service client.

A service client should have the same shape as this code snippet:

namespace Azure.<group>.<service_name> {

    // main service client class
    public class <service_name>Client {

        // simple constructors; don't use default parameters
        public <service_name>Client(<simple_binding_parameters>);
        public <service_name>Client(<simple_binding_parameters>, <service_name>ClientOptions options);

        // 0 or more advanced constructors
        public <service_name>Client(<advanced_binding_parameters>, <service_name>ClientOptions options = default);

        // mocking constructor
        protected <service_name>Client();

        // service methods (synchronous and asynchronous)
        public virtual Task<Response<<model>> <service_operation>Async(<parameters>, CancellationToken cancellationToken = default);
        public virtual Response<model> <service_operation>(<parameters>, CancellationToken cancellationToken = default);

        // other members
    }

    // options for configuring the client
    public class <service_name>ClientOptions : ClientOptions {

    }
}

For example, the Application Configuration Service client looks like this code snippet:

namespace Azure.Data.Configuration {

    public class ConfigurationClient {

        public ConfigurationClient(string connectionString);
        public ConfigurationClient(string connectionString, ConfigurationClientOptions options);

        protected ConfigurationClient(); // for mocking

        public virtual Task<Response<<ConfigurationSetting>> GetConfigurationSettingAsync(string key, CancellationToken cancellationToken = default);
        public virtual Response<ConfigurationSetting> GetConfigurationSetting(string key, CancellationToken cancellationToken = default);

        // other members
        
    }

    // options for configuring the client
    public class ConfigurationClientOptions : ClientOptions {
        ...
    }
}

You can find the full sources of here.

DO name service client types with the Client suffix.

For example, the service client for the Application Configuration service is called ConfigurationClient.

DO place at least one service client in the root namespace of their corresponding component.

DO make service clients classes (reference types), not structs (value types).

DO make service clients immutable.

Client instances are often shared between threads (stored in application statics) and it should be difficult, if not impossible, for one of these threads to affect others.

DO see Namespace Naming guidelines for how to choose the namespace for the client types.

Service Client Constructors

DO provide a minimal constructor that takes only the parameters required to connect to the service.

For example, you may use a connection string, or host name and authentication. It should be easy to start using the client without extensive customization.

public class ConfigurationClient {
    public ConfigurationClient(string connectionString);
}

⛔️ DO NOT use default parameters in the simplest constructor.

DO provide constructor overloads that allow specifying additional options such as credentials, a custom HTTP pipeline, or advanced configuration.

Custom pipeline and client-specific configuration are represented by an options parameter. The type of the parameter is typically a subclass of ClientOptions type, shown below.

Using ClientOptions

DO name subclasses of ClientOptions by adding Options suffix to the name of the client type the options subclass is configuring.

// options for configuring ConfigurationClient
public class ConfigurationClientOptions : ClientOptions {
    ...
}

public class ConfigurationClient {

    public ConfigurationClient(string connectionString, ConfigurationClientOptions options);
    ...
}

If the options type can be shared by multiple client types, name it with a more general name, such as <library_name>ClientOptions. For example, the BlobClientOptions class can be used by BlobClient, BlobContainerClient, and BlobAccountClient.

⛔️ DO NOT have a default constructor on the options type.

Each overload constructor should take at least version parameter to specify the service version. See Versioning guidelines for details.

For example, the ConfigurationClient type and its public constructors look as follows:

public class ConfigurationClient {
    public ConfigurationClient(string connectionString);
    public ConfigurationClient(string connectionString, ConfigurationClientOptions options);
    public ConfigurationClient(Uri uri, TokenCredential credential, ConfigurationClientOptions options = default);
}
Service Versions

DO call the highest supported service API version by default.

DO allow the consumer to explicitly select a supported service API version when instantiating the service client.

Use a constructor parameter called version on the client options type.

  • The version parameter must be the first parameter to all constructor overloads.
  • The version parameter must be required, and default to the latest supported service version.
  • The type of the version parameter must be ServiceVersion; an enum nested in the options type.
  • The ServiceVersion enum must use explicit values starting from 1.
  • ServiceVersion enum value 0 is reserved. When 0 is passed into APIs, ArgumentException should be thrown.

For example, the following is a code snippet from the ConfigurationClientOptions:

public class ConfigurationClientOptions : ClientOptions {

    public ConfigurationClientOptions(ServiceVersion version = ServiceVersion.V2019_05_09) {
        if (version == default)
            throw new ArgumentException($"The service version {version} is not supported by this library.");
        }
    }

    public enum ServiceVersion {
        V2019_05_09 = 1,
    }
    ...
}

⛔️ DO NOT force consumers to test service API versions to check support for a feature. Use the tester-doer .NET pattern to implement feature flags, or use Nullable<T>.

For example, if the client library supports two service versions, only one of which can return batches, the consumer might write the following code:

if (client.CanBatch) {
    Response<SettingBatch> response = await client.GetBatch("some_key*");
    Guid? Guid = response.Result.Guid;
} else {
    Response<ConfigurationSetting> response1 = await client.GetAsync("some_key1");
    Response<ConfigurationSetting> response2 = await client.GetAsync("some_key2");
    Response<ConfigurationSetting> response3 = await client.GetAsync("some_key3");
}
Mocking

DO provide protected parameterless constructor for mocking.

public class ConfigurationClient {
    protected ConfigurationClient();
}

⛔️ DO NOT reference virtual properties of the client class as parameters to other methods or constructors within the client constructor. This violates the .NET Framework Constructor Design because a field to which a virtual property refers may not be initialized yet, or a mocked virtual property may not be set up yet. Use parameters or local variables instead:

public class ConfigurationClient {
    private readonly ConfigurationRestClient _client;
    public ConfigurationClient(string connectionString) {
        ConnectionString = connectionString;
        // Use parameter below instead of the class-defined virtual property.
        _client = new ConfigurationRestClient(connectionString);
    }
    public virtual string ConnectionString { get; }
}

In mocks, using the virtual property instead of the parameter requires the property to be mocked to return the value before the constructor is called when the mock is created. In Moq this requires using the delegate parameter to create the mock, which may not be an obvious workaround.

See Support for Mocking for details.

Subclients

There are two categories of clients: service clients and their subclients. Service clients can be instantiated and have the Client suffix. Subclients can only be created by calling factory methods on other clients (commonly on service clients) and do not have the client suffix.

As discussed above, the service client is the entry point to the API for an Azure service – from it, library users can invoke all operations the service provides and can easily implement the most common scenarios. Where it will simplify an API’s design, groups of service calls can be organized around smaller subclient types.

DO use service clients to indicate the starting point(s) for the most common customer scenarios.

☑️ YOU SHOULD use subclients to group operations related to a service resource or functional area to improve API usability.

There are a variety of types of subclients. These include:

  • Resource Clients, which group methods bound to a specific resource, along with information about the resource.
  • Operation Group Clients, which are not bound to a resource but group related operations. If referring to a specific resource, these would take a resource identifier as a parameter.
  • Subclasses of Operation<T>, which manage service calls related to long running operations.
  • Pageable<T> types returned from paging methods, which manage service calls to retrieve pages of elements in a collection.

For example, in the Azure Container Registry API, a ContainerRegistryClient service client provides an entry point for communicating with the service, and a ContainerRepository resource client organizes operations related to a specific repository resource:

public class ContainerRegistryClient {
    // ...
    public virtual ContainerRepository GetRepository(string name);
}
 
public class ContainerRepository {
    protected ContainerRepository();
    public virtual string Name { get; }
    public virtual Response Delete(CancellationToken cancellationToken = default);
    public virtual Response<ContainerRepositoryProperties> GetProperties(CancellationToken cancellationToken = default);
    public virtual Response<ContainerRepositoryProperties> UpdateProperties(ContainerRepositoryProperties value, CancellationToken cancellationToken = default);
    // ...
}

ServiceBusSender groups operations for sending messages to a specific entity with properties that identify that entity.

    public class ServiceBusSender {
        protected ServiceBusSender();
        public virtual string EntityPath { get; }
        public virtual Task CancelScheduledMessageAsync(long sequenceNumber, CancellationToken cancellationToken = default);
        public virtual ValueTask<ServiceBusMessageBatch> CreateMessageBatchAsync(CancellationToken cancellationToken = default);
        public virtual Task<long> ScheduleMessageAsync(ServiceBusMessage message, DateTimeOffset scheduledEnqueueTime, CancellationToken cancellationToken = default);
        public virtual Task SendMessageAsync(ServiceBusMessage message, CancellationToken cancellationToken = default);
        // ...
    }

DO provide factory methods to create a subclient.

✔️ YOU MAY include a suffix the method that creates a subclient, according to the table below:

Client Type Naming Convention Factory Method Naming Convention
Service Client Client Suffix Get<client>Client()
Resource Client No Suffix Get<resource>()
Operation Group Client No Suffix Get<group>Client()
Long Running Operation Operation Suffix (long LRO) Start prefix; (short LRO) no prefix
Pageable Pageable<T> Get<resource>s

☑️ YOU SHOULD take a resource identifier as a parameter to the resource client factory method.

☑️ YOU SHOULD expose resource identifiers as properties on the resource client.

✔️ YOU MAY place operations on collections of resources a separate subclient to avoid cluttering the parent client with too many methods.

While API usability is the primary reason for subclients, another motivating factor is resource efficiency. Clients need to be cached, so if the set of client instances is large or unlimited (in case the client takes a scoping parameter, like a hub, or a container), using subclients allows an application to cache the top level client and create instances of subclients on demand. In addition, if there is an expensive shared resource (e.g. an AMQP connection), subclients are preferred, as they naturally lead to resource sharing.

☑️ YOU SHOULD use the HttpPipeline that belongs to the type providing the factory method to make network calls to the service from the subclient. An exception to this might be if subclient needs different pipeline policies than the parent client.

⛔️ DO NOT provide a public constructor on a subclient. Subclients are non-instantiable by design.

DO provide a protected parameterless constructor on subclients for mocking.

Choosing between Service Clients and Subclients

In many cases, an Azure SDK API should contain one service client and zero or more subclients. Both service clients and subclients have service methods. Consider adding more than one service client to the API in the following cases:

✔️ YOU MAY consider providing an additional service client in an API when the service has different common scenarios for multiple target users, such as a service administrator and an end-user of the entities the administrator creates.

For example, the Azure Form Recognizer library provides a FormRecognizerClient for application developers to read form fields in their applications, and a FormTrainingClient for data scientist customers to train the form recognition models.

✔️ YOU MAY consider providing an additional service client when a service has advanced scenarios you want to keep separate from the types that support the most common scenarios. In this case, consider using a .Specialized namespace to contain the additional clients.

For example, the Azure Storage Blobs library provides a BlockBlobClient in the Azure.Storage.Blobs.Specialized namespace that gives finer grained control of how blobs are uploaded. For further discussion of designing APIs for advanced scenarios, please see the .NET Framework Guidelines sections on progressive frameworks and the principle of layered architecture.

✔️ YOU MAY consider providing an additional service client for a service resource that is commonly referenced with a URL that points to it directly. This will allow users to instantiate a client directly from the resource endpoint, without needing to parse the URL to obtain the root service endpoint.

✔️ YOU MAY consider providing additional service clients for each level in a resource hierarchy. For service clients representing resources in a hierarchy, you should also provide a <parent>.Get<child>Client(...) method to retrieve the client for the named child.

For example, the Azure Storage service provides an account that contains zero or more containers, which in turn contain zero or more blobs. The Azure SDK storage library provides service clients for each level: BlobServiceClient, BlobContainerClient, and BlobClient.

public class BlobServiceClient {
    // ...
    public virtual BlobContainerClient GetBlobContainerClient(string blobContainerName);
    // ...
}

public class BlobContainerClient {
    // ...
    public virtual BlobClient GetBlobClient(string blobName);
    // ...
}

public class BlobClient {
    // ...
}

Service Methods

Service methods are the methods on the client that invoke operations on the service.

Here are the main service methods in the ConfigurationClient. They meet all the guidelines that are discussed below.

public class ConfigurationClient {

    public virtual Task<Response<ConfigurationSetting>> AddAsync(ConfigurationSetting setting, CancellationToken cancellationToken = default);
    public virtual Response<ConfigurationSetting> Add(ConfigurationSetting setting, CancellationToken cancellationToken = default);

    public virtual Task<Response<ConfigurationSetting>> SetAsync(ConfigurationSetting setting, CancellationToken cancellationToken = default);
    public virtual Response<ConfigurationSetting> Set(ConfigurationSetting setting, CancellationToken cancellationToken = default);

    public virtual Task<Response<ConfigurationSetting>> GetAsync(string key, SettingFilter filter = default, CancellationToken cancellationToken = default);
    public virtual Response<ConfigurationSetting> Get(string key, SettingFilter filter = default, CancellationToken cancellationToken = default);

    public virtual Task<Response<ConfigurationSetting>> DeleteAsync(string key, SettingFilter filter = default, CancellationToken cancellationToken = default);
    public virtual Response<ConfigurationSetting> Delete(string key, SettingFilter filter = default, CancellationToken cancellationToken = default);
}
Sync and Async

DO provide both asynchronous and synchronous variants for all service methods.

Many developers want to port existing applications to the Cloud. These applications are often synchronous, and the cost of rewriting them to be asynchronous is usually prohibitive. Calling asynchronous APIs from synchronous methods can only be done through a technique called sync-over-async, which can cause deadlocks. The Azure SDK provides synchronous APIs to minimize friction when porting existing application to Azure.

DO ensure that the names of the asynchronous and the synchronous variants differ only by the Async suffix.

Naming

Most methods in Azure SDK libraries should be named following the typical .NET method naming conventions. The Azure SDK Design Guidelines add special conventions for methods that access and manipulate server resources.

☑️ YOU SHOULD use standard verbs for methods that access or manipulate server resources.

Verb
Parameters
Returns
Comments
Verb

Create

Parameters

key, item

Returns

Created item

Comments

Creates a resource. Throws if the resource exists.

Verb

Set

Parameters

key, item

Returns
Comments

Creates or replaces a resource.

Verb

Update

Parameters

key, item

Returns

item

Comments

Updates a resource. Throws if the resource does not exist. Update methods might take a parameter controlling whether the update is a replace, merge, or other specific semantics.

Verb

Get<resource_name>

Parameters

key

Returns

item

Comments

Retrieves a resource. Throws if the resource does not exist.

Verb

Get<resource_name_plural>

Parameters

key, item

Returns

item

Comments

Retrieves one or more resources. Returns empty set if no resources found.

Verb

Delete

Parameters

item

Returns

item

Comments

Deletes one or more resources, or no-op if the resources do not exist.

Verb

Remove

Parameters

index, item

Returns

item

Comments

Remove a reference to a resource from a collection. This method doesn’t delete the actual resource, only the reference.

Verb

<resource_name>Exists

Parameters

key

Returns

item

Comments

Returns true if the resource exists, otherwise returns false.

Cancellation

DO ensure all service methods, both asynchronous and synchronous, take an optional CancellationToken parameter called cancellationToken or, in case of protocol methods, an optional RequestContext parameter called context.

The token should be further passed to all calls that take a cancellation token. DO NOT check the token manually, except when running a significant amount of CPU-bound work within the library, e.g. a loop that can take more than a typical network call.

Mocking

DO make service methods virtual.

Virtual methods are used to support mocking. See Support for Mocking for details.

Return Types

DO return Response<T> or Response from synchronous methods.

T represents the content of the response, as described below.

DO return Task<Response<T>> or Task<Response> from asynchronous methods that make network requests.

There are two possible return types from asynchronous methods: Task and ValueTask. Your code will be doing a network request in the majority of cases. The Task variant is more appropriate for this use case. For more information, see this blog post.

T represents the content of the response, as described below.

The T can be either an unstructured payload (e.g. bytes of a storage blob) or a model type representing deserialized response content.

DO use one of the following return types to represent an unstructured payload:

  • System.IO.Stream - for large payloads
  • byte[] - for small payloads
  • ReadOnlyMemory<byte> - for slices of small payloads

DO return a model type if the content has a schema and can be deserialized.

For more information, see Model Types

Thread Safety

DO be thread-safe. All public members of the client type must be safe to call from multiple threads concurrently.

Service Method Parameters

Service methods fall into two main groups when it comes to the number and complexity of parameters they accept:

  • Service Methods with Simple Inputs, simple methods for short
  • Service Methods with Complex Inputs, complex methods for short

Simple methods are methods that take up to six parameters, with most of the parameters being simple BCL primitives. Complex methods are methods that take large number of parameters and typically correspond to REST APIs with complex request payloads.

Simple methods should follow standard .NET Framework Design Guidelines for parameter list and overload design.

Complex methods should use option parameter to represent the request payload, and consider providing convenience simple overloads for most common scenarios.

public class BlobContainerClient {

    // simple service method
    public virtual Response<BlobInfo> UploadBlob(string blobName, Stream content, CancellationToken cancellationToken = default);

    // complex service method
    public virtual Response<BlobInfo> CreateBlob(BlobCreateOptions options = null, CancellationToken cancellationToken = default);

    // convinience overload[s]
    public virtual Response<BlobContainerInfo> CreateBlob(string blobName, CancellationToken cancellationToken = default);
}

public class BlobCreateOptions {
    public PublicAccessType Access { get; set; }
    public IDictionary<string, string> Metadata { get; }
    public BlobContainerEncryptionScopeOptions Encryption { get; set; }
    ...
}

The Options class is designed similarly to .NET custom attributes, where required service method parameters are modeled as Options class constructor parameters and get-only properties, and optional parameters are get-set properties.

DO use the options parameter pattern for complex service methods.

✔️ YOU MAY use the options parameter pattern for simple service methods that you expect to grow in the future.

✔️ YOU MAY add simple overloads of methods using the options parameter pattern.

If in common scenarios, users are likely to pass just a small subset of what the options parameter represents, consider adding an overload with a parameter list representing just this subset.

    // main overload taking the options property bag
    public virtual Response<BlobInfo> CreateBlob(BlobCreateOptions options = null, CancellationToken cancellationToken = default);

    // simple overload with a subset of parameters of the options bag
    public virtual Response<BlobContainerInfo> CreateBlob(string blobName, CancellationToken cancellationToken = default);
}

✔️ YOU MAY name the option parameter type with the ‘Options’ suffix.

Parameter Validation

Service methods take two kinds of parameters: service parameters and client parameters. Service parameters are sent across the wire to the service as URL segments, query parameters, request header values, and request bodies (typically JSON or XML). Client parameters are used solely within the client library and are not sent to the service; examples are path parameters, CancellationTokens or file paths.

DO validate client parameters.

⛔️ DO NOT validate service parameters.

Common parameter validations include null checks, empty string checks, and range checks. Let the service validate its parameters.

DO test the developer experience when invalid service parameters are passed in. Ensure clear error messages are generated by the client. If the developer experience is inadequate, work with the service team to correct the problem.

Methods Returning Collections (Paging)

Many Azure REST APIs return collections of data in batches or pages. A client library will expose such APIs as special enumerable types Pageable<T> or AsyncPageable<T>. These types are located in the Azure.Core package.

For example, the configuration service returns collections of items as follows:

public class ConfigurationClient {

    // asynchronous API returning a collection of items
    public virtual AsyncPageable<Response<ConfigurationSetting>> GetConfigurationSettingsAsync(...);

    // synchronous variant of the method above
    public virtual Pageable<ConfigurationSetting> GetConfigurationSettings(...);
    ...
}

DO return Pageable<T> or AsyncPageable<T> from service methods that return a collection of items.

Methods Invoking Long Running Operations

Some service operations, known as Long Running Operations or LROs take a long time (up to hours or days). Such operations do not return their result immediately, but rather are started, their progress is polled, and finally the result of the operation is retrieved.

Azure.Core library exposes an abstract type called Operation<T>, which represents such LROs and supports operations for polling and waiting for status changes, and retrieving the final operation result. A service method invoking a long running operation will return a subclass of Operation<T>, as shown below.

Note that some older libraries use a slightly different, older LRO pattern. In the old pattern, LRO methods started with the prefix ‘Start’ and did not take the WaitUntil parameter. Such libraries are free to continue using this older pattern, or they can transition to the new pattern.

// the following type is located in Azure.Core
public abstract class Operation<T> : Operation {

    public abstract bool HasCompleted { get; }
    public abstract bool HasValue { get; }

    public abstract string Id { get; }

    public abstract T Value { get; } // throws if CachedStatus != Succeeded
    public abstract Response GetRawResponse();

    public abstract Response UpdateStatus(CancellationToken cancellationToken = default);
    public abstract ValueTask<Response> UpdateStatusAsync(CancellationToken cancellationToken = default);

    public virtual Response<T> WaitForCompletion(CancellationToken cancellationToken = default);
    public virtual Response<T> WaitForCompletion(TimeSpan pollingInterval, CancellationToken cancellationToken);	
    public virtual ValueTask<Response<T>> WaitForCompletionAsync(CancellationToken cancellationToken = default);	
    public virtual ValueTask<Response<T>> WaitForCompletionAsync(TimeSpan pollingInterval, CancellationToken cancellationToken = default);

    // inherited  members returning untyped responses
    public virtual Response WaitForCompletionResponse(CancellationToken cancellationToken = default);	
    public virtual Response WaitForCompletionResponse(TimeSpan pollingInterval, CancellationToken cancellationToken = default);	
    public virtual ValueTask<Response> WaitForCompletionResponseAsync(CancellationToken cancellationToken = default);	
    public virtual ValueTask<Response> WaitForCompletionResponseAsync(TimeSpan pollingInterval, CancellationToken cancellationToken = default);
}

Client libraries need to inherit from Operation<T> not only to implement all abstract members, but also to provide a constructor required to access an existing LRO (an LRO initiated by a different process).

public class CopyFromUriOperation : Operation<long> {
    public CopyFromUriOperation(string id, BlobBaseClient client);
    ...
}

public class BlobBaseClient {

    public virtual CopyFromUriOperation CopyFromUri(WaitUntil wait, ..., CancellationToken cancellationToken = default);
    public virtual Task<CopyFromUriOperation> CopyFromUriAsync(WaitUntil wait, ..., CancellationToken cancellationToken = default);
}

The following code snippet shows how an SDK consumer would use the Operation to poll for a response.

BlobBaseClient client = ...

// automatic polling
{
    Operation<long> operation = await client.CopyFromUri(WaitUntil.Completed, ...);
    Console.WriteLine(operation.Value);
}

// manual polling
{
    CopyFromUriOperation operation = await client.CopyFromUriAsync(WaitUntil.Started, ...);
    while (true)
    {
        await operation.UpdateStatusAsync();
        if (operation.HasCompleted) break;
        await Task.Delay(1000); // play some elevator music
    }
    if (operation.HasValue) Console.WriteLine(operation.Value);
}

// saving operation ID
{
    CopyFromUriOperation operation = await client.CopyFromUriAsync(WaitUntil.Started, ...);
    string operationId = operation.Id;

    // two days later
    var operation2 = new CopyFromUriOperation(operationId, client);
    long value = await operation2.WaitForCompletionAsync();
}

DO return a subclass of Operation<T> from LRO methods.

DO take WaitUntil as the first parameter to LRO methods.

✔️ YOU MAY add additional APIs to subclasses of Operation<T>. For example, some subclasses add a constructor allowing to create an operation instance from a previously saved operation ID. Also, some subclasses are more granular states besides the IsCompleted and HasValue states that are present on the base class.

DO provide a public constructor on subclasses of Operation<T> to allow users to access an existing LRO.

DO provide protected parameterless constructor for mocking in subclasses of Operation<T>.

public class CopyFromUriOperation {
    protected CopyFromUriOperation();
}

DO make all properties virtual to allow mocking.

public class CopyFromUriOperation {
    public virtual Uri SourceUri { get; }
}
Conditional Request Methods

Some services support conditional requests that are used to implement optimistic concurrency control. In Azure, optimistic concurency is typically implemented using If-Match headers and ETags. See Managing Concurrency in Blob Storage as a good example.

DO use Azure.Core ETag to represent ETags.

✔️ YOU MAY take MatchConditions, RequestConditions, (or a custom subclass) as a parameter to conditional service call methods.

TODO: more guidelines comming. see https://github.com/Azure/azure-sdk/issues/2154

Supporting Types

In addition to service client types, Azure SDK APIs provide and use other supporting types as well.

Model Types

This section describes guidelines for the design model types and all their transitive closure of public dependencies (i.e. the model graph). A model type is a representation of a REST service’s resource.

For example, review the configuration service model type below:

public sealed class ConfigurationSetting : IEquatable<ConfigurationSetting> {

    public ConfigurationSetting(string key, string value, string label = default);

    public string ContentType { get; set; }
    public string ETag { get; internal set; }
    public string Key { get; set; }
    public string Label { get; set; }
    public DateTimeOffset LastModified { get; internal set; }
    public bool Locked { get; internal set; }
    public IDictionary<string, string> Tags { get; }
    public string Value { get; set; }

    public bool Equals(ConfigurationSetting other);

    [EditorBrowsable(EditorBrowsableState.Never)]
    public override bool Equals(object obj);
    [EditorBrowsable(EditorBrowsableState.Never)]
    public override int GetHashCode();
    [EditorBrowsable(EditorBrowsableState.Never)]
    public override string ToString();
}

This model is returned from service methods as follows:

public class ConfigurationClient {
    public virtual Task<Response<ConfigurationSetting>> GetAsync(...);
    public virtual Response<ConfigurationSetting> Get(...);
    ...
}

DO ensure model public properties are get-only if they aren’t intended to be changed by the user.

Most output-only models can be fully read-only. Models that are used as both outputs and inputs (i.e. received from and sent to the service) typically have a mixture of read-only and read-write properties.

For example, the Locked property of ConfigurationSetting is controlled by the service. It shouldn’t be changed by the user. The ContentType property, by contrast, can be modified by the user.

public sealed class ConfigurationSetting : IEquatable<ConfigurationSetting> {

    public string ContentType { get; set; }

    public bool Locked { get; internal set; }
}

Ensure you include an internal setter to allow for deserialization. For more information, see JSON Serialization.

DO ensure model types are structs, if they meet the criteria for being structs.

Good candidates for struct are types that are small and immutable, especially if they are often stored in arrays. See .NET Framework Design Guidelines for details.

☑️ YOU SHOULD implement basic data type interfaces on model types, per .NET Framework Design Guidelines.

For example, implement IEquatable<T>, IComparable<T>, IEnumerable<T>, etc. if applicable.

☑️ YOU SHOULD use the following collection types for properties of model types:

  • IReadOnlyList<T> and IList<T> for most collections
  • IReadOnlyDictionary<T> and IDictionary<T> for lookup tables
  • T[], Memory<T>, and ReadOnlyMemory<T> when low allocations and performance are critical

Note that this guidance does not apply to input parameters. Input parameters representing collections should follow standard .NET Framework Design Guidelines, e.g. use IEnumerable<T> is allowed. Also, this guidance does not apply to return types of service method calls. These should be using Pageable<T> and AsyncPageable<T> discussed in Methods Returning Collections.

✔️ YOU MAY place output model types in .Models subnamespace to avoid cluttering the main namespace with too many types.

It is important for the main namespace of a client library to be clutter free. Some client libraries have a relatively small number of model types, and these should keep the model types in the main namespace. For example, model types of Azure.Data.AppConfiguration package are in the main namespace. On the other hand, model types of Azure.Storage.Blobs package are in .Models subnamespace.

namespace Azure.Storage.Blobs {
    public class BlobClient { ... }
    public class BlobClientOptions { ... }
    ...
}
namespace Azure.Storage.Blobs.Models {
    ...
    public class BlobContainerItem { ... }
    public class BlobContainerProperties { ...}
    ...
}

☑️ YOU SHOULD apply the [EditorBrowsable(EditorBrowsableState.Never)] attribute to methods on the model type that the user isn’t meant to call.

Adding this attribute will hide the methods from being shown with IntelliSense. A user will almost never call GetHashCode() directly. Equals(object) is almost never called if the type implements IEquatable<T> (which is preferred). Hide the ToString() method if it isn’t overridden.

public sealed class ConfigurationSetting : IEquatable<ConfigurationSetting> {
    [EditorBrowsable(EditorBrowsableState.Never)]
    public override bool Equals(object obj);
    [EditorBrowsable(EditorBrowsableState.Never)]
    public override int GetHashCode();
}

DO ensure all model types can be used in mocks.

In practice, you need to provide public APIs to construct model graphs. See Support for Mocking for details.

Model Type Naming

TODO: issue #2298

Enumerations

DO use an enum for parameters, properties, and return types when values are known.

✔️ YOU MAY use a readonly struct in place of an enum that declares well-known fields but can contain unknown values returned from the service, or user-defined values passed to the service.

See enumeration-like structure documentation for implementation details.

Using Azure Core Types

The Azure.Core package provides common functionality for client libraries. Documentation and usage examples can be found in the azure/azure-sdk-for-net repository.

Using Primitive Types

DO use Azure.ETag to represent ETags.

The Azure.ETag type is located in Azure.Core package.

DO use System.Uri to represent URIs.

Exceptions

In .NET, throwing exceptions is how we communicate to library consumers that the services returned an error.

DO throw RequestFailedException or its subtype when a service method fails with non-success status code.

The exception is available in Azure.Core package:

public class RequestFailedException : Exception {

    public RequestFailedException(Response response);
    public RequestFailedException(Response response, Exception innerException);
    public RequestFailedException(Response response, Exception innerException, RequestFailedDetailsParser detailsParser);

    public int Status { get; }
}

The exception message will be formed from the passed in Response content. For example:

if (response.Status != 200) {
    throw new RequestFailedException(response);
}

DO use RequestFailedException or one of its subtypes where possible.

DO provide RequestFailedDetailsParser for non-standard error formats.

If customization is required to parse the response content, e.g. because the service does not adhere to the standard error format as represented by the ResponseError type, libraries can must implement a RequestFailedDetailsParser and pass the parser into the construction of the HttpPipeline via the HttpPipelineOptions type. If more granular control is required than associating the parser per pipeline, there is a constructor of RequestFailedException that takes a RequestFailedDetailsParser that may be used.

Don’t introduce new exception types unless there’s a programmatic scenario for handling the new exception that’s different than RequestFailedException

Authentication

The client library consumer should construct a service client using just the constructor. After construction, service methods can successfully invoke service operations. The constructor parameters must take all parameters required to create a functioning client, including all information needed to authenticate with the service.

The general constructor pattern refers to binding parameters.

// simple constructors
public <service_name>Client(<simple_binding_parameters>);
public <service_name>Client(<simple_binding_parameters>, <service_name>ClientOptions options);

// 0 or more advanced constructors
public <service_name>Client(<advanced_binding_parameters>, <service_name>ClientOptions options = default);

Typically, binding parameters would include a URI to the service endpoint and authorization credentials. For example, the blob service client can be bound using any of:

  • a connection string (which contains both endpoint information and credentials),
  • an endpoint (for anonymous access),
  • an endpoint and credentials (for authenticated access).
// hello world constructors using the main authentication method on the service's Azure Portal (typically a connection string)
// we don't want to use default parameters here; all other overloads can use default parameters
public BlobServiceClient(string connectionString)
public BlobServiceClient(string connectionString, BlobClientOptions options)

// anonymous access
public BlobServiceClient(Uri uri, BlobClientOptions options = default)

// using credential types
public BlobServiceClient(Uri uri, StorageSharedKeyCredential credential, BlobClientOptions options = default)
public BlobServiceClient(Uri uri, TokenCredential credential, BlobClientOptions options = default)

☑️ YOU SHOULD use credential types provided in the Azure.Core package.

Currently, Azure.Core provides TokenCredential for OAuth style tokens, including MSI credentials.

DO support changing credentials without having to create a new client instance.

Credentials passed to the constructors must be read before every request (for example, by calling TokenCredential.GetToken()).

DO contact adparch if you want to add a new credential type.

✔️ YOU MAY offer a way to create credentials from a connection string only if the service offers a connection string via the Azure portal.

Don’t ask users to compose connection strings manually if they aren’t available through the Azure portal. Connection strings are immutable. It’s impossible for an application to roll over credentials when using connection strings.

Namespaces

DO adhere to the following scheme when choosing a namespace: Azure.<group>.<service>[.<feature>]

For example, Azure.Storage.Blobs.

DO use one of the following pre-approved namespace groups:

  • Azure.AI for artificial intelligence, including machine learning
  • Azure.Analytics for client libraries that gather or process analytics data
  • Azure.Communication communication services
  • Azure.Core for libraries that aren’t service specific
  • Azure.Cosmos for object database technologies
  • Azure.Data for client libraries that handle databases or structured data stores
  • Azure.DigitalTwins for DigitalTwins related technologies
  • Azure.Identity for authentication and authorization client libraries
  • Azure.IoT for client libraries dealing with the Internet of Things.
    • Use Iot for Pascal cased compound words, such as IotClient, otherwise follow language conventions.
    • Do not use IoT more than once in a namespace.
  • Azure.Media for client libraries that deal with audio, video, or mixed reality
  • Azure.Messaging for client libraries that provide messaging services, such as push notifications or pub-sub.
  • Azure.Monitor for observability and Azure Monitor client libraries.
  • Azure.ResourceManager.[ResourceProvider] for management plane client libraries for a given resource provider.
    • For example the compute management plane namespace would be Azure.ResourceManager.Compute.
  • Azure.Search for search technologies
  • Azure.Security for client libraries dealing with security
  • Azure.Storage for client libraries that handle unstructured data

If you think a new group should be added to the list, contact adparch.

DO register all namespaces with adparch.

⛔️ DO NOT place APIs in the second-level namespace (directly under the Azure namespace).

☑️ YOU SHOULD consider placing model types in a .Models namespace if number of model types is or might become large.

See model type guidelines for details.

Support for Mocking

All client libraries must support mocking to enable non-live testing of service clients by customers.

Here is an example of how the ConfigurationClient can be mocked using Moq (a popular .NET mocking library):

// Create a mock response
var mockResponse = new Mock<Response>();
// Create a client mock
var mock = new Mock<ConfigurationClient>();
// Setup client method
mock.Setup(c => c.Get("Key", It.IsAny<string>(), It.IsAny<DateTimeOffset>(), It.IsAny<CancellationToken>()))
    .Returns(new Response<ConfigurationSetting>(mockResponse.Object, ConfigurationModelFactory.ConfigurationSetting("Key", "Value")));

// Use the client mock
ConfigurationClient client = mock.Object;
ConfigurationSetting setting = client.Get("Key");
Assert.AreEqual("Value", setting.Value);

For more details on mocking, see Unit testing and mocking with the Azure SDK for .NET.

DO provide protected parameterless constructor for mocking.

DO make all service methods virtual.

DO make all properties virtual.

    public class BlobContainerClient {
        public virtual string Name { get; }
        public virtual Uri Uri { get; }
    }

DO make methods returning other clients virtual.

public class BlobContainerClient
{
    public virtual BlobClient GetBlobClient();
}

DO use instance methods instead of extension methods when defined in the same assembly. The instance methods are simpler to mock.

public class BlobContainerClient
{
    // This method is possible to mock
    public virtual AppendBlobClient GetAppendBlobClient() {}
}

public class BlobContainerClientExtensions
{
    // This method is impossible to mock
    public static AppendBlobClient GetAppendBlobClient(this BlobContainerClient containerClient) {}
}

DO provide factory or builder for constructing model graphs returned from virtual service methods.

Model types shouldn’t have public constructors. Instances of the model are typically returned from the client library, and are not constructed by the consumer of the library. Mock implementations need to create instances of model types. Implement a static class called <service>ModelFactory in the same namespace as the model types:

public static class ConfigurationModelFactory {
    public static ConfigurationSetting ConfigurationSetting(string key, string value, string label=default, string contentType=default, ETag eTag=default, DateTimeOffset? lastModified=default, bool? locked=default);
    public static SettingBatch SettingBatch(ConfigurationSetting[] settings, string link, SettingSelector selector);
}

DO hide older overloads and avoid ambiguity.

When read-only properties are added to models and factory methods must be added to optionally set these properties, you must hide the previous method and remove all default parameter values to avoid ambiguity:

public static class ConfigurationModelFactory {
    [EditorBrowsable(EditorBrowsableState.Never)]
    public static ConfigurationSetting ConfigurationSetting(string key, string value, string label, string contentType, ETag eTag, DateTimeOffset? lastModified, bool? locked) =>
        ConfigurationSetting(key, value, label, contentType, eTag, lastModified, locked, default);
    public static ConfigurationSetting ConfigurationSetting(string key, string value, string label=default, string contentType=default, ETag eTag=default, DateTimeOffset? lastModified=default, bool? locked=default, int? ttl=default);
}

Azure SDK Library Design

Packaging

DO package all components as NuGet packages.

If your client library is built by the Azure SDK engineering systems, all packaging requirements will be met automatically. Follow the .NET packaging guidelines if you’re self-publishing. For Microsoft owned packages we need to support both windows (for windows dump diagnostics) and portable (for x-platform debugging) pdb formats which means you need to publish them to the Microsoft symbol server and not the Nuget symbol server which only supports portable pdbs.

DO name the package based on the name of the main namespace of the component.

For example, if the component is in the Azure.Storage.Blobs namespace, the component DLL will be Azure.Storage.Blobs.dll and the NuGet package will bAzure.Storage.Blobs``.

☑️ YOU SHOULD place small related components that evolve together in a single NuGet package.

DO build all libraries for .NET Standard 2.0.

Use the following target setting in the .csproj file:

<TargetFramework>netstandard2.0</TargetFramework>

DO define the same APIs for all target framework monikers (TFMs).

You may multi-target client libraries to different TFMs but the public API must be the same for all targets including class, interface, parameter, and return types.

Common Libraries

There are occasions when common code needs to be shared between several client libraries. For example, a set of cooperating client libraries may wish to share a set of exceptions or models.

DO gain Architecture Board discuss how to design such common library.

Versioning

Client Versions

DO be 100% backwards compatible with older versions of the same package.

For detailed rules, see .NET Breaking Changes.

DO introduce a new package (with new assembly names, new namespace names, and new type names) if you must do an API breaking change.

Breaking changes should happen rarely, if ever. Register your intent to do a breaking change with adparch. You’ll need to have a discussion with the language architect before approval.

Package Version Numbers

Consistent version number scheme allows consumers to determine what to expect from a new version of the library.

DO use MAJOR.MINOR.PATCH format for the version of the library dll and the NuGet package.

Use -beta._N suffix for beta package versions. For example, 1.0.0-beta.2.

DO change the version number of the client library when ANYTHING changes in the client library.

DO increment the patch version when fixing a bug.

⛔️ DO NOT include new APIs in a patch release.

DO increment the major or minor version when adding support for a service API version.

DO increment the major or minor version when adding a new method to the public API.

☑️ YOU SHOULD increment the major version when making large feature changes.

DO select a version number greater than the highest version number of any other released Track 1 package for the service in any other scope or language.

Dependencies

☑️ YOU SHOULD minimize dependencies outside of the .NET Standard and Azure.Core packages.

⛔️ DO NOT depend on any NuGet package except the following packages:

  • Azure.* packages from the azure/azure-sdk-for-net repository.
  • System.Text.Json.
  • Microsoft.BCL.AsyncInterfaces.
  • packages produced by your own team.

In the past, JSON.NET, aka Newtonsoft.Json, was commonly used for serialization and deserialization. Use the System.Text.Json package that is now a part of the .NET platform instead.

⛔️ DO NOT publicly expose types from dependencies unless the types follow these guidelines as well.

Native Code

Native dependencies introduce lots of complexities to .NET libraries and so they should be avoided.

⚠️ YOU SHOULD NOT native dependencies.

Documentation Comments

DO document every exposed (public or protected) type and member within your library’s code.

DO use C# documentation comments for reference documentation.

See the documentation guidelines for language-independent guidelines for how to provide good documentation.

Repository Guidelines

DO locate all source code and README in the azure/azure-sdk-for-net GitHub repository.

DO follow Azure SDK engineering systems guidelines for working in the azure/azure-sdk-for-net GitHub repository.

Documentation Style

TODO: issue #2338

README

DO have a README.md file in the component root folder.

An example of a good README.md file can be found here.

DO optimize the README.md for the consumer of the client library.

The contributor guide (CONTRIBUTING.md) should be a separate file linked to from the main component README.md.

Samples

Each client library should have a quickstart guide with code samples. Developers like to learn about a library by looking at sample code; not by reading in-depth technology papers.

DO have usage samples in samples subdirectory of main library directory.

For a complete example, see the Configuration Service samples.

DO have a README.md file with the following front matter:

---
page_type: sample
languages:
- csharp
products:
- azure
- azure-app-configuration
name: Azure.Data.AppConfiguration samples for .NET
description: Samples for the Azure.Data.AppConfiguration client library
---

The README.md file should be written as a getting started guide. See the ServiceBus README for a good example.

DO link to each of the samples files using a brief description as the link text.

DO have a sample file called Sample1_HelloWorld.md. All other samples are ordered from simplest to most complex using the Sample<number>_ prefix.

DO use synchronous APIs in the Sample1_HelloWorld.md sample. Add a second sample named Sample1_HelloWorldAsync.md that does the same thing as Sample1_HelloWorld.md using asynchronous code.

DO use #regions in source with a unique identifier starting with “Snippet:” like Snippet:AzConfigSample1_CreateConfigurationClient. This must be unique within the entire repo.

DO C# code fences with the corresponding #region name like so:

```C# Snippet:AzConfigSample1_CreateConfigurationClient
var client = new ConfigurationClient(connectionString);
```

DO make sure all the samples build and run as part of the CI process.

TODO: Update guidance on samples to reflect what we do in most places.

Commonly Overlooked .NET API Design Guidelines

Some .NET Framework Design Guidelines have been notoriously overlooked in earlier Azure SDKs. This section serves as a way to highlight these guidelines.

⚠️ YOU SHOULD NOT have many types in the main namespace. Number of types is directly proportional to the perceived complexity of a library.

⛔️ DO NOT use abstractions unless the Azure SDK both returns and consumes the abstraction. An abstraction is either an interface or abstract class.

⛔️ DO NOT use interfaces if you can use abstract classes. The only reasons to use an interface are: a) you need to “multiple-inherit”, b) you want structs to implement an abstraction.

⛔️ DO NOT use generic words and terms for type names. For example, do not use names like OperationResponse or DataCollection.

⚠️ YOU SHOULD NOT use parameter types where it’s not clear what valid values are supported. For example, do not use strings but only accept certain values in the string.

⛔️ DO NOT have empty types (types with no members).