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 beServiceVersion
; 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.
Create
key, item
Created item
Creates a resource. Throws if the resource exists.
Set
key, item
Creates or replaces a resource.
Update
key, item
item
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.
Get<resource_name>
key
item
Retrieves a resource. Throws if the resource does not exist.
Get<resource_name_plural>
key, item
item
Retrieves one or more resources. Returns empty set if no resources found.
Delete
item
item
Deletes one or more resources, or no-op if the resources do not exist.
Remove
index, item
item
Remove a reference to a resource from a collection. This method doesn’t delete the actual resource, only the reference.
<resource_name>Exists
key
item
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 payloadsbyte[]
- for small payloadsReadOnlyMemory<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.
✅ DO put methods that cannot be called until after the long-running operation has started on the subclass of Operation<T>
. For example, if a service provides an API to cancel an operation, the Cancel
method should appear on the subclass of Operation
.
✔️ 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. Some service operations have intermediate states they pass through prior to completion. These can be represented with an added Status
property to augment the HasCompleted
property on the base Operation
type.
✅ 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>
andIList<T>
for most collectionsIReadOnlyDictionary<T>
andIDictionary<T>
for lookup tablesT[]
,Memory<T>
, andReadOnlyMemory<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 learningAzure.Analytics
for client libraries that gather or process analytics dataAzure.Communication
communication servicesAzure.Core
for libraries that aren’t service specificAzure.Cosmos
for object database technologiesAzure.Data
for client libraries that handle databases or structured data storesAzure.DigitalTwins
for DigitalTwins related technologiesAzure.Identity
for authentication and authorization client librariesAzure.IoT
for client libraries dealing with the Internet of Things.- Use
Iot
for Pascal cased compound words, such asIotClient
, otherwise follow language conventions. - Do not use
IoT
more than once in a namespace.
- Use
Azure.Media
for client libraries that deal with audio, video, or mixed realityAzure.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 technologiesAzure.Security
for client libraries dealing with securityAzure.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 #region
s 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).