Understanding the Azure Core library for .NET

Anne Thompson

One of the goals of the Azure SDK is to provide a unified developer experience across Azure. That means that basic capabilities—like authenticating with a service or retrieving a value from service—should work the same way, regardless of what Azure service you’re working with. To achieve this, Azure SDK client libraries use common patterns that you can learn once, and then use with any library. Many of the types used in these common patterns live in a foundational library called Azure Core.

This post gives an introduction to the .NET Azure.Core library and some of the most common types in it. We’ll start with a sample that introduces the standard usage pattern for an Azure SDK client, and show how Azure.Core enables consistent use of that pattern across .NET clients. Then, we’ll give an overview of the library’s namespaces and how they organize the types that the library holds. Finally, we’ll go through some of the types you would use when building applications with the Azure SDK.

This article is primarily focused on the .NET Azure.Core library, but every Azure SDK language has an Azure Core library that implements similar patterns. The main features of the Azure Core libraries are described in the Azure SDK General Guidelines, and the guidance regarding how client libraries should use them in each of the SDK languages are linked from there. Language-specific libraries can be found at the following links:

Other language libraries are also available on our Azure SDK Releases page.

The Azure SDK for .NET client pattern

To get started, let’s look at the standard usage pattern for a typical Azure SDK for .NET client. This sample uses the Azure.AI.TextAnalytics library to analyze the sentiment of some text with the Azure Text Analytics service.

using Azure.AI.TextAnalytics;
using Azure.Identity;

TextAnalyticsClient client = new TextAnalyticsClient(
    new Uri("https://example.com"), 
    new DefaultAzureCredential());

DocumentSentiment analysis = client.AnalyzeSentiment("Today was great!");

Console.WriteLine($"Text sentiment is {analysis.Sentiment}.");

When you write an application using the Azure SDK for .NET, you start by creating an instance of a service client. You then invoke a service method on the client to call its corresponding Azure service. The service method returns a model type, that you can then use in your application. In our example, we simply print the sentiment inferred from the text to the console.

So how does Azure.Core support this pattern?

In this example, Azure.Core types are used in both the client constructor and the service method. In the client constructor, we pass a DefaultAzureCredential that is defined in the Azure.Identity library, but it inherits from Azure.Core‘s TokenCredential type. Many Azure SDK clients take DefaultAzureCredential or another subtype of TokenCredential to authenticate with the service. The service method AnalyzeSentiment returns a templated type Response<T> that is also defined in Azure.Core. Response<T> has an implicit cast)-t) to T, which represents the value you retrieve from the service. In this case, our T is the DocumentSentiment model type.

We’ll look more closely at these and other Azure.Core types in the following sections. But before we do, let’s talk about how the library is organized and what it’s doing at a high level.

What’s in Azure.Core?

As we mentioned earlier, the goal of Azure.Core is to help ensure consistency across Azure SDK libraries. That means it provides types that are used in the common patterns the client libraries implement. Some of these types are used in the client libraries’ public APIs, and some of them are used to implement common behaviors without being visible externally.

At a high level, we think of the library as having two groups of users:

  1. The consumers of our client libraries, who use types like Response<T> that appear in clients’ public APIs. Broadly speaking, types for end-user developers can be found in Azure.Core‘s Azure namespace.
  2. The developers who write the client libraries themselves. Types used to implement the SDK clients live primarily in the Azure.Core namespace or one of the specialized namespaces like Azure.Core.Serialization.

In general, types that are intended for consumers have simpler APIs and usage patterns. Types in the specialized namespaces may be more complex since they’re intended for use with more advanced scenarios.

One specialized namespace worth noting is the Azure.Core.Pipeline namespace. Clients for HTTP-based Azure services use an HTTP pipeline to exchange messages with the service, and Azure.Core implements this pipeline and related types. The pipeline is composed of a sequence of policies that implement functionality common to all libraries, such as authentication, retry-handling logic, logging, and tracing. Requests flow through from the first policy to the transport, and responses travel back through them in the opposite direction. Details of the pipeline architecture are described in Jeffrey Richter’s article Inside the Azure SDK Architecture, so we won’t go into many of those details here. Types that implement the pipeline are defined in the Azure.Core.Pipeline namespace.

For the rest of this post, we’ll look more closely at some of the types that appear in Azure SDK clients public APIs.

Response types

As mentioned before, Azure SDK clients for HTTP-based services have service methods you can call to invoke an operation on the service. When the service sends back a response, the service method returns it to you as one of the response types defined in the Azure namespace. The most common of these types are Response<T>, Pageable<T>, and Operation<T>.

Response<T>

Most service methods for operations on HTTP-based services return Response<T>, like the AnalyzeSentiment method shown at the start of this post. The T in Response<T> is typically a model type—a strongly typed representation of a resource defined by the service you’re working with. The model value can be accessed by using the implicit cast to T, as shown in the sample, or by using Response<T>‘s Value property. For example, the earlier sample could be rewritten to access the DocumentSentiment model like this:

Response<DocumentSentiment> response = client.AnalyzeSentiment("Today was great!");
DocumentSentiment analysis = response.Value;

The only other member defined on Response<T> is the GetRawResponse method. Calling GetRawResponse returns a Response object, which holds many of the raw details of the service’s HTTP response. Response is considered a more advanced type and—since the response’s payload has been abstracted into the strongly typed model—it shouldn’t be needed for most common scenarios. This means you as a developer can focus on the application logic that uses the resource, without having to reason about the details of the messaging protocol. If you have a more advanced use case and need access to details that aren’t exposed on the model type, you can always get to those by calling GetRawResponse on Response<T>. For example, if you needed to read the HTTP status code that the service returned, you might write:

Response<DocumentSentiment> response = client.AnalyzeSentiment("Today was great!");
Response httpResponse = response.GetRawResponse();
Console.WriteLine($"HTTP status code is {httpResponse.Status}.");

You can also access HTTP headers from Response. For example, if you needed more information about the response message’s content, you might write:

Response<DocumentSentiment> response = client.AnalyzeSentiment("Today was great!");
Response httpResponse = response.GetRawResponse();
Console.WriteLine("Content-Length is " + httpResponse.Headers.ContentLength);
Console.WriteLine("Content-Type is " + httpResponse.Headers.ContentType);

Pageable<T>

Some service operations let you retrieve a collection of resources held on the service. Many of these service APIs let you request the elements of the collection in separate groups, called pages, to prevent the response size from becoming too large. Managing paged requests is relatively complex, and we wanted .NET users to be able to treat these collections like any other .NET collection type. The Pageable<T> type lets you do this.

At its simplest, you can take the Pageable<T> object returned from a service method that returns a collection, and foreach over it to iterate through the items in the collection. For example, to retrieve a collection of ConfigurationSetting values from the Azure App Configuration service and print them to the screen, you might write:

Pageable<ConfigurationSetting> settings = client.GetConfigurationSettings();
foreach (ConfigurationSetting setting in settings)
{
    Console.WriteLine(setting.Value);
}

Under the hood, Pageable<T> handles all the requests needed to obtain the first page, keep track of the most recent collection item you’ve seen, and request subsequent pages as needed. As a user, you just see an IEnumerable that returns each element of the collection as you iterate through the enumeration.

Further details regarding how to use Pageable<T> and its async counterpart AsyncPageable<T> can be found in the article Pagination with the Azure SDK for .NET.

Operation<T>

Some service operations take a long time to process, so the service isn’t able to return a response immediately. Azure services use long-running operations (LROs) to model these “service asynchronous” operations. Like managing paged requests, handling the sequence of calls needed to start an LRO, poll for updates, and then retrieve the result once the operation has completed can require writing complex code. The Operation<T> type is designed to make handling LROs a little easier.

The following example shows how Operation<T> is used when you search for addresses with the Azure Maps client.

List<SearchAddressQuery> queries = new List<SearchAddressQuery>
{
    new SearchAddressQuery("One Microsoft Way, Redmond, WA 98052"),
    new SearchAddressQuery("15010 NE 36th St, Redmond, WA 98052")
};

SearchAddressBatchOperation operation = client.SearchAddressBatch(WaitUntil.Started, queries);
Response<SearchAddressBatchResult> result = operation.WaitForCompletion();

In this example, the SearchAddressBatch method returns a SearchAddressBatchOperation object, which inherits from Operation<T>. The T in this example is a SearchAddressBatchResult, and this is the model type that’s returned when the operation completes. Calling SearchAddressBatch sends a message to the service telling it to begin the LRO. After that, the Operation<T> type internally polls the service to find out whether the service has finished computing the result. When the operation has completed, Operation<T> does the work to collect the output resource from the appropriate service endpoint and make it available on the client.

As a developer working with Operation<T>, you obtain the output value of the operation as a Response<T> from the WaitForCompletion method, or from its async counterpart, WaitForCompletionAsync. Once the operation has successfully completed, you can also retrieve the result object from the operation’s Value property, which is similar to the Value property on Response<T>.

Service methods that start an LRO on the service are named the same way as other service methods—with the name of the operation. In our example, the service method is called SearchAddressBatch. You can change how the control flow returns from the service method by passing a WaitUntil enum value. If you pass WaitUntil.Started, the method will return after the first message is sent to the service telling it to start the operation. If you pass WaitUntil.Completed, the method returns when the operation completes on the service and the result is available. For example, we could rewrite the previous example without the call to WaitForCompletion by doing the following:

SearchAddressBatchOperation operation = client.SearchAddressBatch(WaitUntil.Completed, queries);
Response<SearchAddressBatchResult> result = operation.Value;

More details of the Operation<T> API can be found in the documentation for the Operation<T> class.

Error types

Sometimes a service needs to notify you of an error, either because of a problem with the input it received or because of a problem on the service side. In .NET, when an error occurs, we throw an exception. Today in Azure.Core, we have only one exception type to indicate that a service request failed, and that is RequestFailedException.

Like other common types appearing in our public APIs, RequestFailedException lives in the Azure namespace. A few of the details of the error response are available as properties on the exception, including the response status code and the Azure-standard error code. Beyond that, the client parses details from the error message returned by the service and makes them available in the exception’s Message property. If details of the HTTP response are needed programmatically at runtime, they can be accessed from the Response returned by calling the exception’s GetRawResponse method, the same way you access these from Response<T>.

This makes the code you write to handle service errors work the same way as exception handling code in any .NET application. For example, to request a secret from Azure Key Vault, you might write:

try
{
    KeyVaultSecret secret = client.GetSecret("NonexistentSecret");
}
catch (RequestFailedException e) when (e.Status == 404)
{
    Console.WriteLine("ErrorCode " + e.ErrorCode);
}

More details of the RequestFailedException API can be found in the documentation for the RequestFailedException class.

Client configuration

So far, we’ve covered some of the most common types in Azure.Core, types in the Azure namespace that you could expect to see in any application built using the Azure SDK. The last type we’ll cover in this post is in the Azure.Core namespace, and as you might expect from that, enables more advanced scenarios.

Most of the time, you construct an Azure SDK client by passing the service endpoint and authentication details as we showed in the first example in this post. Sometimes, however, you might need to do something specialized, like work with a specific version of a service, or log the content of HTTP messages sent and received by the client. To do this, you need to pass special configuration options to the client using its ClientOptions type.

The following example shows how to create an instance of an Azure Key Vault SecretClient that logs the content of request and response messages:

SecretClientOptions options = new SecretClientOptions()
{
    Diagnostics =
    {
        IsLoggingContentEnabled = true
    }
};

SecretClient client = new SecretClient(Uri("https://example.com"), new DefaultAzureCredential(), options);

Notice that we created an instance of SecretClientOptions and set its DiagnosticOptions property IsLoggingContentEnabled to true. Every Azure SDK for .NET client constructor for an HTTP-based Azure service has an overload that takes a subtype of ClientOptions. For the Key Vault SecretClient, the client options type is called SecretClientOptions. Every client-specific options type inherits from the ClientOptions type in Azure.Core. Some client options types add configuration options specific to the service, but deriving from Azure.Core‘s ClientOptions type means they all implement the same core set of functionality.

The set of things you can configure using ClientOptions fall into four main buckets:

  • Diagnostics
  • Retries
  • The HTTP pipeline
  • The pipeline transport

The configurable options for diagnostics are defined on the DiagnosticOptions type, and primarily control client logging and distributed tracing. How the client retries HTTP requests can be configured using the client options RetryOptions, which let you configure the pipeline’s default retry policy, and change values like the time between retries and the maximum number of retries to send. Alternatively, you can take complete control of how retries are sent by replacing the default retry policy with your own RetryPolicy implementation.

If you need to make changes to the client’s HTTP pipeline, ClientOptions lets you insert your own policy implementations into the pipeline using the AddPolicy method. And at the lowest level, you can replace the pipeline’s HttpPipelineTransport with your own implementation using the client options Transport property. To succeed at reconfiguring the client pipeline requires a good knowledge of types in the Azure.Core.Pipeline namespace, and is a topic for a later blog post about Azure.Core.

What else?

We hope this overview of the Azure.Core library helps explain a little about the library and the types it contains, as well as patterns you’ll see across all our Azure SDK clients. We’re always looking for ways to make our products great for your specific needs, so leave suggestions and feedback in our GitHub repo. If there are topics you’d like to dive into in more depth in a later blog post on Azure.Core, let us know by posting in the comments, or leaving feedback for us on GitHub. We look forward to hearing from you about your experiences, and even more, to working with you to improve the Azure SDK!

2 comments

Discussion is closed. Login to edit/delete existing comments.

  • Michael Taylor 1

    Looks like there is a markdown issue for the list of things that `ClientOptions` provide. It looks like the last bullet is actually a bullet followed by a paragraph of text but it is being rendered as part of the bullet.

    • Scott AddieMicrosoft employee 0

      Thanks for reading and for making us aware of this problem. It has been addressed.

Feedback usabilla icon