File: Commands\SettingsSchemaBuilder.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.Json.Serialization;
using Aspire.Cli.Configuration;
 
namespace Aspire.Cli.Commands;
 
/// <summary>
/// Builds schema information from AspireJsonConfiguration using reflection.
/// </summary>
internal static class SettingsSchemaBuilder
{
    /// <summary>
    /// Builds a SettingsSchema from AspireJsonConfiguration by inspecting its properties.
    /// </summary>
    /// <param name="excludeLocalOnly">If true, excludes properties marked with <see cref="LocalAspireJsonConfigurationPropertyAttribute"/>.</param>
    public static SettingsSchema BuildSchema(bool excludeLocalOnly)
    {
        var properties = new List<PropertyInfo>();
        var type = typeof(AspireJsonConfiguration);
 
        foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
        {
            // Skip extension data property as it's for capturing additional properties
            if (prop.GetCustomAttribute<JsonExtensionDataAttribute>() != null)
            {
                continue;
            }
 
            // Skip local-only properties when building global settings schema
            if (excludeLocalOnly && prop.GetCustomAttribute<LocalAspireJsonConfigurationPropertyAttribute>() != null)
            {
                continue;
            }
 
            var jsonPropertyName = prop.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name ?? prop.Name;
            
            // Skip $schema as it's a special JSON Schema property, not a user-editable setting
            if (jsonPropertyName == "$schema")
            {
                continue;
            }
            
            var description = prop.GetCustomAttribute<DescriptionAttribute>()?.Description ?? string.Empty;
            var jsonType = GetJsonType(prop.PropertyType);
            
            // For now, no properties are strictly required (all nullable/optional)
            const bool required = false;
 
            properties.Add(new PropertyInfo(jsonPropertyName, jsonType, description, required));
        }
 
        return new SettingsSchema(properties.OrderBy(p => p.Name).ToList());
    }
 
    /// <summary>
    /// Maps C# types to JSON schema types.
    /// </summary>
    private static string GetJsonType(Type type)
    {
        // Handle nullable types
        var underlyingType = Nullable.GetUnderlyingType(type) ?? type;
 
        if (underlyingType == typeof(string))
        {
            return "string";
        }
        
        if (underlyingType == typeof(bool))
        {
            return "boolean";
        }
        
        if (underlyingType == typeof(int) || underlyingType == typeof(long) || 
            underlyingType == typeof(decimal) || underlyingType == typeof(double) || 
            underlyingType == typeof(float))
        {
            return "number";
        }
        
        if (typeof(System.Collections.IDictionary).IsAssignableFrom(underlyingType))
        {
            return "object";
        }
        
        if (typeof(System.Collections.IEnumerable).IsAssignableFrom(underlyingType) && underlyingType != typeof(string))
        {
            return "array";
        }
        
        return "object";
    }
 
    /// <summary>
    /// Builds a SettingsSchema from AspireConfigFile by inspecting its properties,
    /// including nested types like AspireConfigAppHost and AspireConfigSdk.
    /// </summary>
    /// <param name="excludeLocalOnly">If true, excludes properties marked with <see cref="LocalAspireJsonConfigurationPropertyAttribute"/>.</param>
    public static SettingsSchema BuildConfigFileSchema(bool excludeLocalOnly)
    {
        var properties = BuildPropertiesForType(typeof(AspireConfigFile), excludeLocalOnly);
 
        return new SettingsSchema(properties.OrderBy(p => p.Name).ToList());
    }
 
    [UnconditionalSuppressMessage("Trimming", "IL2070", Justification = "Only called with well-known types from this assembly.")]
    [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Only called with well-known types from this assembly.")]
    [UnconditionalSuppressMessage("Trimming", "IL2062", Justification = "Only called with well-known types from this assembly.")]
    [UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "Only called with well-known types from this assembly.")]
    private static List<PropertyInfo> BuildPropertiesForType(
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type,
        bool excludeLocalOnly)
    {
        var properties = new List<PropertyInfo>();
 
        foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
        {
            // Skip extension data property
            if (prop.GetCustomAttribute<JsonExtensionDataAttribute>() is not null)
            {
                continue;
            }
 
            // Skip JsonIgnore properties (like convenience accessors)
            if (prop.GetCustomAttribute<JsonIgnoreAttribute>() is not null)
            {
                continue;
            }
 
            // Skip local-only properties when building global schema
            if (excludeLocalOnly && prop.GetCustomAttribute<LocalAspireJsonConfigurationPropertyAttribute>() is not null)
            {
                continue;
            }
 
            var jsonPropertyName = prop.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name ?? prop.Name;
 
            // Skip $schema
            if (jsonPropertyName == "$schema")
            {
                continue;
            }
 
            var description = prop.GetCustomAttribute<DescriptionAttribute>()?.Description ?? string.Empty;
 
            var propertyType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
            var jsonType = GetJsonType(propertyType);
            const bool required = false;
 
            List<PropertyInfo>? subProperties = null;
            string? additionalPropertiesType = null;
 
            if (jsonType == "object")
            {
                if (!typeof(System.Collections.IDictionary).IsAssignableFrom(propertyType))
                {
                    // It's a typed object — recurse into its properties
                    subProperties = BuildPropertiesForType(propertyType, excludeLocalOnly);
                }
                else
                {
                    // It's a dictionary — determine the value type
                    var dictInterfaces = propertyType.GetInterfaces()
                        .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDictionary<,>))
                        .ToList();
 
                    if (dictInterfaces.Count > 0)
                    {
                        var valueType = dictInterfaces[0].GetGenericArguments()[1];
                        if (valueType == typeof(string))
                        {
                            additionalPropertiesType = "string";
                        }
                        else if (valueType == typeof(bool))
                        {
                            additionalPropertiesType = "boolean";
                        }
                        else
                        {
                            var valueJsonType = GetJsonType(valueType);
                            if (valueJsonType == "object" && !typeof(System.Collections.IDictionary).IsAssignableFrom(valueType))
                            {
                                subProperties = BuildPropertiesForType(valueType, excludeLocalOnly);
                                additionalPropertiesType = "object";
                            }
                            else
                            {
                                additionalPropertiesType = valueJsonType;
                            }
                        }
                    }
                }
            }
 
            properties.Add(new PropertyInfo(jsonPropertyName, jsonType, description, required, subProperties, additionalPropertiesType));
        }
 
        return properties;
    }
}