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.

Currently, the document describes guidelines for client libraries exposing HTTP/REST services. It may be expanded in the future to cover other, non-REST, services.

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. 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 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 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.

API Design

Service Client Design

Azure services will be exposed to .NET developers as one or more service client types, and a set of supporting types. Service clients are the main starting points for developers trying to call Azure services, and each client library should have at least one client in its main namespace. The guidelines in this section describe patterns for the design of a service client. A service client should look like 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 : HttpPipelineOptions {
        ...
    }
}

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 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. Guidelines for using ClientOptions can be found in Using ClientOptions section below.

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);
}

DO provide protected parameterless constructor for mocking.

public class ConfigurationClient {
    protected ConfigurationClient();
}

See Supporting Mocking for details.

Service Methods

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);
}

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

Many developers want to port existing application to the Cloud. These application 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. Azure SDK is providing 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.

DO ensure all service methods, both asynchronous and synchronous, take an optional CancellationToken parameter called cancellationToken.

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.

DO make service methods virtual.

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

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

T represents the content of the response. For more information, see Service Method Return Types.

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. For more information, see Service Method Return Types.

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

Service Method Return Types

As mentioned above, service methods will often return Response<T>. The T can be either an unstructured payload (e.g. bytes of a storage blob) or a model type representing deserialized response content. This section describes guidelines for the design of unstructured return types, model types, and all their transitive closure of public dependencies (i.e. the model graph).

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 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 perfromance are critical

Note that this guidance does not apply to input parameters. Input parameters representing collections should follow standard .NET 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 Service Method Return Types.

✔️ 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 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 Supporting Mocking for details.

Returning Collections

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.

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, complext 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 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; }
    ...
}

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.

Parameter Validation

Service methods take two kinds of parameters: service parameters and client parameters. Service parameters are directly passed across the wire to the service. Client parameters are used within the client library and aren’t passed directly to the service.

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.

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.

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

    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 abstract ValueTask<Response<T>> WaitForCompletionAsync(CancellationToken cancellationToken = default);
    public abstract ValueTask<Response<T>> WaitForCompletionAsync(TimeSpan pollingInterval, CancellationToken cancellationToken);
}

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 StartCopyFromUri(..., CancellationToken cancellationToken = default);
    public virtual Task<CopyFromUriOperation> StartCopyFromUriAsync(..., CancellationToken cancellationToken = default);
}

The Operation object can be used to poll for a response.

BlobBaseClient client = ...

// automatic polling
{
    var value = await client.StartCopyFromUri(...).WaitForCompletionAsync();
    Console.WriteLine(value);
}

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

// saving operation ID
{
    CopyFromUriOperation operation = await client.StartCopyFromUriAsync(...);
    string operationId = operation.Id;

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

DO name all methods that start an LRO with the Start prefix.

DO return a subclass of Operation<T> from 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.

Supporting Mocking

All client libraries must support mocking. 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);

Review the full sample in the GitHub repository.

DO make all service methods virtual.

DO provide protected parameterless constructor for mocking.

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 must 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);
}

Authentication

The client library consumer should construct a service client using just the constructor. After construction, service methods can be called successfully. 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.

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.

General Azure SDK Library Design

Namespace Naming

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.Core for libraries that aren’t service specific
  • Azure.Data for client libraries that handle databases or structured data stores
  • Azure.Diagnostics for client libraries that gather data for diagnostics, including logging
  • Azure.Identity for authentication and authorization client libraries
  • Azure.Iot for client libraries dealing with the Internet of Things
  • Azure.Management for client libraries accessing the control plane (Azure Resource Manager)
  • 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.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.

Error Reporting

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(int status, string message);
    public RequestFailedException(int status, string message, Exception innerException);

    public int Status { get; }
}

☑️ YOU SHOULD use ResponseExceptionExtensions to create RequestFailedException instances.

The exception message should contain detailed response information. For example:

if (response.Status != 200) {
    throw await response.CreateRequestFailedExceptionAsync(message);
}

DO use RequestFailedException or one of its subtypes where possible.

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

Logging

Request logging will be done automatically by the HttpPipeline. If a client library needs to add custom logging, follow the same guidelines and mechanisms as the pipeline logging mechanism. If a client library wants to do custom logging, the designer of the library must ensure that the logging mechanism is pluggable in the same way as the HttpPipeline logging policy.

DO follow the logging section of the Azure SDK General Guidelines if logging directly (as opposed to through the HttpPipeline).

Distributed Tracing

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>

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] 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.

Versioning

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

For detailed rules, see .NET Breaking Changes.

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 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.

⛔️ 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");
}

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 -preview.N suffix for preview package versions. For example, 1.0.0-preview.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.

Documentation

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.

Common Type Usage

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 plural or more general name. For example, the BlobClientsOptions 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.

Using HttpPipeline

The following example shows a typical way of using HttpPipeline to implement a service call method. The HttpPipeline will handle common HTTP requirements such as the user agent, logging, distributed tracing, retries, and proxy configuration.

public virtual async Task<Response<ConfigurationSetting>> AddAsync(ConfigurationSetting setting, CancellationToken cancellationToken = default)
{
    if (setting == null) throw new ArgumentNullException(nameof(setting));
    ... // validate other preconditions

    // Use HttpPipeline _pipeline filed of the client type to create new HTTP request
    using (Request request = _pipeline.CreateRequest()) {

        // specify HTTP request line
        request.Method = RequestMethod.Put;
        request.Uri.Reset(_endpoint);
        request.Uri.AppendPath(KvRoute, escape: false);
        requast.Uri.AppendPath(key);

        // add headers
        request.Headers.Add(IfNoneMatchWildcard);
        request.Headers.Add(MediaTypeKeyValueApplicationHeader);
        request.Headers.Add(HttpHeader.Common.JsonContentType);
        request.Headers.Add(HttpHeader.Common.CreateContentLength(content.Length));

        // add content
        ReadOnlyMemory<byte> content = Serialize(setting);
        request.Content = HttpPipelineRequestContent.Create(content);

        // send the request
        var response = await Pipeline.SendRequestAsync(request).ConfigureAwait(false);

        if (response.Status == 200) {
            // deserialize content
            Response<ConfigurationSetting> result = await CreateResponse(response, cancellationToken);
        }
        else
        {
            throw await response.CreateRequestFailedExceptionAsync(message);
        }
    }
}

For a more complete example, see the configuration client implementation.

Using HttpPipelinePolicy

The HTTP pipeline includes a number of policies that all requests pass through. Examples of policies include setting required headers, authentication, generating a request ID, and implementing proxy authentication. HttpPipelinePolicy is the base type of all policies (plugins) of the HttpPipeline. This section describes guidelines for designing custom policies.

DO inherit from HttpPipelinePolicy if the policy implementation calls asynchronous APIs.

See an example here.

DO inherit from HttpPipelineSynchronousPolicy if the policy implementation calls only synchronous APIs.

See an example here.

DO ensure ProcessAsync and Process methods are thread safe.

HttpMessage, Request, and Response don’t have to be thread-safe.

JSON Serialization

DO use System.Text.Json package to write and read JSON content.

☑️ YOU SHOULD use Utf8JsonWriter to write JSON payloads:

var json = new Utf8JsonWriter(writer);
json.WriteStartObject();
json.WriteString("value", setting.Value);
json.WriteString("content_type", setting.ContentType);
json.WriteEndObject();
json.Flush();
written = (int)json.BytesWritten;

☑️ YOU SHOULD use JsonDocument to read JSON payloads:

using (JsonDocument json = await JsonDocument.ParseAsync(content, default, cancellationToken).ConfigureAwait(false))
{
    JsonElement root = json.RootElement;

    var setting = new ConfigurationSetting();

    // required property
    setting.Key = root.GetProperty("key").GetString();

    // optional property
    if (root.TryGetProperty("last_modified", out var lastModified)) {
        if(lastModified.Type == JsonValueType.Null) {
            setting.LastModified = null;
        }
        else {
            setting.LastModified = DateTimeOffset.Parse(lastModified.GetString());
        }
    }
    ...

    return setting;
}

☑️ YOU SHOULD consider using Utf8JsonReader to read JSON payloads.

Utf8JsonReader is faster than JsonDocument but much less convenient to use.

DO make your serialization and deserialization code version resilient.

Optional JSON properties should be deserialized into nullable model properties.

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.

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.

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
---

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.

Commonly Overlooked .NET API Design Guidelines

Some .NET Design Guidelines have been notoriously overlooked in existing 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).