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.
Namespaces
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();
}
⛔️ 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 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 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 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 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 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 learningAzure.Analytics
for client libraries that gather or process analytics dataAzure.Core
for libraries that aren’t service specificAzure.Data
for client libraries that handle databases or structured data storesAzure.Diagnostics
for client libraries that gather data for diagnostics, including loggingAzure.Identity
for authentication and authorization client librariesAzure.Iot
for client libraries dealing with the Internet of ThingsAzure.Management
for client libraries accessing the control plane (Azure Resource Manager)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.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.
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 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 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 -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.
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 #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.
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).