Skip to content

Small PoC on simplifying command and parse result handling#2261

Draft
alzimmermsft wants to merge 1 commit intomicrosoft:mainfrom
alzimmermsft:ChangeCommandParsingExtensions
Draft

Small PoC on simplifying command and parse result handling#2261
alzimmermsft wants to merge 1 commit intomicrosoft:mainfrom
alzimmermsft:ChangeCommandParsingExtensions

Conversation

@alzimmermsft
Copy link
Copy Markdown
Contributor

What does this PR do?

Proof of concept for simplifying how CommandResult and ParseResult is handled with Option and Option<T>.

This changes the need to use Option.Name to retrieve the value when using the extension methods, instead the extension method handles using the name of the Option.Name to handle everything properly.

Option.Name is used for lookup as we have extension methods for Options which changes their required or optional behavior, but this creates a new instance which changes how lookup is handled by CommandResult and ParseResult as it's just looking up against a dictionary, and since the instance is different this could fail.

GitHub issue number?

[Link to the GitHub issue this PR addresses]

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
    • Created a changelog entry if the change falls among the following: new feature, bug fix, UI/UX update, breaking change, or updated dependencies. Follow the changelog entry guide
  • 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 running the script ./eng/scripts/Process-PackageReadMe.ps1. See Package README
    • 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 renamed tools, follow the Tool Rename Checklist and tag the PR with the breaking-change label
    • 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 command list in servers/Azure.Mcp.Server/docs/azmcp-commands.md
    • Ran ./eng/scripts/Update-AzCommandsMetadata.ps1 to update tool metadata in azmcp-commands.md (required for CI)
    • 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

Copy link
Copy Markdown
Contributor

@jongio jongio left a comment

Choose a reason for hiding this comment

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

Design direction is right - routing through option.Name to find the registered instance cleanly fixes the AsRequired/AsOptional identity mismatch. A few things to address across the surface:

TryGetValue<T>(CommandResult, Option<T>, out T?) (CommandResultExtensions.cs line 58) still calls commandResult.GetResult(option) directly. It hasn't been migrated to name-based routing and has the same instance-mismatch problem as the methods you've fixed.

This also needs at least one test demonstrating the fix works: register an option via AsRequired() on the command, then call GetValueOrDefault() with the original static definition. That's the scenario this PR exists for - show it works.

// Custom validation: At least one update property must be specified
if (string.IsNullOrEmpty(commandResult.GetValueOrDefault<string>(ComputeOptionDefinitions.UpgradePolicy.Name)) &&
!commandResult.GetValueOrDefault<int?>(ComputeOptionDefinitions.Capacity.Name).HasValue &&
if (string.IsNullOrEmpty(commandResult.GetValueOrDefault(ComputeOptionDefinitions.UpgradePolicy)) &&
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Only the first 2 conditions were migrated to the new pattern. Lines 69-73 (VmSize, Overprovision, EnableAutoOsUpgrade, ScaleInPolicy, Tags) still use .Name. Same gap in BindOptions (lines 84-91). Since this PoC demonstrates the new API, migrating the whole method makes the before/after comparison cleaner.

if (string.IsNullOrEmpty(commandResult.GetValueOrDefault<string>(ComputeOptionDefinitions.UpgradePolicy.Name)) &&
!commandResult.GetValueOrDefault<int?>(ComputeOptionDefinitions.Capacity.Name).HasValue &&
if (string.IsNullOrEmpty(commandResult.GetValueOrDefault(ComputeOptionDefinitions.UpgradePolicy)) &&
!commandResult.HasOptionResult(ComputeOptionDefinitions.Capacity) &&
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Capacity is Option<int> but the old code queried <int?> - FindOptionTByName<int?> can't match Option<int>, so this check always evaluated as "capacity not provided" even when it was. Switching to HasOptionResult fixes this pre-existing bug. Worth calling out in the PR description.

Also note the semantic shift: HasOptionResult checks token presence while the old code checked .HasValue on the returned nullable. For Option<int> without a default these are equivalent, but be intentional about which semantic you want.

options.SslCheck = parseResult.CommandResult.GetValueWithoutDefault<bool?>(MonitorOptionDefinitions.WebTest.SslCheck.Name);
options.SslLifetimeCheckInDays = parseResult.CommandResult.GetValueWithoutDefault<int?>(MonitorOptionDefinitions.WebTest.SslLifetimeCheckInDays.Name);
options.TimeoutInSeconds = parseResult.CommandResult.GetValueWithoutDefault<int?>(MonitorOptionDefinitions.WebTest.TimeoutInSeconds.Name);
options.AppInsightsComponentId = parseResult.CommandResult.GetValueWithoutDefault(MonitorOptionDefinitions.WebTest.AppInsightsComponentId);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ResourceName (line 102) and ResourceGroup (line 103) still use the old .Name pattern while everything below was migrated.

return GetValueOrDefaultImpl(commandResult, option);
}

private static T? GetValueOrDefaultImpl<T>(this CommandResult commandResult, Option<T> option)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this on a private method is a no-op since it can't be discovered as an extension externally. Same applies to GetValueWithoutDefaultImpl. Drop the this modifier to make the intent clear.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Untriaged

Development

Successfully merging this pull request may close these issues.

2 participants