| 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); }