// 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; using Microsoft.Mcp.Core.Commands; using Microsoft.Mcp.Core.Extensions; namespace Microsoft.Mcp.Core.Options; /// /// Provides static methods for registering System.CommandLine options from TOptions POCOs /// and binding ParseResult values back to TOptions instances. /// public static class OptionBinder { private const DynamicallyAccessedMemberTypes OptionBindingMembers = 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) where TOptions : class { var descriptors = OptionDescriptor.FromType(); foreach (var descriptor in descriptors) { var option = CreateOption(descriptor); command.Options.Add(option); } } /// /// Creates a new instance and populates its properties /// from the parsed command-line values. /// public static TOptions BindOptions<[DynamicallyAccessedMembers(OptionBindingMembers)] TOptions>(ParseResult parseResult) where TOptions : class { var instance = (TOptions)CreateInstance(typeof(TOptions)); var descriptors = OptionDescriptor.FromType(); List missingOptions = []; List errors = []; Dictionary? parentInstances = null; foreach (var descriptor in descriptors) { var optionName = $"--{descriptor.Name}"; object? value; try { value = GetOptionValue(parseResult, descriptor.Type, optionName); } catch (Exception ex) when (ex is InvalidOperationException or FormatException or OverflowException or ArgumentException) { errors.Add($"Invalid value for '{optionName}': {ex.Message}"); continue; } if (value is null) { if (descriptor.Required) { missingOptions.Add(optionName); } continue; } if (descriptor.ParentProperty is not null) { parentInstances ??= []; if (!parentInstances.TryGetValue(descriptor.ParentProperty, out var parent)) { parent = CreateInstance(descriptor.ParentProperty.PropertyType); parentInstances[descriptor.ParentProperty] = parent; } descriptor.TargetProperty.SetValue(parent, value); } else { descriptor.TargetProperty.SetValue(instance, value); } } // Set any nested parent objects that had at least one child value provided if (parentInstances is not null) { foreach (var (parentProp, parentObj) in parentInstances) { parentProp.SetValue(instance, parentObj); } } if (missingOptions.Count > 0 || errors.Count > 0) { var messages = new List(); if (missingOptions.Count > 0) { messages.Add($"Missing Required options: {string.Join(", ", missingOptions)}"); } if (errors.Count > 0) { messages.AddRange(errors); } throw new CommandValidationException( string.Join('\n', messages), HttpStatusCode.BadRequest, missingOptions: missingOptions); } return instance; } private static Option CreateOption(OptionDescriptor descriptor) { var name = $"--{descriptor.Name}"; var handler = GetHandler(descriptor.Type); var option = handler.CreateOption(name); option.Description = descriptor.Description; option.Required = descriptor.Required; option.Hidden = descriptor.Hidden; // For array/collection types, allow multiple values after a single option token // e.g., --modules RedisBloom RedisJSON instead of --modules RedisBloom --modules RedisJSON if (descriptor.Type.IsArray || (descriptor.Type != typeof(string) && descriptor.Type.IsAssignableTo(typeof(System.Collections.IEnumerable)))) { option.Arity = ArgumentArity.OneOrMore; option.AllowMultipleArgumentsPerToken = true; } return option; } private static object? GetOptionValue(ParseResult parseResult, Type type, string optionName) { var handler = GetHandler(type); return handler.GetValue(parseResult, optionName); } private static OptionTypeHandler GetHandler(Type type) { if (s_typeHandlers.TryGetValue(type, out var handler)) { return handler; } // Enums (and Nullable): represented as Option with constrained values Type? underlyingEnum = GetUnderlyingEnumType(type); if (underlyingEnum is not null) { return s_typeHandlers.GetOrAdd(type, _ => new OptionTypeHandler( name => { var option = new Option(name); EnumOptionValidator.Configure(option, underlyingEnum); return option; }, (pr, n) => { var stringValue = pr.GetValueOrDefault(n); if (stringValue is null) { return null; } return Enum.Parse(underlyingEnum, stringValue, ignoreCase: true); })); } throw new InvalidOperationException( $"Unsupported option type '{type}'. Add a handler to s_typeHandlers in OptionBinder, or override RegisterOptions/BindOptions in the command."); } private static Type? GetUnderlyingEnumType(Type type) { if (type.IsEnum) { return type; } Type? nullable = Nullable.GetUnderlyingType(type); if (nullable is not null && nullable.IsEnum) { return nullable; } return null; } [UnconditionalSuppressMessage("Trimming", "IL2067:UnrecognizedReflectionPattern", Justification = "Nested option types are rooted by the application via property references.")] [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "Nested option types use parameterless constructors rooted by the application.")] private static object CreateInstance(Type type) { return Activator.CreateInstance(type) ?? throw new InvalidOperationException($"Failed to create instance of nested options type '{type.Name}'. Ensure it has a public parameterless constructor."); } private sealed class OptionTypeHandler( Func createOption, Func getValue) { public Option CreateOption(string name) => createOption(name); public object? GetValue(ParseResult parseResult, string optionName) => getValue(parseResult, optionName); } }