Use the async Always Encrypted key store APIs on async execution paths - #4585
Use the async Always Encrypted key store APIs on async execution paths#4585cheenamalhotra wants to merge 7 commits into
Conversation
Async command execution now resolves Always Encrypted key material through the asynchronous key store provider APIs, so a provider that performs network I/O (for example Azure Key Vault) no longer blocks a thread pool thread for the duration of every column encryption key decryption, column master key signature verification, and enclave package generation. Utility layer (Phase 4): * SqlSecurityUtility gains DecryptSymmetricKeyAsync, GetKeyFromLocalProvidersAsync and VerifyColumnMasterKeySignatureAsync. * SqlSymmetricKeyCache gains GetKeyAsync, which never holds the cache lock across provider I/O. Call sites (Phase 5): * sp_describe_parameter_encryption result parsing is now shared by the sync and async paths. Signature verifications and key decryptions are recorded while parsing and executed afterwards, so no key store call is made while the reader is still positioned mid-stream. * The two callback-based describe-parameter-encryption continuations collapse into a single async method, preserving the existing thread pool hand-off and error/cancellation semantics. * EnclaveDelegate and SqlCommand gain async enclave package generation, used by the async execution path. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
… store calls The async key store provider APIs all accept a CancellationToken, but the Always Encrypted call sites had no way to reach the token supplied to ExecuteReaderAsync / ExecuteNonQueryAsync / ExecuteXmlReaderAsync, so key store I/O (for example an Azure Key Vault round trip) could not be cancelled. Capture the token in a per-execution field on SqlCommand, set at each async entry point and cleared by the matching cleanup continuation, and pass it to the describe-parameter-encryption key operations and to enclave package generation. A field is used rather than signature plumbing because the token would otherwise have to be threaded through several sync-only overloads of RunExecuteReader and PrepareForTransparentEncryption. Also add a manual test that registers a recording key store provider and asserts that the synchronous execution paths use the synchronous provider API, the asynchronous paths use the asynchronous provider API, and that cancelling the caller's token cancels the key store operation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 82aca8ca-2078-4a47-860c-6f6ee78cbb02
On a query metadata cache hit the driver still loaded every column encryption key synchronously, on the caller's thread, and then generated the enclave package with the synchronous key store provider API. Both can perform key store network I/O, so an ExecuteReaderAsync that hit the metadata cache could still block the caller. Split the cache lookup into an in-memory probe (TryGetCachedQueryMetadata), which matches the cached cipher metadata onto the parameters and reports which keys still need decrypting, and a completion step that loads those keys. The synchronous lookup is now expressed in terms of the same probe, so its behaviour, including the stale key fallback and the cache hit/miss counters, is unchanged. Asynchronous execution now takes the probe and returns a task for the key loads, which means the cache hit path produces a describe-parameter-encryption task exactly like the cache miss path does. As a result it also picks up the asynchronous enclave package generation and no longer issues the command's TDS write from the caller's thread. When the cached key information turns out to be stale, that task falls back to a full describe parameter encryption round trip. The command still reports usedCache = true in that case because the caller has already returned; this is deliberately conservative, since over-reporting a cache hit can only cost one extra retry of an already failing execution, whereas under-reporting it would suppress the retry a genuine cache hit needs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 82aca8ca-2078-4a47-860c-6f6ee78cbb02
There was a problem hiding this comment.
Pull request overview
This PR wires the async SqlColumnEncryptionKeyStoreProvider APIs into the Always Encrypted async execution paths so ExecuteReaderAsync / ExecuteNonQueryAsync / ExecuteXmlReaderAsync no longer block threads on key store network I/O (e.g., Azure Key Vault), while preserving sync behavior and error semantics.
Changes:
- Adds async counterparts in the Always Encrypted utility/enclave layers (e.g.,
SqlSecurityUtility.DecryptSymmetricKeyAsync,SqlSymmetricKeyCache.GetKeyAsync,EnclaveDelegate.GenerateEnclavePackageAsync). - Refactors describe-parameter-encryption processing to share parsing between sync/async and to defer key-store operations until after result-set parsing.
- Flows the caller’s
CancellationTokeninto AE key-store operations via per-execution state onSqlCommand, and updates query metadata cache to split probe vs. async completion.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSecurityUtility.cs | Adds async key decryption + CMK signature verification paths with cancellation and consistent wrapping. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSymmetricKeyCache.cs | Adds GetKeyAsync with lock-release around provider I/O to avoid blocking sync callers. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs | Splits cache lookup into in-memory probe + awaitable completion that loads CEKs asynchronously. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs | Introduces per-execution _asyncExecutionCancellationToken for async AE flows. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs | Refactors describe-parameter-encryption processing to defer key-store ops and add async completion. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs | Adds async enclave package generation and replaces continuation chains with async/await + Task.Run dispatch. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.NonQuery.cs | Captures/clears per-execution cancellation token for async non-query execution. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs | Captures/clears per-execution cancellation token for async XML reader execution. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveDelegate.cs | Adds async key resolution for enclave package generation and shares key validation/projection. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveDelegate.Crypto.cs | Adds GenerateEnclavePackageAsync and shared helper for building the enclave package. |
| src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AlwaysEncrypted/SqlSecurityUtilityAsyncShould.cs | Adds unit tests covering async decryption/signature verification/cancellation/cache behavior. |
| src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AlwaysEncrypted/EnclaveDelegateAsyncShould.cs | Adds unit tests asserting enclave async path uses async provider APIs and honors cancellation. |
| src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AlwaysEncrypted/SqlQueryMetadataCacheAsyncShould.cs | Adds unit tests for async metadata cache completion and stale-key degradation. |
| src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/AsyncKeyStoreProviderTests.cs | Adds manual integration test verifying sync vs async provider API usage and token flow. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
GetParameterEncryptionDataReader returned void with an out Task and did nothing but dispatch asynchronous work, so its name gave no hint that the caller was receiving a pending operation. The name was inherited from main, where it was actively misleading: the overload without the Async suffix was the one that continued from a pending network write, while the overload with the suffix was the one used when that write had already completed. Rename the dispatcher to GetParameterEncryptionDataReaderAsync and have it return the task directly instead of through an out parameter, and rename the async body to ConsumeDescribeParameterEncryptionResultsAsync, which describes what it actually does. Both names now carry the Async suffix and both return a task. Also make the Task.Run rationale precise. It is not merely a convenience: the body issues blocking TDS reads before it reaches a suspension point, and PrepareForTransparentEncryption runs synchronously on the caller's thread under the async entry points, so awaiting inline would put those reads on the caller's thread whenever the network write is null or already complete. Neither continuation this replaced could run inline, since one used Task.Run and the other used ContinueWith without ExecuteSynchronously. No behaviour change. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 82aca8ca-2078-4a47-860c-6f6ee78cbb02
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs:160
- _asyncExecutionCancellationToken is documented as representing the token “currently in flight”, but it looks like it’s only cleared in the async cleanup continuations. If an async entry point throws during setup (e.g., Task.Factory.FromAsync throws before the continuation runs), the token can remain set on the SqlCommand instance, unnecessarily retaining the caller’s CancellationTokenSource and violating the stated invariant. Consider clearing this field in the synchronous exception paths of InternalExecuteReaderAsync / InternalExecuteNonQueryAsync / InternalExecuteXmlReaderAsync as well (before returning a faulted Task).
/// <summary>
/// Cancellation token supplied by the caller of the asynchronous execution that is currently in
/// flight, or <see cref="CancellationToken.None"/> when the command is executing synchronously.
/// </summary>
/// <remarks>
Report CreateColumnEncryptionKeyInfo as the throwing member in the internal null-argument diagnostics. The throws moved into that shared helper and are now reachable from both the sync and async decrypt paths, so naming GetDecryptedKeysToBeSentToEnclave was misleading. This also matches the convention used by EncryptBytePackage and ComputeQueryStringHash in the same type. Use TdsEnums.AEAD_AES_256_CBC_HMAC_SHA256 and SqlClientEncryptionType.Deterministic in the query metadata cache tests instead of bare literals with explanatory comments. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 82aca8ca-2078-4a47-860c-6f6ee78cbb02
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.
Suppressed comments (4)
Previously missed (1) — in code that hasn't changed since the last review.
src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AlwaysEncrypted/SqlQueryMetadataCacheAsyncShould.cs:90
SqlCommand(and its underlyingSqlConnection) should be disposed in this test to avoid leaking resources across the unit test process; use ausingdeclaration for the cached command instance.
This issue also appears in the following locations of the same file:
- line 113
- line 137
- line 150
TestKeyStoreProvider provider = new TestKeyStoreProvider { PlaintextKey = NewPlaintextKey(seed: 41) };
SqlCommand command = NewCachedCommand(provider);
src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AlwaysEncrypted/SqlQueryMetadataCacheAsyncShould.cs:115
- This test creates a
SqlCommandwith an ownedSqlConnectionbut never disposes it; prefer ausingdeclaration so the connection/command are cleaned up even if the test fails.
};
SqlCommand command = NewCachedCommand(provider);
src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AlwaysEncrypted/SqlQueryMetadataCacheAsyncShould.cs:152
- Use a
usingdeclaration for theSqlCommandcreated byNewCachedCommandso the command/connection are disposed deterministically.
TestKeyStoreProvider provider = new TestKeyStoreProvider { PlaintextKey = NewPlaintextKey(seed: 43) };
SqlCommand command = NewCachedCommand(provider);
src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AlwaysEncrypted/SqlQueryMetadataCacheAsyncShould.cs:139
- Dispose the
SqlCommandcreated for this test (it owns aSqlConnection) to avoid resource leakage across the unit test run.
TestKeyStoreProvider provider = new TestKeyStoreProvider { PlaintextKey = NewPlaintextKey(seed: 47) };
SqlCommand command = NewCachedCommand(provider);
A pass over the async Always Encrypted work looking for correctness, performance, and consistency problems. The behavioural changes: Capture the cancellation token on the originating thread. The token lives in a field so it does not have to be threaded through layers of callback-driven code that have no use for it, but the Always Encrypted work is handed to the thread pool and can outlive the cleanup continuation that resets that field. Reading it late could silently observe CancellationToken.None. It is now read once at the two synchronous capture points and passed on as an explicit parameter. Make the symmetric key cache insert non-cancellable. Cancelling between a successful decrypt and the cache insert threw away a key that had already cost a key store round trip, so the next caller paid for it again. Drop a dead serverName parameter from both GetDecryptedKeysToBeSentToEnclave overloads, and give the result list a capacity hint. The rest is allocation and clarity work: lazily allocate the pending key operation lists and hand them out as IReadOnlyList, walk them by index so the interface enumerator is not boxed, and make IsEnclaveEnabled an explicit field on ColumnMasterKeySignatureVerification rather than a hard-coded true that only happens to be correct because of where the call sits. Also documents three decisions that look like missed optimisations but are not. The Task.Run in GetParameterEncryptionDataReaderAsync cannot be awaited inline because the task it waits on is completed by the SNI network callback, so an inline continuation would run blocking TDS reads on a callback thread. Column encryption key decryption stays sequential because distinct cipher metadata entries often share a key, and GetKeyAsync does not hold its gate across provider I/O, so parallel misses for the same key would each issue their own key store call. Sequential decryption lets the first result warm the cache. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 82aca8ca-2078-4a47-860c-6f6ee78cbb02
| catch (Exception ex) when (ex is SqlException or ArgumentException) | ||
| { | ||
| // The key information is stale, so fail the cache lookup. | ||
| OnKeyLoadFailed(sqlCommand, clearParameterMetadata: true); | ||
| return false; | ||
| } | ||
| catch (Exception) | ||
| { | ||
| OnKeyLoadFailed(sqlCommand, clearParameterMetadata: false); | ||
| throw; | ||
| } |
There was a problem hiding this comment.
Good catch on the eviction, fixed in a9a072e.
CompleteCachedQueryMetadataAsync now has an OperationCanceledException catch ahead of the general one that rethrows without touching the cache. Cancellation is not evidence that the cached metadata went stale, so evicting made the next caller pay for another sp_describe_parameter_encryption round trip for no reason. This failure mode cannot arise on the synchronous path, so only the async overload handles it and the two stay otherwise identical.
One correction: misses were not being incremented. The general catch calls OnKeyLoadFailed(clearParameterMetadata: false), and IncrementCacheMisses sits behind the early return that flag guards, so only the invalidation was happening.
The reason this slipped through is that the existing cancellation test only asserted that the call threw. It now also asserts the entry survives and that a subsequent uncancelled load succeeds, and I verified it fails against the old code at that assertion.
CompleteCachedQueryMetadataAsync treated every exception from the column encryption key load as evidence that the cached metadata was stale, and invalidated the entry. Cancellation is not evidence of anything of the sort, so cancelling an execution threw away a perfectly good cache entry and made the next caller pay for another describe parameter encryption round trip. Cancellation now propagates without touching the cache. This failure mode cannot arise on the synchronous path, so only the asynchronous overload handles it and the two stay otherwise identical. The existing cancellation test only asserted that the operation threw, which is why this went unnoticed. It now also asserts that the entry survives and that a subsequent uncancelled load still succeeds, and it fails without the fix. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 82aca8ca-2078-4a47-860c-6f6ee78cbb02
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSymmetricKeyCache.cs:155
- The comment says the provider exception will be “simply bubbled up”, but this method wraps non-cancellation failures in SQL.KeyDecryptionFailed. Please update the comment so it matches the actual error behavior (only OperationCanceledException is propagated unwrapped).
// Decrypt the CEK
// We will simply bubble up the exception from the DecryptColumnEncryptionKeyAsync function.
byte[] plaintextKey;
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSecurityUtility.cs:412
- The comment says the provider exception will be “simply bubbled up”, but this helper wraps non-cancellation failures in SQL.KeyDecryptionFailed. Please update the comment so it matches the actual behavior (only OperationCanceledException is propagated unwrapped).
// Decrypt the CEK
// We will simply bubble up the exception from the DecryptColumnEncryptionKeyAsync function.
byte[] plaintextKey;
The required sqlclient-pr pipeline is failing only on tests unrelated to this change, in a pipeline with a ~48% baseline failure rate across other PRs. Observed failures, none of which touch any file in this PR: MARSTest.MarsScenarioClientJoin (also failed on PR #4567) SqlCommandCancelTest.TimeOutDuringRead_Tcp (timing sensitive) TransactionEnlistmentTest.TestManualEnlistment_Enlist (also failed on PR #4585) The /azp run comment trigger is not enabled on this repo, so refreshing the head SHA is the only available way to re-run the required checks. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: cfc64bc6-a9e6-490d-88b1-4b78d25aa103
Description
SqlColumnEncryptionKeyStoreProvidergained async APIs in #4540, but nothing in the driver called them. Every Always Encrypted key store operation still went through the synchronous overloads, so anExecuteReaderAsyncagainst a key store that performs network I/O (Azure Key Vault, for example) blocked a thread for the duration of the round trip. This PR wires the async APIs into the Always Encrypted execution paths so async callers get a genuine end-to-end async experience.Async utility layer
Adds async counterparts to the Always Encrypted utility layer, mirroring their synchronous versions:
SqlSecurityUtility.DecryptSymmetricKeyAsync(both theSqlCipherMetadataandSqlTceCipherInfoEntryoverloads),GetKeyFromLocalProvidersAsync,VerifyColumnMasterKeySignatureAsyncSqlSymmetricKeyCache.GetKeyAsync, which never holds the cache lock across provider I/O. Concurrent decryptions of the same key are allowed to race, and the first writer wins.EnclaveDelegate.GetDecryptedKeysToBeSentToEnclaveAsyncandGenerateEnclavePackageAsyncEach async method shares its parsing, validation, and error-wrapping logic with the existing synchronous version, so provider failures still surface as the same wrapped
SqlExceptionand multi-key fallback still behaves identically. The one deliberate divergence:DecryptSymmetricKeyAsyncrethrowsOperationCanceledExceptionimmediately instead of continuing to try the remaining candidate keys.Call site integration
The describe-parameter-encryption pipeline was restructured so that sync and async share one parser.
ReadDescribeEncryptionParameterResultsCorenow records the signature verifications and key decryptions it discovers into aPendingColumnEncryptionKeyOperationslist rather than performing them inline; thin sync and async wrappers then execute that work. This avoids duplicating roughly 350 lines of TDS token parsing, and has a useful side effect: no key store call now happens while the describe reader is mid-result-set.GenerateEnclavePackageand the post-describe execution continuation gained async counterparts built on shared helpers (TryPrepareEnclavePackageGeneration,BuildEnclavePackage,CreateColumnEncryptionKeyInfo), so both modes perform identical validation and throw identical exceptions.Cancellation
The async provider APIs all accept a
CancellationToken, but the Always Encrypted call sites had no way to reach the token supplied toExecuteReaderAsync/ExecuteNonQueryAsync/ExecuteXmlReaderAsync. The token is now captured in a per-execution field onSqlCommand, set at each async entry point and cleared by the matching cleanup continuation, and passed down into the key operations and enclave package generation.A field is used rather than signature plumbing because the token would otherwise have to thread through several sync-only overloads of
RunExecuteReaderandPrepareForTransparentEncryption. This mirrors how existing per-execution mutable state such as_cachedAsyncStateis handled.Query metadata cache
On a metadata cache hit the driver still loaded every column encryption key synchronously on the caller's thread. The lookup is now split into an in-memory probe (
TryGetCachedQueryMetadata), which matches cached cipher metadata onto the parameters and reports which keys still need decrypting, and a completion step that loads them. The synchronous lookup is expressed in terms of the same probe, so its behaviour, including stale-key fallback and the hit/miss counters, is unchanged.Notes for reviewers
A few things worth a careful look:
usedCacheis over-reported on the async cache-hit path. When cached key information turns out to be stale, the returned task falls back to a full describe round trip, but the command has already reportedusedCache = truebecause the caller returned before the fallback was discovered. This is deliberately conservative: over-reporting a cache hit can only cost one extra retry of an already-failing execution, whereas under-reporting it would suppress theTCE_CONVERSION_ERROR_CLIENT_RETRYretry that a genuine cache hit depends on.Task.Runis retained on purpose inGetParameterEncryptionDataReaderandRunExecuteReaderTdsWithTransparentParameterEncryption. Removing it would be a regression rather than a simplification:PrepareForTransparentEncryptionruns synchronously on the caller's thread underExecuteReaderAsync, and both bodies issue blocking TDS writes before their first await.Task.Runreproduces the thread pool hand-off the previousContinueWithchain provided and keeps continuations off network callback threads.AsyncHelper.CreateContinuationTaskWithStateinvokes its failure callback only when the antecedent actually faulted, not on cancellation, hence the explicitcatch (OperationCanceledException) { throw; }ahead of theResetAsyncState()handler.keysToBeSentToEnclaveandrequiresEnclaveComputationsare now populated before a failing signature check rather than after. This is safe becauseResetEncryptionState()runs at the start of every execution.#if DEBUGfailpoint block was replaced with a[Conditional("DEBUG")]method. The call is elided in Release exactly as before, and the static fields that ManualTests reflect on are untouched.No public API changes. No behaviour change for synchronous callers.
Behaviour notes worth a careful look
Exception ordering on malformed describe results. Key store work is now deferred until
after the
sp_describe_parameter_encryptionresult set has been fully read, so that no keystore HTTP happens while the describe reader is mid-result-set. A side effect is that if a
result set is both malformed and contains a bad key, the parsing error now surfaces first
where the key store error used to. Both cases still throw; only which exception wins has
changed.
Task.RuninGetParameterEncryptionDataReaderAsyncis deliberate. It looks like aremovable thread-pool hop, but the task being waited on is completed by the SNI network write
callback, so an inline continuation would run blocking TDS reads on a callback thread. There
is a comment on the method saying so.
Column encryption key decryption stays sequential. Distinct cipher metadata entries often
share a key, and
GetKeyAsyncdoes not hold its gate across provider I/O, so decrypting inparallel would let concurrent misses for the same key each issue their own key store call.
Sequential decryption lets the first result warm the cache.
Scope
Async enclave attestation is not in this PR. It adds
virtualasync members to the publicSqlColumnEncryptionEnclaveProviderhierarchy, so it needs reference assembly updates and APIreview, and ships separately. The one remaining blocking call on the async path,
GetEnclaveSession, is marked with a@TODOpointing at that work.Issues
Part of the async Always Encrypted work tracked by #3672, following on from #4540.
Testing
Unit tests (
src/Microsoft.Data.SqlClient/tests/UnitTests/):SqlSecurityUtilityAsyncShould(12 tests) coversDecryptSymmetricKeyAsyncmulti-key fallback, cancellation, provider failure wrapping,VerifyColumnMasterKeySignatureAsync, andSqlSymmetricKeyCache.GetKeyAsynccache behaviour.EnclaveDelegateAsyncShould(5 tests) proves the enclave path uses the async provider API, the sync path still uses the sync API, cancellation produces a cancelled task with no provider call, and failures wrap correctly.SqlQueryMetadataCacheAsyncShould(4 tests) covers async API usage on cache completion, stale-key degradation to a miss, cancellation, and a regression guard that the sync lookup still uses the sync provider API.Manual test (
AsyncKeyStoreProviderTests): registers a recording key store provider and asserts that sync execution hits the sync overload, thatExecuteReaderAsync/ExecuteNonQueryAsynchit the async overload and never the sync one, and that cancelling the caller's token cancels the key store operation. This test requires a live SQL Server plus key store, so it is not part of local verification.Verified locally: Debug and Release builds of the driver on net8.0 with 0 warnings, ManualTests and FunctionalTests compile, and 160 targeted unit tests pass. Three failures in
NativeColumnEncryptionKeyBaselineare pre-existing on macOS and unrelated (they fail inInterop.AppleCrypto.X509StoreAddCertificateagainst the system keychain).Not verified locally: end-to-end Always Encrypted behaviour against a real server and Azure Key Vault. That validation is being done separately, which is why this is a draft.
Guidelines
Please review the contribution guidelines before submitting a pull request: