Skip to content

Great Cancellation Spinoff: Using Cancellation Tokens with Async Enumerators#1649

Merged
msalaman merged 2 commits intomainfrom
masalama/cancellationTokensForEnumerators
Feb 5, 2026
Merged

Great Cancellation Spinoff: Using Cancellation Tokens with Async Enumerators#1649
msalaman merged 2 commits intomainfrom
masalama/cancellationTokensForEnumerators

Conversation

@msalaman
Copy link
Copy Markdown
Contributor

@msalaman msalaman commented Feb 4, 2026

What does this PR do?

Makes sure a cancellationToken is used whenever an Async Enumerator is traversed using the WithCancellation method. Updated New Command instructions to implement this practice in future generated code

GitHub issue number?

#1583

Pre-merge Checklist

  • Required for All PRs
    • Read contribution guidelines
    • PR title clearly describes the change
    • Commit history is clean with descriptive messages (cleanup guide)
    • Added comprehensive tests for new/modified functionality
    • Updated servers/Azure.Mcp.Server/CHANGELOG.md and/or servers/Fabric.Mcp.Server/CHANGELOG.md for product changes (features, bug fixes, UI/UX, updated dependencies)
  • For MCP tool changes:
    • One tool per PR: This PR adds or modifies only one MCP tool for faster review cycles
    • Updated servers/Azure.Mcp.Server/README.md and/or servers/Fabric.Mcp.Server/README.md documentation
    • Validate README.md changes using script at eng/scripts/Process-PackageReadMe.ps1. See Package README
    • Updated command list in /servers/Azure.Mcp.Server/docs/azmcp-commands.md and/or /docs/fabric-commands.md
    • Run .\eng\scripts\Update-AzCommandsMetadata.ps1 to update tool metadata in azmcp-commands.md (required for CI)
    • For new or modified tool descriptions, ran ToolDescriptionEvaluator and obtained a score of 0.4 or more and a top 3 ranking for all related test prompts
    • For tools with new names, including new tools or renamed tools, update consolidated-tools.json
    • For new tools associated with Azure services or publicly available tools/APIs/products, add URL to documentation in the PR description
  • Extra steps for Azure MCP Server tool changes:
    • Updated test prompts in /servers/Azure.Mcp.Server/docs/e2eTestPrompts.md
    • 👉 For Community (non-Microsoft team member) PRs:
      • Security review: Reviewed code for security vulnerabilities, malicious code, or suspicious activities before running tests (crypto mining, spam, data exfiltration, etc.)
      • Manual tests run: added comment /azp run mcp - pullrequest - live to run Live Test Pipeline

@github-project-automation github-project-automation Bot moved this from Untriaged to In Progress in Azure MCP Server Feb 5, 2026
@msalaman msalaman merged commit d6c9ee1 into main Feb 5, 2026
38 checks passed
@msalaman msalaman deleted the masalama/cancellationTokensForEnumerators branch February 5, 2026 20:12
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Azure MCP Server Feb 5, 2026
**API Pattern Discovery:**
- Study existing services (e.g., Sql, Postgres, Redis) to understand resource access patterns
- Use resource collections correctly
- ✅ Good: `.GetSqlServers().GetAsync(serverName)`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetAsync here should be using the CancellationToken parameter or using .WithCancellation

.GetLatestVirtualMachineScaleSetRollingUpgradeAsync(cancellationToken);

// ✅ Correct: VMSS instances
var vms = vmssResource.Value.GetVirtualMachineScaleSetVms().GetAllAsync();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing use of Cancellationoken. Also, it's missing await on the result.

// ✅ Correct: VMSS instances
var vms = vmssResource.Value.GetVirtualMachineScaleSetVms().GetAllAsync();

// Pattern: Get{ResourceType}() returns collection, then .GetAsync() or .GetAllAsync()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both GetAsync and GetAllAsync have CancellationToken arguments. This should be updated to reflect that. I think GetAsync typically takes a string argument for a resource name, so make sure your update includes that detail.

string subscription,
string? resourceGroup = null,
RetryPolicyOptions? retryPolicy = null,
CancellationToken cancellationToken);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have an optional parameter. Probably a miss from a previous change I made.

- **Pattern**:
```csharp
// Correct - use service
var subscriptionResource = await _subscriptionService.GetSubscription(subscription, null, retryPolicy);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is missing using CancellationToken.

**Issue: `cannot convert from 'System.Threading.CancellationToken' to 'string'`**
- **Cause**: Wrong parameter order in resource manager method calls
- **Solution**: Check method signatures; many Azure SDK methods don't take CancellationToken as second parameter
- **Fix**: Use `.GetAsync(resourceName)` instead of `.GetAsync(resourceName, cancellationToken)`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please verify if this "issue" correct, and if the fix should also say to use .WithCancellation


**Issue: Wrong resource access pattern**
- **Problem**: Using `.GetSqlServerAsync(name, cancellationToken)`
- **Solution**: Use resource collections: `.GetSqlServers().GetAsync(name)`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add .WithCancellation?

_storageService = storageService;
}

public override async Task<CommandResponse> ExecuteAsync(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed this in my earlier changes. ExecuteAsync has a CancellationToken argument and should be in this example. Also update the GetStorageAccountsAsync invocation as necessary to show using the CancellationToken

CancellationToken cancellationToken = default)
{
// ✅ Use base class methods that handle authentication and ARM client creation
var armClient = await CreateArmClientAsync(tenant: null, retryPolicy);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CreateArmClientAsync takes a CancellationToken

_sqlService = sqlService;
}

public override async Task<CommandResponse> ExecuteAsync(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExecuteAsync takes a CancellationToken

var options = BindOptions(parseResult);

// ✅ Service calls are async and don't store request state
var databases = await _sqlService.ListDatabasesAsync(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forward CancellationToken parameter from ExecuteAsync signature.


### Azure SDK Integration
- [ ] All Azure SDK property names verified and correct
- [ ] Resource access patterns use collections (e.g., `.GetSqlServers().GetAsync()`)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure this includes something about CancellationToken

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially refer to the specific section headers with details?

try
{
// Perform a lightweight operation to validate the client
await client.ReadAccountAsync();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow up item: ReadAccountAsync doesn't have a signature for a CancellationToken. We'll need to use a pattern that allows us to bail out if the cancellationToken is cancelled even if the underlying operation can't be aborted.

try
{
// Get all cached client keys
keys = await _cacheService.GetGroupKeysAsync(CacheGroup, CancellationToken.None);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please create a follow up work item to make sure we remove uses of CancellationToken.None, like this line. It'll be hard somewhere like here, but we can look online to see if there are recommendations. It's possible that we just don't await if we definitively know this is happening during application exiting. For example, we might be able to DI IHostApplicationLifetime and look at it the CancellationTokens attached to it.

{
try
{
var client = await _cacheService.GetAsync<CosmosClient>(CacheGroup, key);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar as above. We might want to investigate if we should actually await on this. I'd recommend looking online for patterns about how to handle IAsyncDisposable, deciding if we care or how we write this code (which will likely vary based on the object's DI service lifetime, e.g., singleton, scoped, transient). I don't know the best pattern off the top of my head.

try
{
// Create ArmClient for deployments
ArmClient armClient = await CreateArmClientWithApiVersionAsync("Microsoft.CognitiveServices/accounts/deployments", "2025-06-01", null, retryPolicy);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing CancellationToken.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like CreateArmClientWithApiVersionAsync needs to add a parameter for it.

}
};

var result = await CreateOrUpdateGenericResourceAsync<Models.CognitiveServicesAccountDeploymentData>(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CreateOrUpdateGenericResourceAsync needs to add a CancellationToken parameter, and this invocation needs to pass one along.

var credential = await GetCredential(tenant, cancellationToken);
var loadTestClient = new LoadTestRunClient(new Uri($"https://{dataPlaneUri}"), credential, CreateLoadTestingClientOptions(retryPolicy));

var loadTestRunResponse = await loadTestClient.GetTestRunAsync(testRunId);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lacks a way to be cancelled. Look into some sort of wrapping. Also, with stuff like this that are lacking CancellationToken within Azure SDK libraries, please file issues for the SDK to add them to methods that lack CancellationToken. File separate issues per resource provider for easier tracking and distribution of work by relevant owners.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, for GetTestRunAsync, it has a parameter RequestContext which has a CancellationToken property. Should we use that? If so, are there other Azure SDK methods that lack CancellationToken as explicit method parameters but do honor a RequestContext.CancellationToken? You might want to chat with someone on the .NET Azure SDK to confirm if we should depend on RequestContext.


using var requestContent = RequestContent.Create(JsonSerializer.Serialize(requestBody, LoadTestJsonContext.Default.TestRunRequest));

var loadTestRunResponse = await loadTestClient.BeginTestRunAsync(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing CancellationToken. As with other LoadTestingService.cs comment of mine, should we use the RequestContext parameter with an assigned CancellationToken?

var credential = await GetCredential(tenant, cancellationToken);
var loadTestClient = new LoadTestAdministrationClient(new Uri($"https://{dataPlaneUri}"), credential, CreateLoadTestingClientOptions(retryPolicy));

var loadTestResponse = await loadTestClient.GetTestAsync(testId);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing CancellationToken. As with other LoadTestingService.cs comment of mine, should we use the RequestContext parameter with an assigned CancellationToken?

}
};

var loadTestResponse = await loadTestClient.CreateOrUpdateTestAsync(testId, RequestContent.Create(JsonSerializer.Serialize(testRequestPayload, LoadTestJsonContext.Default.TestRequestPayload)));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing CancellationToken. As with other LoadTestingService.cs comment of mine, should we use the RequestContext parameter with an assigned CancellationToken?

@@ -103,7 +103,7 @@ public override async Task<List<string>> GetAvailableRegionsAsync(string resourc
{
var quotas = subscription.GetModelsAsync(region);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing use of CancellationToken parameter.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though it's done in the await foreach, I think we should always make sure to use CancellationToken parameters where available.

@@ -153,7 +153,7 @@ public override async Task<List<string>> GetAvailableRegionsAsync(string resourc
try
{
AsyncPageable<PostgreSqlFlexibleServerCapabilityProperties> result = subscription.ExecuteLocationBasedCapabilitiesAsync(region);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also has a CancellationToken parameter.

colbytimm pushed a commit to colbytimm/microsoft-mcp that referenced this pull request Apr 20, 2026
…erators (microsoft#1649)

* add cancellation token usage to enumerator scenarios

* update new command instructions to use cancellation token for async enumerable scenario
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants