File: ConfigSchemaGenerator.cs
Web Access
Project: src\src\Tools\ConfigurationSchemaGenerator\ConfigurationSchemaGenerator.csproj (ConfigurationSchemaGenerator)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.Extensions.Configuration.Binder.SourceGeneration;
 
namespace ConfigurationSchemaGenerator;
 
public partial class ConfigSchemaGenerator
{
    private const string ConfigurationSchemaAttributeName = "Aspire.ConfigurationSchemaAttribute";
    private const string LoggingCategoriesAttributeName = "Aspire.LoggingCategoriesAttribute";
 
    public static void GenerateSchema(string inputAssembly, string[] references, string outputFile)
    {
        var inputReference = CreateMetadataReference(inputAssembly);
        var compilation = CSharpCompilation.Create(
            "ConfigGenerator",
            references: references.Select(CreateMetadataReference)
                .Concat([inputReference]));
 
        var assemblySymbol = (IAssemblySymbol)compilation.GetAssemblyOrModuleSymbol(inputReference);
        var configSchemaInfo = GetConfigurationSchema(assemblySymbol);
 
        if (configSchemaInfo is not null)
        {
            var parser = new ConfigurationBindingGenerator.Parser(configSchemaInfo, new KnownTypeSymbols(compilation));
            var spec = parser.GetSchemaGenerationSpec(CancellationToken.None);
 
            var emitter = new ConfigSchemaEmitter(spec, compilation);
            var schema = emitter.GenerateSchema();
 
            if (!schema.EndsWith(Environment.NewLine))
            {
                // Ensure the file always ends in a newline to stop certain text editors from injecting it
                schema += Environment.NewLine;
            }
 
            File.WriteAllText(outputFile, schema);
        }
    }
 
    internal static PortableExecutableReference CreateMetadataReference(string path)
    {
        var docPath = Path.ChangeExtension(path, "xml");
        var documentationProvider = XmlDocumentationProvider.CreateFromFile(docPath);
 
        return MetadataReference.CreateFromFile(path, documentation: documentationProvider);
    }
 
    internal static ConfigSchemaAttributeInfo? GetConfigurationSchema(IAssemblySymbol assembly)
    {
        List<INamedTypeSymbol>? types = null;
        List<string>? configurationPaths = null;
        List<string>? exclusionPaths = null;
        List<string>? logCategories = null;
 
        foreach (var attribute in assembly.GetAttributes())
        {
            if (attribute.AttributeClass?.ToDisplayString() == ConfigurationSchemaAttributeName)
            {
                ImmutableArray<TypedConstant> args = attribute.ConstructorArguments;
                if (args.Length != 3)
                {
                    throw new InvalidOperationException("ConfigurationSchemaAttribute should only be used with 3 ctor arguments.");
                }
 
                var path = (string)args[0].Value;
                if (string.IsNullOrEmpty(path))
                {
                    path = $"{ConfigSchemaEmitter.RootPathPrefix}{Guid.NewGuid()}";
                }
 
                (configurationPaths ??= new()).Add(path);
                (types ??= new()).Add((INamedTypeSymbol)args[1].Value);
 
                var exclusionPathsArg = args[2];
                if (!exclusionPathsArg.IsNull)
                {
                    (exclusionPaths ??= new()).AddRange(exclusionPathsArg.Values.Select(v => $"{path}:{(string)v.Value}"));
                }
            }
            else if (attribute.AttributeClass?.ToDisplayString() == LoggingCategoriesAttributeName)
            {
                ImmutableArray<TypedConstant> args = attribute.ConstructorArguments;
                if (args.Length != 1)
                {
                    throw new InvalidOperationException("LoggingCategoriesAttribute should only be used with 1 ctor argument.");
                }
 
                (logCategories ??= new()).AddRange(args[0].Values.Select(v => (string)v.Value));
            }
        }
 
        if (types == null && logCategories == null)
        {
            return null;
        }
 
        return new ConfigSchemaAttributeInfo(types, configurationPaths, exclusionPaths, logCategories);
    }
 
    /// <summary>
    /// Data about configuration schema directly from the ConfigurationSchemaAttribute.
    /// </summary>
    internal sealed record ConfigSchemaAttributeInfo(List<INamedTypeSymbol>? Types, List<string>? ConfigurationPaths, List<string>? ExclusionPaths, List<string>? LogCategories);
}