From 361962d2369765658c4a62178074673b8113e6a8 Mon Sep 17 00:00:00 2001 From: alzimmermsft <48699787+alzimmermsft@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:38:55 -0400 Subject: [PATCH 1/5] Checkpoint work --- .../Subscription/SubscriptionCommand`2.cs | 13 +- .../src/Commands/BaseCommand`2.cs | 23 ++- .../src/Options/OptionBinder.cs | 136 +++------------- .../src/Options/OptionCreators.cs | 151 ++++++++++++++++++ .../src/Options/OptionDescriptor.cs | 13 +- .../Options/OptionBinderTests.cs | 103 +++++++----- 6 files changed, 265 insertions(+), 174 deletions(-) create mode 100644 core/Microsoft.Mcp.Core/src/Options/OptionCreators.cs diff --git a/core/Azure.Mcp.Core/src/Commands/Subscription/SubscriptionCommand`2.cs b/core/Azure.Mcp.Core/src/Commands/Subscription/SubscriptionCommand`2.cs index 80dd1bda63..ae6e31f1ae 100644 --- a/core/Azure.Mcp.Core/src/Commands/Subscription/SubscriptionCommand`2.cs +++ b/core/Azure.Mcp.Core/src/Commands/Subscription/SubscriptionCommand`2.cs @@ -9,15 +9,10 @@ namespace Azure.Mcp.Core.Commands.Subscription; public abstract class SubscriptionCommand< - [DynamicallyAccessedMembers(TrimAnnotations.CommandAnnotations)] TOptions, TResult> + [DynamicallyAccessedMembers(TrimAnnotations.CommandAnnotations)] TOptions, TResult>(ISubscriptionResolver subscriptionResolver) : AuthenticatedCommand where TOptions : class, ISubscriptionOption { - private readonly ISubscriptionResolver _subscriptionResolver; - - protected SubscriptionCommand(ISubscriptionResolver subscriptionResolver) - { - _subscriptionResolver = subscriptionResolver; - } + private readonly ISubscriptionResolver _subscriptionResolver = subscriptionResolver; public override void ValidateOptions(TOptions options, ValidationResult validationResult) { @@ -29,11 +24,9 @@ public override void ValidateOptions(TOptions options, ValidationResult validati } } - public override TOptions BindOptions(ParseResult parseResult) + public override void PostBindOptions(TOptions options) { - var options = base.BindOptions(parseResult); // Always post-process subscription via resolver (env var / CLI profile fallback) options.Subscription = _subscriptionResolver.ResolveSubscription(options.Subscription); - return options; } } diff --git a/core/Microsoft.Mcp.Core/src/Commands/BaseCommand`2.cs b/core/Microsoft.Mcp.Core/src/Commands/BaseCommand`2.cs index 74af73d10a..4419beab50 100644 --- a/core/Microsoft.Mcp.Core/src/Commands/BaseCommand`2.cs +++ b/core/Microsoft.Mcp.Core/src/Commands/BaseCommand`2.cs @@ -23,6 +23,7 @@ public abstract class BaseCommand<[DynamicallyAccessedMembers(TrimAnnotations.Co private const string TroubleshootingUrl = "https://aka.ms/azmcp/troubleshooting"; private readonly Command _command; + private readonly OptionDescriptor[] _descriptors; [UnconditionalSuppressMessage("Trimming", "IL2075:UnrecognizedReflectionPattern", Justification = "CommandMetadataAttribute is only applied to concrete command types that are rooted by DI service registration.")] @@ -45,7 +46,8 @@ protected BaseCommand() Metadata = attr.ToToolMetadata(); _command = new ExtendedCommand(this, Name, Description); - OptionBinder.RegisterOptions(_command); + _descriptors = OptionDescriptor.FromType(); + OptionBinder.RegisterOptions(_command, _descriptors); } public string Id { get; } @@ -56,11 +58,26 @@ protected BaseCommand() public Command GetCommand() => _command; - public virtual TOptions BindOptions(ParseResult parseResult) + public TOptions BindOptions(ParseResult parseResult) { - return OptionBinder.BindOptions(parseResult); + var options = OptionBinder.BindOptions(parseResult, _descriptors); + PostBindOptions(options); + return options; } + /// + /// Called after the options have been bound to the command to perform any additional setup or initialization. + /// + /// The options that have been bound. + public virtual void PostBindOptions(TOptions options) + { + } + + /// + /// Validates the options after they have been bound. + /// + /// The options to validate. + /// The validation result to populate. public virtual void ValidateOptions(TOptions options, ValidationResult validationResult) { } diff --git a/core/Microsoft.Mcp.Core/src/Options/OptionBinder.cs b/core/Microsoft.Mcp.Core/src/Options/OptionBinder.cs index be54ce8973..cc0babf168 100644 --- a/core/Microsoft.Mcp.Core/src/Options/OptionBinder.cs +++ b/core/Microsoft.Mcp.Core/src/Options/OptionBinder.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Collections.Concurrent; -using System.CommandLine.Parsing; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Reflection; @@ -21,111 +19,14 @@ public static class OptionBinder DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor; - /// - /// To prevent native AOT builds from trimming away the filled generic Options methods, we have to maintain a - /// centralized factory pattern. Each entry provides both the option factory (for registration) and value binder - /// (for parsing). If a type is not in this dictionary and is not an enum, it is unsupported and will be rejected - /// at option registration time. - /// - private static readonly ConcurrentDictionary s_typeHandlers = new() - { - // String - [typeof(string)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(string[])] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - - // Boolean - [typeof(bool)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(bool?)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(bool[])] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - - // Int - [typeof(int)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(int?)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(int[])] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - - // Long - [typeof(long)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(long?)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(long[])] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - - // Short - [typeof(short)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(short?)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(short[])] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - - // Byte - [typeof(byte)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(byte?)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(byte[])] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - - // SByte - [typeof(sbyte)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(sbyte?)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(sbyte[])] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - - // UShort - [typeof(ushort)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(ushort?)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(ushort[])] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - - // UInt - [typeof(uint)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(uint?)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(uint[])] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - - // ULong - [typeof(ulong)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(ulong?)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(ulong[])] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - - // Float - [typeof(float)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(float?)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(float[])] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - - // Double - [typeof(double)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(double?)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(double[])] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - - // Decimal - [typeof(decimal)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(decimal?)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(decimal[])] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - - // Char - [typeof(char)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(char?)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(char[])] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - - // DateTime - [typeof(DateTime)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(DateTime?)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(DateTime[])] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - - // DateTimeOffset - [typeof(DateTimeOffset)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(DateTimeOffset?)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(DateTimeOffset[])] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - - // TimeSpan - [typeof(TimeSpan)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(TimeSpan?)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(TimeSpan[])] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - - // Guid - [typeof(Guid)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(Guid?)] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - [typeof(Guid[])] = new(name => new Option(name), (pr, n) => pr.GetValueOrDefault(n)), - }; - /// /// Registers System.CommandLine options on a command based on the public properties of . /// - public static void RegisterOptions<[DynamicallyAccessedMembers(OptionBindingMembers)] TOptions>(Command command) + /// The command to register options on. + /// The option descriptors to register. + public static void RegisterOptions<[DynamicallyAccessedMembers(OptionBindingMembers)] TOptions>(Command command, OptionDescriptor[] descriptors) where TOptions : class { - var descriptors = OptionDescriptor.FromType(); foreach (var descriptor in descriptors) { var option = CreateOption(descriptor); @@ -137,11 +38,12 @@ public static class OptionBinder /// Creates a new instance and populates its properties /// from the parsed command-line values. /// - public static TOptions BindOptions<[DynamicallyAccessedMembers(OptionBindingMembers)] TOptions>(ParseResult parseResult) + /// The parsed command-line values. + /// The option descriptors to bind. + public static TOptions BindOptions<[DynamicallyAccessedMembers(OptionBindingMembers)] TOptions>(ParseResult parseResult, OptionDescriptor[] descriptors) where TOptions : class { var instance = (TOptions)CreateInstance(typeof(TOptions)); - var descriptors = OptionDescriptor.FromType(); List missingOptions = []; List errors = []; Dictionary? parentInstances = null; @@ -218,9 +120,8 @@ public static class OptionBinder private static Option CreateOption(OptionDescriptor descriptor) { - var name = $"--{descriptor.Name}"; - var handler = GetHandler(descriptor.Type); - var option = handler.CreateOption(name); + var handler = GetHandler(descriptor); + var option = handler.CreateOption(descriptor); option.Description = descriptor.Description; option.Required = descriptor.Required; option.Hidden = descriptor.Hidden; @@ -236,14 +137,18 @@ private static Option CreateOption(OptionDescriptor descriptor) return option; } - private static object? GetOptionValue(ParseResult parseResult, Type type, string optionName) + private static object? GetOptionValue(ParseResult parseResult, OptionDescriptor descriptor) { - var handler = GetHandler(type); - return handler.GetValue(parseResult, optionName); + var handler = GetHandler(descriptor); + return handler.GetValue(parseResult); } - private static OptionTypeHandler GetHandler(Type type) + private static OptionTypeHandler GetHandler(OptionDescriptor descriptor) { + if (descriptor.Type == typeof(int)) + { + return new OptionTypeHandler(descriptor, d => OptionCreators.Int(d)); + } if (s_typeHandlers.TryGetValue(type, out var handler)) { return handler; @@ -305,10 +210,11 @@ private static object CreateInstance(Type type) } private sealed class OptionTypeHandler( - Func createOption, - Func getValue) + OptionDescriptor descriptor, + Func createOption) { - public Option CreateOption(string name) => createOption(name); - public object? GetValue(ParseResult parseResult, string optionName) => getValue(parseResult, optionName); + private readonly Lazy