API Implementation
This section describes guidelines for implementing Azure SDK client libraries. Please note that some of these guidelines are automatically enforced by code generation tools.
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, so it’s easy to discover. The guidelines in this section describe patterns for the design of a service client.
When configuring your client library, particular care must be taken to ensure that the consumer of your client library can properly configure the connectivity to your Azure service both globally (along with other client libraries the consumer is using) and specifically with your client library.
Service Methods
Each supported language has an Azure Core library that contains common mechanisms for cross cutting concerns such as configuration and doing HTTP requests.
✅ DO use the HTTP pipeline component within Azure::Core
namespace for communicating to service REST endpoints.
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.
HttpPipelinePolicy/Custom Policies
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.
Some services may require custom policies to be implemented. For example, custom policies may implement fall back to secondary endpoints during retry, request signing, or other specialized authentication techniques.
✅ DO use the policy implementations in Azure::Core
whenever possible.
✅ DO review the proposed policy with the Azure SDK [Architecture Board]. There may already be an existing policy that can be modified/parameterized to satisfy your need.
✅ DO ensure thread-safety for custom policies. A practical consequence of this is that you should keep any per-request or connection bookkeeping data in the context rather than in the policy instance itself.
✅ DO document any custom policies in your package. The documentation should make it clear how a user of your library is supposed to use the policy.
Service Method Parameters
Parameter Validation
See general parameter validation guidelines.
⛔️ DO NOT validate service parameters. This includes null checks, empty strings, and other common validating conditions. Let the service validate any request parameters.
Supporting Types
Serialization
JSON Serialization
Enumeration-like Structs
As described in general enumeration guidelines, you should use enum
types whenever passing or deserializing a fixed well-known set of values to or from the service.
There may be times, however, where a struct
is necessary to capture an extensible value from the service even if well-known values are defined,
or to pass back to the service those same or other user-supplied values:
- The value is retrieved and deserialized from service, and may contain a value not supported by the client library.
- The value is roundtripped: the value is retrieved and deserialized from the service, and may later be serialized and sent back to the server.
✅ DO name enum class
es and enumerators using PascalCase.
✅ DO use enum class
for enumerations. For example:
Note that modifying the enumerators in an enumeration is considered a breaking change, if the enumerators in an enumeration may change over time, use an Extendable Enumeration instead.
Extendable Enumerations
When the set of values in an enumeration is not fixed, the Azure::Core::_internal::ExtendableEnumeration
template should be used.
The ExtendableEnumeration type contains the methods for comparison, copying, and conversion to and from string values.
Using Azure Core Types
Implementing Subtypes of Operation<T>
Subtypes of Operation<T>
are returned from service client methods invoking long running operations.
✅ DO check the value of IsDone
in subclass implementations of PollInternal
and PollUntilDoneInternal
and immediately return the result of GetRawResponse
if it is true.
✅ DO throw from methods on Operation<T>
subclasses in the following scenarios.
- If an underlying service operation call from
Poll
orPollUntilDone
throws, re-throwRequestFailedException
or its subtype. - If the operation completes with a non-success result, throw
RequestFailedException
or its subtype fromPoll
orPollUntilDone
.- Include any relevant error state information in the exception message.
-
If the
Value
property is evaluated after the operation failed (HasValue
is false andIsDone
is true) throw the same exception as the one thrown when the operation failed. - If the
Value
property is evaluated before the operation is complete (IsDone
is false) throwTODO: What to throw
.- The exception message should be: “The operation has not yet completed.”
Azure::Core::Context
✅ DO always pass Azure::Core::Context
types by reference, preferably by const
reference.
⛔️ DO NOT cancel the input Context parameter.
The cancellation requirement stems from the nature of an Azure::Core::Context
object - there may be multiple Azure::Core::Context
objects which share the same cancellation context - cancelling one cancels all of the shared contexts. To avoid this, a service client should instead create a new Azure::Core::Context
from the original context using either Context::WithValue
, Context::WithDeadline
and cancel that new context.
SDK Feature Implementation
Configuration
✅ DO use relevant global configuration settings either by default or when explicitly requested to by the user, for example by passing in a configuration object to a client constructor.
✅ DO allow different clients of the same type to use different configurations.
✅ DO allow consumers of your service clients to opt out of all global configuration settings at once.
✅ DO allow all global configuration settings to be overridden by client-provided options. The names of these options should align with any user-facing global configuration keys.
⛔️ DO NOT change behavior based on configuration changes that occur after the client is constructed. Hierarchies of clients inherit parent client configuration unless explicitly changed or overridden. Exceptions to this requirement are as follows:
- Log level, which must take effect immediately across the Azure SDK.
- Tracing on/off, which must take effect immediately across the Azure SDK.
Configuration via Environment Variables
✅ DO prefix Azure-specific environment variables with AZURE_
.
✔️ YOU MAY use client library-specific environment variables for portal-configured settings which are provided as parameters to your client library. This generally includes credentials and connection details. For example, Service Bus could support the following environment variables:
AZURE_SERVICEBUS_CONNECTION_STRING
AZURE_SERVICEBUS_NAMESPACE
AZURE_SERVICEBUS_ISSUER
AZURE_SERVICEBUS_ACCESS_KEY
Storage could support:
AZURE_STORAGE_ACCOUNT
AZURE_STORAGE_ACCESS_KEY
AZURE_STORAGE_DNS_SUFFIX
AZURE_STORAGE_CONNECTION_STRING
✅ DO get approval from the [Architecture Board] for every new environment variable.
✅ DO use this syntax for environment variables specific to a particular Azure service:
AZURE_<ServiceName>_<ConfigurationKey>
Where ServiceName is the canonical shortname without spaces, and ConfigurationKey refers to an unnested configuration key for that client library.
⛔️ DO NOT use non-alpha-numeric characters in your environment variable names with the exception of underscore. This ensures broad interoperability.
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
).
C++ Logging specific details
Client libraries must support robust logging mechanisms so that the consumer can adequately diagnose issues with the method calls and quickly determine whether the issue is in the consumer code, client library code, or service.
In general, our advice to consumers of these libraries is to establish logging in their preferred manner at the WARNING
level or above in production to capture problems with the application, and this level should be enough for customer support situations. Informational or verbose logging can be enabled on a case-by-case basis to assist with issue resolution.
✅ DO use the Azure Core library for logging.
✅ DO support pluggable log handlers.
✅ DO make it easy for a consumer to enable logging output to the console. The specific steps required to enable logging to the console must be documented.
✅ DO use one of the following log levels when emitting logs: Verbose
(details), Informational
(things happened), Warning
(might be a problem or not), and Error
.
✅ DO use the Error
logging level for failures that the application is unlikely to recover from (out of memory, etc.).
✅ DO use the Warning
logging level when a function fails to perform its intended task. This generally means that the function will raise an exception. Do not include occurrences of self-healing events (for example, when a request will be automatically retried).
✔️ YOU MAY log the request and response (see below) at the Warning
when a request/response cycle (to the start of the response body) exceeds a service-defined threshold. The threshold should be chosen to minimize false-positives and identify service issues.
✅ DO use the Informational
logging level when a function operates normally.
✅ DO use the Verbose
logging level for detailed troubleshooting scenarios. This is primarily intended for developers or system administrators to diagnose specific failures.
✅ DO only log headers and query parameters that are in a service-provided “allow-list” of approved headers and query parameters. All other headers and query parameters must have their values redacted.
✅ DO log request line and headers as an Informational
message. The log should include the following information:
- The HTTP method.
- The URL.
- The query parameters (redacted if not in the allow-list).
- The request headers (redacted if not in the allow-list).
- An SDK provided request ID for correlation purposes.
- The number of times this request has been attempted.
✅ DO log response line and headers as an Informational
message. The format of the log should be the following:
- The SDK provided request ID (see above).
- The status code.
- Any message provided with the status code.
- The response headers (redacted if not in the allow-list).
- The time period between the first attempt of the request and the first byte of the body.
✅ DO log an Informational
message if a service call is cancelled. The log should include:
- The SDK provided request ID (see above).
- The reason for the cancellation (if available).
✅ DO log exceptions thrown as a Warning
level message. If the log level set to Verbose
, append stack trace information to the message.
Distributed Tracing
✅ DO abstract the underlying tracing facility, allowing consumers to use the tracing implementation of their choice.
✅ DO create a new trace span for each API call. New spans must be children of the span that was passed in.
Telemetry
Client library usage telemetry is used by service teams (not consumers) to monitor what SDK language, client library version, and language/platform info a client is using to call into their service. Clients can prepend additional information indicating the name and version of the client application.
✅ DO send telemetry information in the [User-Agent header] using the following format:
[<application_id> ]azsdk-cpp-<package_name>/<package_version> <platform_info>
<application_id>
: optional application-specific string. May contain a slash, but must not contain a space. The string is supplied by the user of the client library, e.g. “XCopy/10.0.4-Preview”<package_name>
: client library (distribution) package name as it appears to the developer, replacing slashes with dashes and removing the Azure and cpp indicator. For example, “azure-security-keyvault-secrets-cpp” would specify “azsdk-cpp-security-keyvault-secrets”.<package_version>
: the version of the package. Note: this is not the version of the service<platform_info>
: information about the currently executing language runtime and OS, e.g. “MSVC/14.10.0(Windows-10-10.0.19041-SP0)”
☑️ YOU SHOULD send additional (dynamic) telemetry information as a semi-colon separated set of key-value types in the X-MS-AZSDK-Telemetry
header. For example:
X-MS-AZSDK-Telemetry: class=BlobClient;method=DownloadFile;blobType=Block
The content of the header is a semi-colon key=value list. The following keys have specific meaning:
class
is the name of the type within the client library that the consumer called to trigger the network operation.method
is the name of the method within the client library type that the consumer called to trigger the network operation.
Any other keys that are used should be common across all client libraries for a specific service. DO NOT include personally identifiable information (even encoded) in this header. Services need to configure log gathering to capture the X-MS-SDK-Telemetry
header in such a way that it can be queried through normal analytics systems.
Testing
We believe testing is a part of the development process, so we expect unit and integration tests to be a part of the source code. All components must be covered by automated testing, and developers should strive to test corner cases and main flows for every use case.
All code should contain, at least, requirements, unit tests, end-to-end tests, and samples.
Tests should be written using the [Google Test][] library.
Language-specific other
Unlike many other programming languages, which have large runtimes, the C++ standard runtime is limited in functionality and scope. The standard library covers areas such as memory and string manipulation, standard input/output, floating point and others. However, many of the features required for modern applications and services; e.g. those required for networking and advanced memory management are not part of the standard library. Instead, many of those features are included in open source C++ libraries that are also cross-platform with good support for Windows, OSX and most Linux platforms. Because of that support and because Azure SDK implementations will need such functionality, it is expected that client libraries will take dependencies on these libraries. Ensure the version matches to allow for compatibility when an application integrates multiple client libraries.
✅ DO utilize the following libraries as needed for commonly required operations:
Library | Version | Usage |
---|---|---|
libxml2 | 2.7.6 | XML parser - https://www.xmlsoft.org/ |
gtest | 1.10.0 | Google Test - https://github.com/google/googletest/ |
Note: None of the libraries in this list currently throw C++ exceptions; policy for propagation of C++ exceptions from dependencies needs to be evaluated on a dependency-by-dependency basis.
⛔️ DO NOT use implicit assignment inside a test. This is generally an accidental omission of the second =
of the logical compare. The following is confusing and prone to error.
Does the programmer really mean assignment here? Sometimes yes, but usually no. Instead use explicit tests and avoid assignment with an implicit test. The recommended form is to do the assignment before doing the test:
⛔️ DO NOT use the register keyword. Modern compilers will put variables in registers automatically.
✅ DO be const
correct. C++ provides the const
keyword to allow passing as parameters objects that cannot change to indicate when a method doesn’t modify its object. Using const
in all the right places is called “const correctness.”
✅ DO use #if
instead of #ifdef
. For example:
Someone else might compile the code with turned-of debug info like:
Alway use #if
if you have to use the preprocessor. This works fine, and does the right thing, even if DEBUG
is not defined at all (!)
If you really need to test whether a symbol is defined or not, test it with the defined()
construct, which allows you to add more things later to the conditional without editing text that’s already in the program:
✅ DO
Use #if
to comment out large code blocks.
Sometimes large blocks of code need to be commented out for testing. The easiest way to do this is with an #if 0
block:
You can’t use /**/
style comments because comments can’t contain comments and a large block of your code will probably contain connects.
Do not use #if 0
directly. Instead, use descriptive macro names:
Always add a short comment explaining why it is commented out.
⛔️ DO NOT put data definitions in header files. For example, this should be avoided:
It’s bad magic to have space consuming code silently inserted through the innocent use of header files. It’s not common practice to define variables in the header file, so it will not occur to developers to look for this when there are problems. Instead, define the variable once in a source file and then use an extern
statement to reference it in the header file.
⛔️ DO NOT use magic numbers. A magic number is a bare naked number used in source code. It’s magic because no-one will know what it means after a while. This significantly reduces maintainability. For example:
Instead of magic numbers, use a real name that means something. You can use constexpr
for names. For example:
✅ DO check every system call for an error return, unless you know you wish to ignore errors. For example, printf
returns an error code but it is rarely relevant. Cast the return to (void) if you do not care about the error code.
✅ DO include the system error text when reporting system error messages.
☑️ YOU SHOULD follow the CPP Core Guidelines whenever possible, with the following exceptions:
CentOS
Oracle Linux | 7+ | x64 | gcc-4.8 | [Red Hat lifecycle](https://access.redhat.com/support/policy/updates/errata/)
[CentOS lifecycle](https://www.centos.org/centos-linux-eol/)
[Oracle Linux lifecycle](https://www.oracle.com/us/support/library/elsp-lifetime-069338.pdf) | Debian | 9+ | x64 | gcc-6.3 | [Debian lifecycle](https://wiki.debian.org/DebianReleases) | Ubuntu | 18.04, 16.04 | x64 | gcc-7.3 | [Ubuntu lifecycle](https://wiki.ubuntu.com/Releases) | Linux Mint | 18+ | x64 | gcc-7.3 | [Linux Mint lifecycle](https://www.linuxmint.com/download_all.php) | openSUSE | 15+ | x64 | gcc-7.5 | [OpenSUSE lifecycle](https://en.opensuse.org/Lifetime) | SUSE Enterprise Linux (SLES) | 12 SP2+ | x64 | gcc-4.8 | [SUSE lifecycle](https://www.suse.com/lifecycle/) ☑️ YOU SHOULD support the following additional platforms and associated compilers when implementing your client library. ⚠️ YOU SHOULD NOT use compiler extensions. Examples of extensions to avoid include: * [MSVC compiler extensions](https://docs.microsoft.com/cpp/build/reference/microsoft-extensions-to-c-and-cpp) * [clang language extensions](https://clang.llvm.org/docs/LanguageExtensions.html) * [GNU C compiler extensions](https://gcc.gnu.org/extensions.html) Use the appropriate options for each compiler to prevent the use of such extensions. ✅ DO use compiler flags to identify warnings: | Compiler | Compiler Flags | |:-------------------------|------------------| | gcc | `-Wall -Wextra` | | cpp and XCode | `-Wall -Wextra` | | MSVC | `/W4` |