Azure SDK for .NET (July 2024)

The Azure SDK team ships a .NET changelog every month, and most months it is a long list of package bumps that nobody outside the SDK team reads closely. July 2024 was one of those months on the surface, with 51 packages released across stable, patch, and beta channels. But two changes buried in that list are worth pulling out and understanding properly: a new way to do vector search without managing your own embedding calls, and a new credential type that fixes a real pain point in Azure Pipelines authentication.

This article focuses on those two changes, plus a few other releases from the same batch that are worth knowing about even if you will not touch them this week. If you maintain a .NET service that talks to Azure AI Search or authenticates through Azure DevOps pipelines, this is the release to read carefully.

Azure AI Search 11.6.0: bring your own vectorizer

Before this release, a typical RAG pipeline built on Azure AI Search worked like this: your application code calls an embedding model (Azure OpenAI, or something else) to turn the user query into a vector, then sends that vector to the search index as part of a VectorizedQuery. The vectorization step always happened on the client side, in your code, before the request reached the service.

Azure.Search.Documents 11.6.0 adds VectorSearchVectorizer, with two concrete implementations: AzureOpenAIVectorizer and WebApiVectorizer. You configure a vectorizer once on the index definition, pointing it at your embedding endpoint (an Azure OpenAI deployment, or any HTTP endpoint that returns embeddings). After that, the service handles vectorization internally whenever it receives a VectorizableTextQuery, which takes plain text instead of a pre-computed vector array.

var vectorizer = new AzureOpenAIVectorizer("my-openai-vectorizer")
{
    Parameters = new AzureOpenAIVectorizerParameters
    {
        ResourceUri = new Uri("https://my-aoai-resource.openai.azure.com"),
        DeploymentName = "text-embedding-3-large",
        ApiKey = aoaiApiKey
    }
};
 
var index = new SearchIndex("products-index")
{
    VectorSearch = new VectorSearch
    {
        Vectorizers = { vectorizer },
        Profiles =
        {
            new VectorSearchProfile("products-vector-profile", "hnsw-config")
            {
                VectorizerName = "my-openai-vectorizer"
            }
        }
    }
};

This snippet registers an AzureOpenAIVectorizer against a live Azure OpenAI deployment and attaches it to a vector search profile through VectorizerName. Once the index is created with this configuration, every field that uses this profile knows how to reach the embedding model on its own, without your application ever calling the OpenAI SDK directly for that field.

Querying changes too. Instead of embedding the query text yourself and building a VectorizedQuery, you send a VectorizableTextQuery with the raw text, and the service vectorizes it server-side using the configured vectorizer.

var searchOptions = new SearchOptions
{
    VectorSearch = new()
    {
        Queries = { new VectorizableTextQuery(text: "wireless noise-cancelling headphones")
        {
            KNearestNeighborsCount = 5,
            Fields = { "contentVector" }
        } }
    }
};
 
var response = await searchClient.SearchAsync<ProductDoc>(searchOptions);

The expected output is a normal SearchResults response, same as with a client-vectorized query, but you never had to call an embedding endpoint from your own code to get there. The common pitfall is forgetting that the vectorizer configuration must match the embedding model actually used to build the index. If your index was populated with vectors from text-embedding-ada-002 and your vectorizer points at text-embedding-3-large, you will get results back, but similarity scoring will be quietly wrong because the vector spaces do not line up.

The practical benefit here is architectural, not just a smaller code diff. Centralizing vectorization inside the index configuration means every consumer of that index, whether it is a .NET service, a Python script, or a low-code tool hitting the REST API directly, gets consistent vectorization behavior without reimplementing the embedding call. It also means you can swap the underlying embedding model by updating the vectorizer definition once, instead of hunting down every place in your codebase that calls the embeddings API.

It is worth being direct about the trade-off too. Server-side vectorization adds a network hop from the search service to your embedding endpoint on every query, which is extra latency you do not control as tightly as an in-process call. For latency-sensitive search-as-you-type scenarios, client-side vectorization with a warmed-up embedding client may still be faster. Bring-your-own-endpoint vectorization is a better fit for RAG pipelines and batch or background query workloads where a small amount of added latency does not matter.

Vector compression: BinaryQuantizationCompression and ScalarQuantizationCompression

The same release adds VectorSearchCompression, with two implementations: BinaryQuantizationCompression and ScalarQuantizationCompression. Both reduce the storage footprint of vector fields in an index, which matters once you are indexing millions of documents with 1536 or 3072-dimension embeddings.

var index = new SearchIndex("products-index")
{
    VectorSearch = new VectorSearch
    {
        Compressions = { new ScalarQuantizationCompression("sq-compression")
        {
            RescoringOptions = new RescoringOptions { EnableRescoring = true }
        } },
        Profiles =
        {
            new VectorSearchProfile("products-vector-profile", "hnsw-config")
            {
                CompressionName = "sq-compression"
            }
        }
    }
};

This configures scalar quantization on the vector profile, which compresses full-precision floating point vectors down to lower-precision representations for storage and initial ranking, then optionally rescans the top candidates against the original vectors when RescoringOptions.EnableRescoring is true. The result is a smaller index and faster approximate nearest neighbor search, with a small accuracy trade-off that rescoring mostly offsets.

Binary quantization compresses further than scalar quantization, at a higher accuracy cost, and is the better choice when storage cost is the dominant concern and your embedding model produces vectors with enough dimensions to tolerate aggressive compression. As a rule of thumb, test both against your actual query set before committing, because the accuracy impact depends heavily on which embedding model you are using and how tightly clustered your documents are.

Azure Identity 1.12.0: AzurePipelinesCredential

Azure.Identity 1.12.0 adds AzurePipelinesCredential, built specifically for Azure Pipelines service connections that use workload identity federation. Before this credential existed, authenticating a pipeline task to Azure typically meant either storing a service principal secret in a pipeline variable group, or working around the fact that DefaultAzureCredential has no concept of the pipeline’s OIDC token exchange.

var credential = new AzurePipelinesCredential(
    tenantId: Environment.GetEnvironmentVariable("AZURE_TENANT_ID"),
    clientId: Environment.GetEnvironmentVariable("AZURE_CLIENT_ID"),
    serviceConnectionId: Environment.GetEnvironmentVariable("AZURE_SERVICE_CONNECTION_ID"),
    systemAccessToken: Environment.GetEnvironmentVariable("SYSTEM_ACCESSTOKEN"));
 
var blobClient = new BlobServiceClient(
    new Uri("https://mystorageaccount.blob.core.windows.net"),
    credential);

This exchanges the pipeline’s OIDC token, obtained through SYSTEM_ACCESSTOKEN, for an Azure AD access token scoped to the federated service connection identified by serviceConnectionId. No client secret is stored anywhere in the pipeline. The tenant ID, client ID, and service connection ID typically come from pipeline variables set up alongside the federated credential in Microsoft Entra ID, and SYSTEM_ACCESSTOKEN needs to be explicitly mapped into the pipeline environment since Azure Pipelines does not expose it by default.

The most common setup mistake is forgetting that last step. SYSTEM_ACCESSTOKEN is not automatically available as an environment variable in an Azure Pipelines YAML job; you have to map it explicitly using env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) in the pipeline definition, or the credential throws a CredentialUnavailableException at runtime with a message that does not always make the root cause obvious on first read. The other prerequisite, easy to overlook, is that the federated credential itself has to be configured on the app registration in Entra ID beforehand, pointing at the specific service connection; the .NET code has nothing to configure on that side.

For teams already using workload identity federation with GitHub Actions through DefaultAzureCredential, this is the direct equivalent for Azure Pipelines, and it removes one of the last common reasons a team would still keep a service principal secret sitting in a variable group.

What else shipped, and what to skip

The July batch also carries a breaking behavior change in OpenAI Inference 2.0.0-beta.2 worth flagging if you have adopted Azure OpenAI’s newer client library early. Streaming chat completions picked up a new CancellationToken-supporting method signature, and pairing Azure.AI.OpenAI 2.0.0-beta.1 with OpenAI 2.0.0-beta.5 or later previously threw an error about an unrecognized stream_options argument. This release fixes that pairing, but it is a reminder that beta packages in this family move fast enough that pinning both the Azure wrapper and the underlying OpenAI package to tested, compatible versions matters more than usual.

Azure AI Search 11.6.0 also removes several concepts that had been sitting in preview without making it into the 2024-07-01 GA API version, including index aliases, normalizers, and Azure Machine Learning skills. If your code target-referenced any of those while they were preview-only, this upgrade will break the build, not just the behavior. Checking the changelog’s breaking changes section before bumping a major search dependency is not optional here.

On the Identity side, ManagedIdentityCredential now sets a RefreshOn value at roughly half the token’s remaining lifetime for tokens with more than two hours left, which quietly reduces the number of token requests a long-running service makes against the managed identity endpoint. It is a small change, but it is the kind of thing worth knowing about if you were ever debugging unexplained token endpoint throttling on a service using managed identity under load.

The rest of the 51 packages are mostly patch-level fixes and resource management client updates generated from Azure Resource Manager API definitions, which most teams will never call directly unless they are building infrastructure automation on top of the management plane SDKs rather than Bicep or Terraform.

Should you upgrade now

If you are running Azure AI Search in production with a client-side vectorization pipeline, evaluate the bring-your-own-endpoint vectorizer as an architectural simplification, but do it in a non-production index first, since re-indexing with a different vectorizer configuration is not something you want to discover has scoring implications after the fact. Compression is safe to test incrementally on a copy of your index since it is a profile-level setting, not a breaking API change.

AzurePipelinesCredential is close to a drop-in win for any team currently storing service principal secrets in Azure Pipelines variable groups purely for pipeline-to-Azure authentication. The setup cost is one environment variable mapping and one federated credential configuration in Entra ID, and the payoff is one less secret to rotate and audit.

Leave a Reply

Discover more from Behind the Stack

Subscribe now to keep reading and get access to the full archive.

Continue reading