// 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 System.Text;
using System.Xml.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
namespace Aspire.Hosting.Azure.Provisioning.Generators;
[Generator(LanguageNames.CSharp)]
internal sealed class AspireProvisioningProxyGenerator : IIncrementalGenerator
{
private const string AttributeMetadataName = "Aspire.Hosting.Azure.Provisioning.GenerateAspireProvisioningProxyAttribute";
private const string BicepDictionaryMetadataName = "Azure.Provisioning.BicepDictionary<T>";
private const string BicepListMetadataName = "Azure.Provisioning.BicepList<T>";
private const string BicepValueBaseMetadataName = "Azure.Provisioning.BicepValue";
private const string BicepValueMetadataName = "Azure.Provisioning.BicepValue<T>";
private const string CoreProvisioningAssemblyName = "Azure.Provisioning";
private const string AzureCoreAssemblyName = "Azure.Core";
private const string AzureLocationMetadataName = "Azure.Core.AzureLocation";
private const string ResourceIdentifierMetadataName = "Azure.Core.ResourceIdentifier";
private const string ResourceTypeMetadataName = "Azure.Core.ResourceType";
private const string SystemDataMetadataName = "Azure.Provisioning.SystemData";
private const string BicepValueProxyTypeName = "global::Aspire.Hosting.Azure.Provisioning.BicepValueProxy";
private const string ProvisionableResourceProxyTypeName = "global::Aspire.Hosting.Azure.Provisioning.ProvisionableResourceProxy";
private const string AzureResourceInfrastructureTypeName = "global::Aspire.Hosting.Azure.AzureResourceInfrastructure";
private const string FactoryClassName = "AzureResourceInfrastructureProvisioningExtensions";
private const string DiagnosticCategory = "Aspire.Hosting.Azure.Provisioning";
private const string ExperimentalDiagnosticId = "ASPIREAZUREPROVISIONING001";
private const string ExperimentalUrlFormat = "https://aka.ms/aspire/diagnostics/{0}";
private static readonly DiagnosticDescriptor s_unsupportedProperty = new(
id: "ASPIREAZUREPROVISIONING002",
title: "Unsupported provisioning property",
messageFormat: "Property '{0}' on provisioning type '{1}' cannot be exported because {2}",
category: DiagnosticCategory,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
private static readonly DiagnosticDescriptor s_unsupportedMethod = new(
id: "ASPIREAZUREPROVISIONING003",
title: "Unsupported provisioning method",
messageFormat: "Method '{0}' on provisioning type '{1}' cannot be exported because {2}",
category: DiagnosticCategory,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
private static readonly DiagnosticDescriptor s_duplicateProjectedMethod = new(
id: "ASPIREAZUREPROVISIONING004",
title: "Duplicate projected provisioning method",
messageFormat: "Method '{0}' on provisioning type '{1}' projects to duplicate signature '{2}' and will not be exported",
category: DiagnosticCategory,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
private static readonly DiagnosticDescriptor s_unsupportedRootType = new(
id: "ASPIREAZUREPROVISIONING005",
title: "Unsupported provisioning proxy root",
messageFormat: "Type '{0}' cannot be used as a provisioning proxy root because it is not a non-generic, non-static class or struct",
category: DiagnosticCategory,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
private static readonly DiagnosticDescriptor s_unsupportedMutableStructRoot = new(
id: "ASPIREAZUREPROVISIONING006",
title: "Unsupported mutable provisioning proxy root",
messageFormat: "Type '{0}' cannot be used as a provisioning proxy root because mutable structs are not supported",
category: DiagnosticCategory,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
private static readonly SymbolDisplayFormat s_typeDisplayFormat = SymbolDisplayFormat.FullyQualifiedFormat
.WithMiscellaneousOptions(
SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers |
SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier);
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(static sourceContext =>
sourceContext.AddSource(
"GenerateAspireProvisioningProxyAttribute.g.cs",
SourceText.From(
"""
// <auto-generated/>
namespace Aspire.Hosting.Azure.Provisioning
{
#pragma warning disable ASPIREEXPORT018
[global::Aspire.Hosting.AspireExportProvider]
#pragma warning restore ASPIREEXPORT018
[global::System.AttributeUsage(global::System.AttributeTargets.Assembly, AllowMultiple = true)]
internal sealed class GenerateAspireProvisioningProxyAttribute : global::System.Attribute
{
public GenerateAspireProvisioningProxyAttribute(global::System.Type type)
{
}
public bool IsInfrastructureRoot { get; set; } = true;
public bool IncludeContainingAssemblyTypes { get; set; }
public string[] ExcludedMemberNames { get; set; } = global::System.Array.Empty<string>();
}
}
""",
Encoding.UTF8)));
var rootTypeGroups = context.SyntaxProvider
.ForAttributeWithMetadataName(
AttributeMetadataName,
static (_, _) => true,
static (attributeContext, _) => GetRootTypes(attributeContext));
context.RegisterSourceOutput(
context.CompilationProvider.Combine(rootTypeGroups.Collect()),
static (sourceContext, input) => Generate(
sourceContext,
GetGeneratedNamespace(input.Left.AssemblyName),
input.Right.SelectMany(static roots => roots).ToImmutableArray()));
}
private static ImmutableArray<ProxyRoot> GetRootTypes(GeneratorAttributeSyntaxContext context)
{
var roots = ImmutableArray.CreateBuilder<ProxyRoot>();
foreach (var attribute in context.Attributes)
{
if (attribute.ConstructorArguments.Length == 1 &&
attribute.ConstructorArguments[0] is { Kind: TypedConstantKind.Type, Value: INamedTypeSymbol type })
{
var isInfrastructureRoot = true;
var includeContainingAssemblyTypes = false;
var excludedMembers = ImmutableArray.CreateBuilder<string>();
foreach (var namedArgument in attribute.NamedArguments)
{
if (namedArgument.Key == "IsInfrastructureRoot" &&
namedArgument.Value.Value is bool value)
{
isInfrastructureRoot = value;
}
if (namedArgument.Key == "IncludeContainingAssemblyTypes" &&
namedArgument.Value.Value is bool includeAssemblyTypes)
{
includeContainingAssemblyTypes = includeAssemblyTypes;
}
if (namedArgument.Key == "ExcludedMemberNames" &&
namedArgument.Value.Kind == TypedConstantKind.Array)
{
foreach (var excludedMember in namedArgument.Value.Values)
{
if (excludedMember.Value is string memberName)
{
excludedMembers.Add(memberName);
}
}
}
}
roots.Add(new ProxyRoot(
type,
isInfrastructureRoot,
includeContainingAssemblyTypes,
excludedMembers.ToImmutable()));
}
}
return roots.ToImmutable();
}
private static void Generate(
SourceProductionContext context,
string generatedNamespace,
ImmutableArray<ProxyRoot> roots)
{
if (roots.IsDefaultOrEmpty)
{
return;
}
var normalizedRoots = NormalizeRoots(roots);
var expandedRoots = ExpandContainingAssemblyTypes(normalizedRoots);
var validRoots = ImmutableArray.CreateBuilder<ProxyRoot>();
foreach (var root in expandedRoots)
{
if (CanGenerateProxy(root.Type))
{
if (IsMutableStruct(root.Type))
{
context.ReportDiagnostic(Diagnostic.Create(
s_unsupportedMutableStructRoot,
GetDiagnosticLocation(root.Type),
root.Type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)));
}
else
{
validRoots.Add(root);
}
}
else
{
context.ReportDiagnostic(Diagnostic.Create(
s_unsupportedRootType,
GetDiagnosticLocation(root.Type),
root.Type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)));
}
}
if (validRoots.Count == 0)
{
return;
}
var rootTypes = validRoots.Select(static root => root.Type).ToImmutableArray();
var excludedMemberNames = CreateExcludedMemberNames(validRoots);
var discovery = DiscoverProxyTypes(rootTypes, excludedMemberNames);
if (discovery.Types.Count == 0)
{
return;
}
var proxyNames = CreateProxyNames(discovery.Types, generatedNamespace);
var collectionNames = CreateCollectionNames(discovery.Collections, proxyNames, generatedNamespace);
var source = new StringBuilder(
"""
// <auto-generated/>
#nullable enable
""");
source.Append("namespace ").Append(generatedNamespace).AppendLine();
source.AppendLine("{");
source.AppendLine();
foreach (var type in discovery.Types.OrderBy(static type => type.ToDisplayString(), StringComparer.Ordinal))
{
GenerateProxy(context, source, type, proxyNames, collectionNames, excludedMemberNames);
}
foreach (var collection in discovery.Collections
.Where(collection => collectionNames.ContainsKey(collection.Type))
.OrderBy(collection => collection.Type.ToDisplayString(), StringComparer.Ordinal))
{
GenerateCollectionProxy(source, collection, proxyNames, collectionNames);
}
GenerateFactoryClass(
source,
validRoots.ToImmutable(),
discovery.Types,
proxyNames,
collectionNames);
source.AppendLine("}");
context.AddSource("AspireProvisioningProxies.g.cs", SourceText.From(source.ToString(), Encoding.UTF8));
}
private static ImmutableArray<ProxyRoot> NormalizeRoots(IEnumerable<ProxyRoot> roots)
{
return roots
.GroupBy(static root => root.Type, SymbolEqualityComparer.Default)
.Select(static group => new ProxyRoot(
(INamedTypeSymbol)group.Key,
group.Any(static root => root.IsInfrastructureRoot),
group.Any(static root => root.IncludeContainingAssemblyTypes),
group.SelectMany(static root => root.ExcludedMemberNames)
.Distinct(StringComparer.Ordinal)
.ToImmutableArray()))
.ToImmutableArray();
}
private static ImmutableArray<ProxyRoot> ExpandContainingAssemblyTypes(ImmutableArray<ProxyRoot> roots)
{
var expandedRoots = roots.ToBuilder();
var expandedAssemblies = new HashSet<IAssemblySymbol>(SymbolEqualityComparer.Default);
foreach (var root in roots)
{
if (!root.IncludeContainingAssemblyTypes ||
!expandedAssemblies.Add(root.Type.ContainingAssembly))
{
continue;
}
foreach (var type in EnumeratePublicProxyTypes(root.Type.ContainingAssembly.GlobalNamespace))
{
expandedRoots.Add(new ProxyRoot(
type,
isInfrastructureRoot: false,
includeContainingAssemblyTypes: false,
ImmutableArray<string>.Empty));
}
}
return NormalizeRoots(expandedRoots);
}
private static IEnumerable<INamedTypeSymbol> EnumeratePublicProxyTypes(INamespaceSymbol namespaceSymbol)
{
foreach (var type in namespaceSymbol.GetTypeMembers())
{
foreach (var publicType in EnumeratePublicProxyTypes(type))
{
yield return publicType;
}
}
foreach (var childNamespace in namespaceSymbol.GetNamespaceMembers())
{
foreach (var publicType in EnumeratePublicProxyTypes(childNamespace))
{
yield return publicType;
}
}
}
private static IEnumerable<INamedTypeSymbol> EnumeratePublicProxyTypes(INamedTypeSymbol type)
{
if (type.DeclaredAccessibility != Accessibility.Public)
{
yield break;
}
if (CanGenerateProxy(type) && !IsMutableStruct(type))
{
yield return type;
}
foreach (var nestedType in type.GetTypeMembers())
{
foreach (var publicType in EnumeratePublicProxyTypes(nestedType))
{
yield return publicType;
}
}
}
private static string GetGeneratedNamespace(string? assemblyName)
{
if (string.IsNullOrEmpty(assemblyName))
{
return "Aspire.Hosting.Provisioning.Generated";
}
var namespaceName = new StringBuilder(assemblyName.Length + ".Generated".Length + 8);
var segments = assemblyName.Split('.');
for (var index = 0; index < segments.Length; index++)
{
if (index > 0)
{
namespaceName.Append('.');
}
namespaceName.Append(SanitizeNamespaceSegment(segments[index]));
}
namespaceName.Append(".Generated");
return namespaceName.ToString();
}
private static HashSet<string> CreateExcludedMemberNames(
ImmutableArray<ProxyRoot>.Builder roots)
{
var excludedMemberNames = new HashSet<string>(StringComparer.Ordinal);
foreach (var root in roots)
{
excludedMemberNames.UnionWith(root.ExcludedMemberNames);
}
return excludedMemberNames;
}
private static DiscoveryResult DiscoverProxyTypes(
ImmutableArray<INamedTypeSymbol> roots,
HashSet<string> excludedMemberNames)
{
var types = new List<INamedTypeSymbol>();
var collections = new List<CollectionShape>();
var seenTypes = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
var seenCollections = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
var rootNamespaces = new HashSet<string>(StringComparer.Ordinal);
foreach (var root in roots)
{
rootNamespaces.Add(root.ContainingNamespace.ToDisplayString());
AddType(root);
}
for (var index = 0; index < types.Count; index++)
{
var type = types[index];
foreach (var property in GetExportableProperties(type))
{
if (!IsExcluded(property, excludedMemberNames))
{
DiscoverMappedType(property.Type);
}
}
foreach (var method in GetExportableMethods(type))
{
if (IsExcluded(method, excludedMemberNames))
{
continue;
}
DiscoverMappedType(method.ReturnType);
foreach (var parameter in method.Parameters)
{
DiscoverMappedType(parameter.Type);
}
}
}
return new DiscoveryResult(types, collections);
void DiscoverMappedType(ITypeSymbol type)
{
if (TryGetBicepValueArgument(type, out var valueType))
{
type = valueType;
}
type = type.WithNullableAnnotation(NullableAnnotation.NotAnnotated);
if (TryGetNullableValueType(type, out var nullableValueType))
{
type = nullableValueType;
}
if (TryGetCollection(type, out var collectionKind, out var elementType, out var collectionType))
{
if (seenCollections.Add(collectionType))
{
collections.Add(new CollectionShape(collectionType, elementType, collectionKind));
DiscoverMappedType(elementType);
}
return;
}
if (type is INamedTypeSymbol namedType &&
namedType.TypeKind is TypeKind.Class or TypeKind.Struct &&
!IsProvisionableResourceBase(namedType) &&
namedType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat) != BicepValueBaseMetadataName &&
IsInProxyScope(namedType) &&
!IsSimpleType(namedType))
{
AddType(namedType);
}
}
bool IsInProxyScope(INamedTypeSymbol type)
{
// Core provisioning models and the small Azure.Core value-type graph they depend on are
// duplicated as internal proxies in each opt-in package. Service SDK assemblies remain
// namespace-bounded so one integration cannot pull in another service's entire API surface.
if (IsSharedProxyType(type))
{
return true;
}
var namespaceName = type.ContainingNamespace.ToDisplayString();
foreach (var rootNamespace in rootNamespaces)
{
if (namespaceName == rootNamespace ||
namespaceName.StartsWith(rootNamespace + ".", StringComparison.Ordinal))
{
return true;
}
}
return false;
}
void AddType(INamedTypeSymbol type)
{
type = (INamedTypeSymbol)type.WithNullableAnnotation(NullableAnnotation.NotAnnotated);
if (CanGenerateProxy(type) &&
!IsMutableStruct(type) &&
seenTypes.Add(type))
{
types.Add(type);
}
}
}
private static Dictionary<INamedTypeSymbol, string> CreateProxyNames(
List<INamedTypeSymbol> types,
string generatedNamespace)
{
var names = new Dictionary<INamedTypeSymbol, string>(SymbolEqualityComparer.Default);
var coreTypePrefix = GetPackageIdentifier(generatedNamespace);
var desiredNames = new Dictionary<INamedTypeSymbol, string>(SymbolEqualityComparer.Default);
foreach (var type in types)
{
desiredNames.Add(
type,
(IsSharedProxyType(type) ? coreTypePrefix : string.Empty) +
type.Name +
"Proxy");
}
// Shared Azure.Provisioning types receive the package prefix, which can make their final
// proxy identifier collide with a service SDK type. Resolve collisions only after applying
// that prefix so every declaration and reference uses the same unique name.
var groups = desiredNames
.GroupBy(static pair => pair.Value, StringComparer.Ordinal)
.OrderBy(static group => group.Key, StringComparer.Ordinal)
.ToList();
var usedNames = new HashSet<string>(
groups.Where(static group => group.Count() == 1).Select(static group => group.Key),
StringComparer.Ordinal);
foreach (var group in groups)
{
if (group.Count() == 1)
{
var pair = group.Single();
names.Add(pair.Key, pair.Value);
continue;
}
var nameWithoutSuffix = group.Key.Substring(0, group.Key.Length - "Proxy".Length);
var suffix = 1;
foreach (var pair in group.OrderBy(
static pair => pair.Key.ToDisplayString(),
StringComparer.Ordinal))
{
string generatedName;
do
{
generatedName = nameWithoutSuffix +
"_" +
suffix.ToString(System.Globalization.CultureInfo.InvariantCulture) +
"Proxy";
suffix++;
}
while (!usedNames.Add(generatedName));
names.Add(pair.Key, generatedName);
}
}
return names;
}
private static string GetPackageIdentifier(string generatedNamespace)
{
const string generatedSuffix = ".Generated";
var packageNamespace = generatedNamespace.EndsWith(generatedSuffix, StringComparison.Ordinal)
? generatedNamespace.Substring(0, generatedNamespace.Length - generatedSuffix.Length)
: generatedNamespace;
var separatorIndex = packageNamespace.LastIndexOf('.');
return SanitizeIdentifier(packageNamespace.Substring(separatorIndex + 1));
}
private static Dictionary<INamedTypeSymbol, string> CreateCollectionNames(
List<CollectionShape> collections,
Dictionary<INamedTypeSymbol, string> proxyNames,
string generatedNamespace)
{
var supportedCollections = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
var addedCollection = true;
while (addedCollection)
{
addedCollection = false;
foreach (var collection in collections)
{
if (!supportedCollections.Contains(collection.Type) &&
CanMapCollectionElement(collection.ElementType, proxyNames, supportedCollections))
{
supportedCollections.Add(collection.Type);
addedCollection = true;
}
}
}
var names = new Dictionary<INamedTypeSymbol, string>(SymbolEqualityComparer.Default);
var supportedShapes = collections.Where(collection => supportedCollections.Contains(collection.Type));
var namePrefix = SanitizeIdentifier(generatedNamespace) + "_";
foreach (var group in supportedShapes.GroupBy(
collection => namePrefix + collection.Kind + "Of" + GetFriendlyTypeIdentifier(collection.ElementType) + "Proxy",
StringComparer.Ordinal))
{
if (group.Count() == 1)
{
var collection = group.Single();
names.Add(collection.Type, group.Key);
continue;
}
var index = 0;
foreach (var collection in group.OrderBy(
static collection => collection.Type.ToDisplayString(),
StringComparer.Ordinal))
{
var qualifiedElementName = SanitizeIdentifier(
collection.ElementType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat));
names.Add(
collection.Type,
group.Key.Substring(0, group.Key.Length - "Proxy".Length) +
qualifiedElementName +
"_" +
(++index).ToString(System.Globalization.CultureInfo.InvariantCulture) +
"Proxy");
}
}
return names;
}
private static bool CanMapCollectionElement(
ITypeSymbol elementType,
Dictionary<INamedTypeSymbol, string> proxyNames,
HashSet<INamedTypeSymbol> supportedCollections)
{
if (IsSimpleType(elementType) || IsIPAddress(elementType))
{
return true;
}
if (elementType is not INamedTypeSymbol namedType)
{
return false;
}
return IsProvisionableResourceBase(namedType) ||
proxyNames.ContainsKey(namedType) ||
supportedCollections.Contains(namedType);
}
private static string GetFriendlyTypeIdentifier(ITypeSymbol type)
{
if (type is not INamedTypeSymbol namedType || !namedType.IsGenericType)
{
return SanitizeIdentifier(type.Name);
}
var name = new StringBuilder(namedType.Name);
foreach (var argument in namedType.TypeArguments)
{
name.Append("Of").Append(GetFriendlyTypeIdentifier(argument));
}
return SanitizeIdentifier(name.ToString());
}
private static string SanitizeIdentifier(string value)
{
var identifier = new StringBuilder(value.Length + 1);
foreach (var character in value)
{
identifier.Append(char.IsLetterOrDigit(character) || character == '_' ? character : '_');
}
if (identifier.Length == 0 || char.IsDigit(identifier[0]))
{
identifier.Insert(0, '_');
}
return identifier.ToString();
}
private static string SanitizeNamespaceSegment(string value)
{
var identifier = new StringBuilder(value.Length + 2);
foreach (var character in value)
{
identifier.Append(char.IsLetterOrDigit(character) || character == '_' ? character : '_');
}
if (identifier.Length == 0 || !SyntaxFacts.IsIdentifierStartCharacter(identifier[0]))
{
identifier.Insert(0, '_');
}
while (!SyntaxFacts.IsValidIdentifier(identifier.ToString()) ||
SyntaxFacts.GetKeywordKind(identifier.ToString()) != SyntaxKind.None ||
SyntaxFacts.GetContextualKeywordKind(identifier.ToString()) != SyntaxKind.None)
{
identifier.Insert(0, '_');
}
return identifier.ToString();
}
private static void GenerateProxy(
SourceProductionContext context,
StringBuilder source,
INamedTypeSymbol type,
Dictionary<INamedTypeSymbol, string> proxyNames,
Dictionary<INamedTypeSymbol, string> collectionNames,
HashSet<string> excludedMemberNames)
{
var proxyName = proxyNames[type];
var underlyingTypeName = type.ToDisplayString(s_typeDisplayFormat);
var isProvisionableResource = IsProvisionableResource(type);
var proxyBaseType = GetProxyBaseType(type, proxyNames);
if (!AppendDocumentationComment(source, type, " "))
{
AppendDocumentationSummary(source, " ", $"Represents the {type.Name} Azure Provisioning model.");
}
source.AppendLine(" [global::Aspire.Hosting.AspireExportAttribute]");
source.Append(" internal class ").Append(proxyName);
if (proxyBaseType is not null)
{
source.Append(" : ").Append(proxyNames[proxyBaseType]);
}
else if (isProvisionableResource)
{
source.Append(" : ").Append(ProvisionableResourceProxyTypeName);
}
source.AppendLine();
source.AppendLine(" {");
source.Append(" internal ").Append(proxyName).Append('(').Append(underlyingTypeName).Append(" value)");
if (proxyBaseType is not null || isProvisionableResource)
{
source.Append(" : base(value)");
}
source.AppendLine();
source.AppendLine(" {");
if (proxyBaseType is null && !isProvisionableResource)
{
if (type.IsValueType)
{
source.AppendLine(" Inner = value;");
}
else
{
source.AppendLine(" Inner = value ?? throw new global::System.ArgumentNullException(nameof(value));");
}
}
source.AppendLine(" }");
source.AppendLine();
if (proxyBaseType is not null || isProvisionableResource)
{
source.Append(" internal new ").Append(underlyingTypeName)
.AppendLine(" Inner => (" + underlyingTypeName + ")base.Inner;");
}
else
{
source.Append(" internal ").Append(underlyingTypeName).AppendLine(" Inner { get; }");
}
if (isProvisionableResource)
{
GenerateAddToMethod(source, proxyName, proxyBaseType is not null && IsProvisionableResource(proxyBaseType));
}
foreach (var property in GetExportableProperties(
type,
includeInherited: true,
stopBeforeType: proxyBaseType))
{
if (IsExcluded(property, excludedMemberNames))
{
continue;
}
if (property.IsIndexer)
{
if (proxyNames.ContainsKey(property.ContainingType))
{
context.ReportDiagnostic(Diagnostic.Create(
s_unsupportedProperty,
GetDiagnosticLocation(property),
property.Name,
type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
"indexers are not supported"));
}
continue;
}
if (isProvisionableResource && property.Name == "BicepIdentifier")
{
continue;
}
if (TryMapType(property.Type, proxyNames, collectionNames, out var mappedType))
{
GenerateProperty(source, property, mappedType, proxyName);
}
else
{
if (proxyNames.ContainsKey(property.ContainingType))
{
context.ReportDiagnostic(Diagnostic.Create(
s_unsupportedProperty,
GetDiagnosticLocation(property),
property.Name,
type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
$"type '{property.Type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)}' is not supported"));
}
}
}
var mappedMethods = GetMappedMethods(
context,
type,
proxyNames,
collectionNames,
excludedMemberNames,
GetInheritedProjectedMethodSignatures(
proxyBaseType,
proxyNames,
collectionNames,
excludedMemberNames),
includeInherited: true,
stopBeforeType: proxyBaseType);
foreach (var methodGroup in mappedMethods.GroupBy(static method => method.Method.Name, StringComparer.Ordinal))
{
var overloads = methodGroup.OrderBy(static method => method.Signature, StringComparer.Ordinal).ToList();
for (var index = 0; index < overloads.Count; index++)
{
GenerateMethod(source, overloads[index], proxyName, index, overloads.Count);
}
}
source.AppendLine(" }");
source.AppendLine();
}
private static void GenerateAddToMethod(StringBuilder source, string proxyName, bool hidesBaseMethod)
{
source.AppendLine();
AppendDocumentationSummary(
source,
" ",
"Adds this provisioning resource to the Azure resource infrastructure.");
AppendInfrastructureParameterDocumentation(source, " ");
AppendMethodExportAttribute(source, proxyName + ".addTo", "AddTo");
source.Append(" internal ");
if (hidesBaseMethod)
{
source.Append("new ");
}
source.Append("void AddTo(").Append(AzureResourceInfrastructureTypeName)
.AppendLine(" infrastructure)");
source.AppendLine(" {");
source.AppendLine(" if (infrastructure is null)");
source.AppendLine(" {");
source.AppendLine(" throw new global::System.ArgumentNullException(nameof(infrastructure));");
source.AppendLine(" }");
source.AppendLine();
source.AppendLine(" infrastructure.Add(Inner);");
source.AppendLine(" }");
}
private static void GenerateProperty(
StringBuilder source,
IPropertySymbol property,
MappedType mappedType,
string proxyName)
{
var canRead = property.GetMethod?.DeclaredAccessibility == Accessibility.Public;
var canWrite = property.SetMethod is { DeclaredAccessibility: Accessibility.Public, IsInitOnly: false };
if (!canRead && !canWrite)
{
return;
}
source.AppendLine();
if (!AppendDocumentationComment(source, property, " ", includeValue: true))
{
var accessor = canRead && canWrite ? "Gets or sets" : canRead ? "Gets" : "Sets";
AppendDocumentationSummary(
source,
" ",
$"{accessor} the {property.Name} provisioning property.");
}
AppendExperimentalAttribute(source);
source.AppendLine(" [global::Aspire.Hosting.AspireExportAttribute]");
if (mappedType is { Kind: MappedTypeKind.BicepValue, LiteralTypeName: not null })
{
AppendUnionAttribute(source, mappedType, " ");
source.AppendLine();
}
source.Append(" internal ").Append(mappedType.ExposedTypeName).Append(" @").Append(property.Name).AppendLine();
source.AppendLine(" {");
if (canRead)
{
source.Append(" get => ");
AppendMappedFromUnderlying(source, "Inner.@" + property.Name, mappedType);
source.AppendLine(";");
}
if (canWrite)
{
if (mappedType.Kind == MappedTypeKind.BicepValue)
{
GenerateBicepValueSetter(source, property, mappedType);
}
else
{
source.Append(" set => Inner.@").Append(property.Name).Append(" = ");
AppendMappedToUnderlying(source, "value", mappedType);
source.AppendLine(";");
}
}
source.AppendLine(" }");
if (canWrite && mappedType.Kind == MappedTypeKind.BicepValue)
{
source.AppendLine();
AppendDocumentationSummary(
source,
" ",
$"Clears the {property.Name} provisioning property.");
AppendMethodExportAttribute(source, proxyName + ".clear" + property.Name, "Clear" + property.Name);
source.Append(" internal void Clear").Append(property.Name).AppendLine("()");
source.AppendLine(" {");
source.Append(" Inner.@").Append(property.Name).AppendLine(".ClearValue();");
source.AppendLine(" }");
}
}
private static void GenerateBicepValueSetter(
StringBuilder source,
IPropertySymbol property,
MappedType mappedType)
{
source.AppendLine(" set");
source.AppendLine(" {");
if (mappedType.LiteralTypeName is not null)
{
source.Append(" if (value is ").Append(mappedType.LiteralTypeName).AppendLine(" literal)");
source.AppendLine(" {");
source.Append(" Inner.@").Append(property.Name).AppendLine(" = literal;");
source.AppendLine(" }");
source.Append(" else if (value is ").Append(BicepValueProxyTypeName).AppendLine(" bicepValue)");
}
else
{
source.AppendLine(" if (value is not null)");
}
source.AppendLine(" {");
if (mappedType.LiteralTypeName is not null)
{
source.Append(" bicepValue.AssignTo(Inner.@").Append(property.Name).AppendLine(");");
}
else
{
source.Append(" value.AssignTo(Inner.@").Append(property.Name).AppendLine(");");
}
source.AppendLine(" }");
source.AppendLine(" else");
source.AppendLine(" {");
source.Append(" throw new global::System.ArgumentException(\"Expected ");
if (mappedType.LiteralTypeName is not null)
{
source.Append(mappedType.LiteralTypeName).Append(" or ");
}
source.Append(BicepValueProxyTypeName).AppendLine(".\", nameof(value));");
source.AppendLine(" }");
source.AppendLine(" }");
}
private static List<MappedMethod> GetMappedMethods(
SourceProductionContext context,
INamedTypeSymbol type,
Dictionary<INamedTypeSymbol, string> proxyNames,
Dictionary<INamedTypeSymbol, string> collectionNames,
HashSet<string> excludedMemberNames,
HashSet<string> projectedSignatures,
bool includeInherited,
INamedTypeSymbol? stopBeforeType)
{
var mappedMethods = new List<MappedMethod>();
foreach (var method in GetExportableMethods(type, includeInherited, stopBeforeType).OrderBy(GetMethodSignature, StringComparer.Ordinal))
{
if (IsExcluded(method, excludedMemberNames))
{
continue;
}
if (!TryMapMethod(method, proxyNames, collectionNames, out var mappedMethod, out var failureReason))
{
if (proxyNames.ContainsKey(method.ContainingType))
{
context.ReportDiagnostic(Diagnostic.Create(
s_unsupportedMethod,
GetDiagnosticLocation(method),
method.Name,
type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
failureReason));
}
continue;
}
var projectedSignature = GetProjectedMethodSignature(mappedMethod);
if (projectedSignatures.Add(projectedSignature))
{
mappedMethods.Add(mappedMethod);
}
else
{
if (proxyNames.ContainsKey(method.ContainingType))
{
context.ReportDiagnostic(Diagnostic.Create(
s_duplicateProjectedMethod,
GetDiagnosticLocation(method),
method.Name,
type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
projectedSignature));
}
}
}
return mappedMethods;
}
private static HashSet<string> GetInheritedProjectedMethodSignatures(
INamedTypeSymbol? proxyBaseType,
Dictionary<INamedTypeSymbol, string> proxyNames,
Dictionary<INamedTypeSymbol, string> collectionNames,
HashSet<string> excludedMemberNames)
{
var projectedSignatures = new HashSet<string>(StringComparer.Ordinal);
for (var current = proxyBaseType; current is not null; current = GetProxyBaseType(current, proxyNames))
{
foreach (var method in GetExportableMethods(current, includeInherited: false))
{
if (!IsExcluded(method, excludedMemberNames) &&
TryMapMethod(method, proxyNames, collectionNames, out var mappedMethod, out _))
{
projectedSignatures.Add(GetProjectedMethodSignature(mappedMethod));
}
}
}
return projectedSignatures;
}
private static string GetProjectedMethodSignature(MappedMethod method)
{
return method.Method.Name + "(" + string.Join(
",",
method.Parameters.Select(static parameter => parameter.Type.ExposedTypeName)) + ")";
}
private static bool TryMapMethod(
IMethodSymbol method,
Dictionary<INamedTypeSymbol, string> proxyNames,
Dictionary<INamedTypeSymbol, string> collectionNames,
out MappedMethod mappedMethod,
out string failureReason)
{
if (method.IsGenericMethod)
{
mappedMethod = null!;
failureReason = "generic methods are not supported";
return false;
}
if (method.ReturnsByRef || method.ReturnsByRefReadonly)
{
mappedMethod = null!;
failureReason = "by-reference return values are not supported";
return false;
}
if (!TryMapType(method.ReturnType, proxyNames, collectionNames, out var returnType))
{
mappedMethod = null!;
failureReason = $"return type '{method.ReturnType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)}' is not supported";
return false;
}
var mappedParameters = new List<MappedParameter>();
foreach (var parameter in method.Parameters)
{
if (parameter.RefKind != RefKind.None)
{
mappedMethod = null!;
failureReason = $"parameter '{parameter.Name}' uses unsupported ref kind '{parameter.RefKind}'";
return false;
}
if (!TryMapType(parameter.Type, proxyNames, collectionNames, out var parameterType))
{
mappedMethod = null!;
failureReason = $"parameter '{parameter.Name}' uses unsupported type '{parameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)}'";
return false;
}
mappedParameters.Add(new MappedParameter(parameter, parameterType));
}
mappedMethod = new MappedMethod(
method,
returnType,
AllocateGeneratedParameterNames(mappedParameters),
GetMethodSignature(method));
failureReason = string.Empty;
return true;
}
private static void GenerateMethod(
StringBuilder source,
MappedMethod mappedMethod,
string proxyName,
int overloadIndex,
int overloadCount)
{
var method = mappedMethod.Method;
var methodName = ToCamelCase(method.Name);
var capabilityId = proxyName + "." + methodName + ".method";
if (overloadCount > 1)
{
capabilityId += ".overload" +
(overloadIndex + 1).ToString(System.Globalization.CultureInfo.InvariantCulture) +
"_" +
GetProjectedSignatureSuffix(mappedMethod.Parameters);
}
source.AppendLine();
if (!AppendDocumentationComment(
source,
method,
" ",
parameters: [.. mappedMethod.Parameters.Select(static parameter =>
new DocumentationParameter(parameter.Parameter.Name, parameter.GeneratedName))],
includeReturns: !method.ReturnsVoid))
{
AppendDocumentationSummary(
source,
" ",
$"Invokes {method.Name} on the {method.ContainingType.Name} provisioning model.");
AppendGeneratedParameterDocumentation(source, mappedMethod.Parameters);
if (!method.ReturnsVoid)
{
AppendDocumentationElement(
source,
" ",
new XElement("returns", $"The result of {method.Name}."),
parameterNames: null);
}
}
AppendMethodExportAttribute(source, capabilityId, method.Name);
source.Append(" internal ").Append(mappedMethod.ReturnType.ExposedTypeName).Append(" @").Append(method.Name).Append('(');
for (var index = 0; index < mappedMethod.Parameters.Count; index++)
{
if (index > 0)
{
source.Append(", ");
}
AppendMappedParameter(source, mappedMethod.Parameters[index], includeDefaultValue: false);
}
source.AppendLine(")");
source.AppendLine(" {");
var invocation = new StringBuilder();
invocation.Append("Inner.@").Append(method.Name).Append('(');
for (var index = 0; index < mappedMethod.Parameters.Count; index++)
{
if (index > 0)
{
invocation.Append(", ");
}
var mappedParameter = mappedMethod.Parameters[index];
AppendMappedToUnderlying(invocation, "@" + mappedParameter.GeneratedName, mappedParameter.Type);
}
invocation.Append(')');
if (method.ReturnsVoid)
{
source.Append(" ").Append(invocation).AppendLine(";");
}
else
{
if (RequiresCapturedNullableReturnValue(mappedMethod.ReturnType))
{
AppendCapturedNullableReturnValue(source, invocation.ToString(), mappedMethod);
}
else
{
source.Append(" return ");
AppendMappedFromUnderlying(source, invocation.ToString(), mappedMethod.ReturnType);
source.AppendLine(";");
}
}
source.AppendLine(" }");
}
private static string GetProjectedSignatureSuffix(List<MappedParameter> parameters)
{
return parameters.Count == 0
? "none"
: SanitizeIdentifier(string.Join("_", parameters.Select(static parameter => parameter.Type.ExposedTypeName)));
}
private static bool RequiresCapturedNullableReturnValue(MappedType mappedType)
=> mappedType.IsNullable &&
mappedType.Kind is MappedTypeKind.Proxy or MappedTypeKind.Collection or MappedTypeKind.ProvisionableResource;
private static void AppendCapturedNullableReturnValue(
StringBuilder source,
string expression,
MappedMethod mappedMethod)
{
var usedNames = new HashSet<string>(
mappedMethod.Parameters.Select(static parameter => parameter.GeneratedName),
StringComparer.Ordinal);
var underlyingValueName = GetUniqueGeneratedLocalName(usedNames, "__underlyingValue");
var mappedValueName = GetUniqueGeneratedLocalName(usedNames, "__mappedValue");
source.Append(" var ").Append(underlyingValueName).Append(" = ").Append(expression).AppendLine(";");
source.Append(" return ").Append(underlyingValueName).Append(" is { } ").Append(mappedValueName)
.Append(" ? new ").Append(mappedMethod.ReturnType.ExposedTypeName.TrimEnd('?'))
.Append('(').Append(mappedValueName).AppendLine(") : null;");
}
private static string GetUniqueGeneratedLocalName(HashSet<string> usedNames, string candidate)
{
while (!usedNames.Add(candidate))
{
candidate += "_";
}
return candidate;
}
private static void GenerateCollectionProxy(
StringBuilder source,
CollectionShape collection,
Dictionary<INamedTypeSymbol, string> proxyNames,
Dictionary<INamedTypeSymbol, string> collectionNames)
{
var proxyName = collectionNames[collection.Type];
var underlyingTypeName = collection.Type.ToDisplayString(s_typeDisplayFormat);
var elementTypeName = collection.ElementType
.WithNullableAnnotation(NullableAnnotation.NotAnnotated)
.ToDisplayString(s_typeDisplayFormat);
var simpleElement = IsSimpleType(collection.ElementType) || IsIPAddress(collection.ElementType);
TryMapType(collection.ElementType, proxyNames, collectionNames, out var mappedElementType);
source.AppendLine(" [global::Aspire.Hosting.AspireExportAttribute]");
source.Append(" internal sealed class ").Append(proxyName).AppendLine();
source.AppendLine(" {");
source.Append(" internal ").Append(proxyName).Append('(').Append(underlyingTypeName).AppendLine(" value)");
source.AppendLine(" {");
source.AppendLine(" Inner = value ?? throw new global::System.ArgumentNullException(nameof(value));");
source.AppendLine(" }");
source.AppendLine();
source.Append(" internal ").Append(underlyingTypeName).AppendLine(" Inner { get; }");
source.AppendLine();
AppendExperimentalAttribute(source);
source.AppendLine(" [global::Aspire.Hosting.AspireExportAttribute]");
source.AppendLine(" internal int Count => Inner.Count;");
if (collection.Kind == CollectionKind.Dictionary)
{
source.AppendLine();
AppendExperimentalAttribute(source);
source.AppendLine(" [global::Aspire.Hosting.AspireExportAttribute]");
source.AppendLine(" internal string[] Keys => new global::System.Collections.Generic.List<string>(Inner.Keys).ToArray();");
}
GenerateCollectionGetMethod(source, collection, proxyName, simpleElement, mappedElementType);
GenerateCollectionSetMethod(source, collection, proxyName, elementTypeName, simpleElement, mappedElementType);
if (collection.Kind == CollectionKind.List)
{
GenerateListMutationMethods(source, proxyName, elementTypeName, simpleElement, mappedElementType);
}
else
{
GenerateDictionaryMutationMethods(source, proxyName);
}
source.AppendLine(" }");
source.AppendLine();
}
private static void GenerateCollectionGetMethod(
StringBuilder source,
CollectionShape collection,
string proxyName,
bool simpleElement,
MappedType mappedElementType)
{
source.AppendLine();
if (!simpleElement)
{
source.AppendLine(" // Expression-backed complex elements cannot be materialized as model proxies because");
source.AppendLine(" // Azure.Provisioning does not expose a literal model instance for those values.");
}
AppendMethodExportAttribute(source, proxyName + ".get.method", "Get");
source.Append(" internal ")
.Append(simpleElement ? BicepValueProxyTypeName : mappedElementType.ExposedTypeName)
.Append(" Get(")
.Append(collection.Kind == CollectionKind.List ? "int index" : "string key")
.AppendLine(")");
source.AppendLine(" {");
var indexExpression = collection.Kind == CollectionKind.List ? "index" : "key";
if (simpleElement)
{
source.Append(" return ").Append(BicepValueProxyTypeName)
.Append(".Create(Inner[").Append(indexExpression).AppendLine("]);");
}
else
{
source.Append(" var element = Inner[").Append(indexExpression).AppendLine("];");
source.AppendLine(" if (((global::Azure.Provisioning.IBicepValue)element).Kind != global::Azure.Provisioning.BicepValueKind.Literal)");
source.AppendLine(" {");
source.AppendLine(" throw new global::System.InvalidOperationException(\"Expression-backed complex collection elements cannot be returned as model proxies.\");");
source.AppendLine(" }");
source.AppendLine();
source.AppendLine(" var literal = element.Value;");
if (!collection.ElementType.IsValueType)
{
source.AppendLine(" if (literal is null)");
source.AppendLine(" {");
source.AppendLine(" throw new global::System.InvalidOperationException(\"Null complex collection elements cannot be returned as model proxies.\");");
source.AppendLine(" }");
source.AppendLine();
}
source.Append(" return ");
AppendMappedFromUnderlying(source, "literal", mappedElementType);
source.AppendLine(";");
}
source.AppendLine(" }");
}
private static void GenerateCollectionSetMethod(
StringBuilder source,
CollectionShape collection,
string proxyName,
string elementTypeName,
bool simpleElement,
MappedType mappedElementType)
{
source.AppendLine();
AppendMethodExportAttribute(source, proxyName + ".set.method", "Set");
source.Append(" internal void Set(")
.Append(collection.Kind == CollectionKind.List ? "int index, " : "string key, ");
AppendCollectionValueParameter(source, elementTypeName, simpleElement, mappedElementType);
source.AppendLine(")");
source.AppendLine(" {");
source.Append(" Inner[")
.Append(collection.Kind == CollectionKind.List ? "index" : "key")
.Append("] = ");
AppendCollectionValueToUnderlying(source, elementTypeName, simpleElement, mappedElementType);
source.AppendLine(";");
source.AppendLine(" }");
}
private static void GenerateListMutationMethods(
StringBuilder source,
string proxyName,
string elementTypeName,
bool simpleElement,
MappedType mappedElementType)
{
source.AppendLine();
AppendMethodExportAttribute(source, proxyName + ".add.method", "Add");
source.Append(" internal void Add(");
AppendCollectionValueParameter(source, elementTypeName, simpleElement, mappedElementType);
source.AppendLine(")");
source.AppendLine(" {");
source.Append(" Inner.Add(");
AppendCollectionValueToUnderlying(source, elementTypeName, simpleElement, mappedElementType);
source.AppendLine(");");
source.AppendLine(" }");
source.AppendLine();
AppendMethodExportAttribute(source, proxyName + ".insert.method", "Insert");
source.Append(" internal void Insert(int index, ");
AppendCollectionValueParameter(source, elementTypeName, simpleElement, mappedElementType);
source.AppendLine(")");
source.AppendLine(" {");
source.Append(" Inner.Insert(index, ");
AppendCollectionValueToUnderlying(source, elementTypeName, simpleElement, mappedElementType);
source.AppendLine(");");
source.AppendLine(" }");
source.AppendLine();
AppendMethodExportAttribute(source, proxyName + ".removeAt.method", "RemoveAt");
source.AppendLine(" internal void RemoveAt(int index)");
source.AppendLine(" {");
source.AppendLine(" Inner.RemoveAt(index);");
source.AppendLine(" }");
source.AppendLine();
AppendMethodExportAttribute(source, proxyName + ".clear.method", "Clear");
source.AppendLine(" internal void Clear()");
source.AppendLine(" {");
source.AppendLine(" Inner.Clear();");
source.AppendLine(" }");
}
private static void GenerateDictionaryMutationMethods(StringBuilder source, string proxyName)
{
source.AppendLine();
AppendMethodExportAttribute(source, proxyName + ".remove.method", "Remove");
source.AppendLine(" internal bool Remove(string key)");
source.AppendLine(" {");
source.AppendLine(" return Inner.Remove(key);");
source.AppendLine(" }");
source.AppendLine();
AppendMethodExportAttribute(source, proxyName + ".clear.method", "Clear");
source.AppendLine(" internal void Clear()");
source.AppendLine(" {");
source.AppendLine(" Inner.Clear();");
source.AppendLine(" }");
}
private static void AppendCollectionValueParameter(
StringBuilder source,
string elementTypeName,
bool simpleElement,
MappedType mappedElementType)
{
if (simpleElement)
{
// ATS carries IP address literals as strings; Convert<IPAddress> parses them for the SDK.
source.Append("[global::Aspire.Hosting.AspireUnionAttribute(typeof(")
.Append(BicepValueProxyTypeName)
.Append("), typeof(")
.Append(elementTypeName == "global::System.Net.IPAddress" ? "string" : elementTypeName)
.Append("))] object value");
}
else
{
source.Append(mappedElementType.ExposedTypeName).Append(" value");
}
}
private static void AppendCollectionValueToUnderlying(
StringBuilder source,
string elementTypeName,
bool simpleElement,
MappedType mappedElementType)
{
if (simpleElement)
{
source.Append(BicepValueProxyTypeName).Append(".Convert<").Append(elementTypeName).Append(">(value)");
}
else
{
AppendMappedToUnderlying(source, "value", mappedElementType);
}
}
private static void GenerateFactoryClass(
StringBuilder source,
ImmutableArray<ProxyRoot> roots,
List<INamedTypeSymbol> types,
Dictionary<INamedTypeSymbol, string> proxyNames,
Dictionary<INamedTypeSymbol, string> collectionNames)
{
var concreteTypes = types
.Where(static type => !type.IsAbstract)
.OrderBy(static type => type.ToDisplayString(), StringComparer.Ordinal)
.ToList();
var factoryTypeNames = CreateFactoryTypeNames(types, proxyNames);
// Lookup, creation, and static-property exports all share this extension class. Reserve
// signatures across every phase so independently derived method names cannot collide.
var factoryMethodSignatures = new HashSet<string>(StringComparer.Ordinal);
source.AppendLine(" [global::Aspire.Hosting.AspireExportAttribute]");
source.Append(" internal static class ").Append(FactoryClassName).AppendLine();
source.AppendLine(" {");
foreach (var root in roots
.Where(root => root.IsInfrastructureRoot &&
IsProvisionableResource(root.Type) &&
proxyNames.ContainsKey(root.Type))
.OrderBy(static root => root.Type.ToDisplayString(), StringComparer.Ordinal))
{
GenerateRootLookupMethod(
source,
root.Type,
proxyNames[root.Type],
factoryTypeNames[root.Type],
factoryMethodSignatures);
}
foreach (var type in concreteTypes)
{
var proxyName = proxyNames[type];
var factoryTypeName = factoryTypeNames[type];
if (IsProvisionableResource(type))
{
GenerateResourceLookupMethods(
source,
type,
proxyName,
factoryTypeName,
factoryMethodSignatures);
if (SelectConstructor(type, isProvisionableResource: true, proxyNames, collectionNames) is { } constructor)
{
GenerateCreationMethod(
source,
type,
proxyName,
factoryTypeName,
constructor,
isProvisionableResource: true,
factoryMethodSignatures);
}
}
else if (SelectConstructor(type, isProvisionableResource: false, proxyNames, collectionNames) is { } constructor)
{
GenerateCreationMethod(
source,
type,
proxyName,
factoryTypeName,
constructor,
isProvisionableResource: false,
factoryMethodSignatures);
}
GenerateStaticPropertyFactoryMethods(
source,
type,
proxyNames,
collectionNames,
proxyName,
factoryTypeName,
factoryMethodSignatures);
}
source.AppendLine(" }");
source.AppendLine();
}
private static Dictionary<INamedTypeSymbol, string> CreateFactoryTypeNames(
List<INamedTypeSymbol> types,
Dictionary<INamedTypeSymbol, string> proxyNames)
{
var names = new Dictionary<INamedTypeSymbol, string>(SymbolEqualityComparer.Default);
foreach (var group in types.GroupBy(static type => type.Name, StringComparer.Ordinal))
{
var groupCount = group.Count();
foreach (var type in group)
{
var proxyBasedName = proxyNames[type].Substring(0, proxyNames[type].Length - "Proxy".Length);
names.Add(
type,
groupCount == 1 && !IsSharedProxyType(type)
? type.Name
: proxyBasedName);
}
}
return names;
}
private static void GenerateRootLookupMethod(
StringBuilder source,
INamedTypeSymbol type,
string proxyName,
string factoryTypeName,
HashSet<string> factoryMethodSignatures)
{
var methodName = AllocateFactoryMethodName(
factoryMethodSignatures,
"Get" + factoryTypeName,
parameterSignature: string.Empty);
var underlyingTypeName = type.ToDisplayString(s_typeDisplayFormat);
source.AppendLine();
if (!AppendDocumentationComment(source, type, " "))
{
AppendDocumentationSummary(
source,
" ",
$"Gets the {type.Name} provisioning resource for the current Aspire resource.");
}
AppendInfrastructureParameterDocumentation(source, " ");
AppendFactoryExportAttribute(source, proxyName, methodName, "root");
source.Append(" internal static ").Append(proxyName).Append(' ').Append(methodName)
.Append("(this ").Append(AzureResourceInfrastructureTypeName).AppendLine(" infrastructure)");
source.AppendLine(" {");
AppendInfrastructureNullCheck(source);
source.AppendLine(" var bicepIdentifier = global::Aspire.Hosting.AzureResourceExtensions.GetBicepIdentifier(infrastructure.AspireResource);");
source.Append(" var value = global::System.Linq.Enumerable.Single(")
.Append("global::System.Linq.Enumerable.OfType<").Append(underlyingTypeName)
.Append(">(infrastructure.GetProvisionableResources()), ")
.AppendLine("value => value.BicepIdentifier == bicepIdentifier);");
source.Append(" return new ").Append(proxyName).AppendLine("(value);");
source.AppendLine(" }");
}
private static void GenerateResourceLookupMethods(
StringBuilder source,
INamedTypeSymbol type,
string proxyName,
string factoryTypeName,
HashSet<string> factoryMethodSignatures)
{
var underlyingTypeName = type.ToDisplayString(s_typeDisplayFormat);
var getMethodName = AllocateFactoryMethodName(
factoryMethodSignatures,
"Get" + factoryTypeName + "ByIdentifier",
parameterSignature: "string");
source.AppendLine();
if (!AppendDocumentationComment(source, type, " "))
{
AppendDocumentationSummary(
source,
" ",
$"Gets a {type.Name} provisioning resource by its Bicep identifier.");
}
AppendInfrastructureParameterDocumentation(source, " ");
source.Append(" ")
.AppendLine("/// <param name=\"bicepIdentifier\">The Bicep identifier of the provisioning resource to return.</param>");
AppendFactoryExportAttribute(source, proxyName, getMethodName, "byIdentifier");
source.Append(" internal static ").Append(proxyName).Append(' ').Append(getMethodName)
.Append("(this ").Append(AzureResourceInfrastructureTypeName)
.AppendLine(" infrastructure, string bicepIdentifier)");
source.AppendLine(" {");
AppendInfrastructureNullCheck(source);
source.Append(" var value = global::System.Linq.Enumerable.Single(")
.Append("global::System.Linq.Enumerable.OfType<").Append(underlyingTypeName)
.Append(">(infrastructure.GetProvisionableResources()), ")
.AppendLine("value => value.BicepIdentifier == bicepIdentifier);");
source.Append(" return new ").Append(proxyName).AppendLine("(value);");
source.AppendLine(" }");
var getAllMethodName = AllocateFactoryMethodName(
factoryMethodSignatures,
"Get" + PluralizeIdentifier(factoryTypeName),
parameterSignature: string.Empty);
source.AppendLine();
if (!AppendDocumentationComment(source, type, " "))
{
AppendDocumentationSummary(
source,
" ",
$"Gets all {type.Name} provisioning resources.");
}
AppendInfrastructureParameterDocumentation(source, " ");
AppendFactoryExportAttribute(source, proxyName, getAllMethodName, "all");
source.Append(" internal static ").Append(proxyName).Append("[] ").Append(getAllMethodName)
.Append("(this ").Append(AzureResourceInfrastructureTypeName).AppendLine(" infrastructure)");
source.AppendLine(" {");
AppendInfrastructureNullCheck(source);
source.Append(" var values = new global::System.Collections.Generic.List<").Append(proxyName).AppendLine(">();");
source.AppendLine(" foreach (var resource in infrastructure.GetProvisionableResources())");
source.AppendLine(" {");
source.Append(" if (resource is ").Append(underlyingTypeName).AppendLine(" value)");
source.AppendLine(" {");
source.Append(" values.Add(new ").Append(proxyName).AppendLine("(value));");
source.AppendLine(" }");
source.AppendLine(" }");
source.AppendLine();
source.AppendLine(" return values.ToArray();");
source.AppendLine(" }");
}
private static void GenerateCreationMethod(
StringBuilder source,
INamedTypeSymbol type,
string proxyName,
string factoryTypeName,
SelectedConstructor constructor,
bool isProvisionableResource,
HashSet<string> factoryMethodSignatures)
{
var methodName = AllocateFactoryMethodName(
factoryMethodSignatures,
(isProvisionableResource ? "Add" : "Create") + factoryTypeName,
GetFactoryParameterSignature(constructor.Parameters));
var underlyingTypeName = type.ToDisplayString(s_typeDisplayFormat);
source.AppendLine();
var hasSourceDocumentation = AppendDocumentationComment(
source,
constructor.Constructor,
" ",
parameters: [.. constructor.Parameters.Select(static parameter =>
new DocumentationParameter(parameter.Parameter.Name, parameter.GeneratedName))]);
if (!hasSourceDocumentation)
{
AppendGeneratedFactoryDocumentation(source, type, constructor.Parameters, isProvisionableResource);
}
AppendInfrastructureParameterDocumentation(source, " ");
AppendFactoryExportAttribute(source, proxyName, methodName, "factory");
source.Append(" internal static ").Append(proxyName).Append(' ').Append(methodName)
.Append("(this ").Append(AzureResourceInfrastructureTypeName).Append(" infrastructure");
foreach (var parameter in constructor.Parameters)
{
source.Append(", ");
AppendMappedParameter(source, parameter, includeDefaultValue: true);
}
source.AppendLine(")");
source.AppendLine(" {");
AppendInfrastructureNullCheck(source);
source.Append(" var instance = new ").Append(underlyingTypeName).Append('(');
for (var index = 0; index < constructor.Parameters.Count; index++)
{
if (index > 0)
{
source.Append(", ");
}
var parameter = constructor.Parameters[index];
if (IsNullableFactoryProxyParameter(parameter))
{
source.Append('@').Append(parameter.GeneratedName)
.Append(" is null ? null : @").Append(parameter.GeneratedName).Append(".Inner");
}
else
{
AppendMappedToUnderlying(source, "@" + parameter.GeneratedName, parameter.Type);
}
}
source.AppendLine(");");
if (isProvisionableResource)
{
source.AppendLine(" infrastructure.Add(instance);");
}
source.AppendLine();
source.Append(" return new ").Append(proxyName).AppendLine("(instance);");
source.AppendLine(" }");
}
private static void GenerateStaticPropertyFactoryMethods(
StringBuilder source,
INamedTypeSymbol type,
Dictionary<INamedTypeSymbol, string> proxyNames,
Dictionary<INamedTypeSymbol, string> collectionNames,
string proxyName,
string factoryTypeName,
HashSet<string> factoryMethodSignatures)
{
// AzureLocation exposes every known region as a static property. Exporting those convenience
// constants into every provisioning package would dominate the opt-in API surface, while the
// generated string constructor provides the same functionality for polyglot callers.
if (IsAzureCoreType(type, AzureLocationMetadataName))
{
return;
}
foreach (var property in type.GetMembers()
.OfType<IPropertySymbol>()
.Where(static property =>
property.IsStatic &&
!property.IsIndexer &&
property.DeclaredAccessibility == Accessibility.Public &&
property.GetMethod?.DeclaredAccessibility == Accessibility.Public)
.OrderBy(static property => property.Name, StringComparer.Ordinal))
{
if (!TryMapType(property.Type, proxyNames, collectionNames, out var mappedType))
{
continue;
}
var methodName = AllocateFactoryMethodName(
factoryMethodSignatures,
"Get" + factoryTypeName + property.Name,
parameterSignature: string.Empty);
source.AppendLine();
if (!AppendDocumentationComment(source, property, " ", includeValue: true))
{
AppendDocumentationSummary(
source,
" ",
$"Gets the {property.Name} value defined by {type.Name}.");
}
AppendInfrastructureParameterDocumentation(source, " ");
AppendFactoryExportAttribute(source, proxyName, methodName, "staticProperty");
source.Append(" internal static ").Append(mappedType.ExposedTypeName).Append(' ')
.Append(methodName).Append("(this ").Append(AzureResourceInfrastructureTypeName)
.AppendLine(" infrastructure)");
source.AppendLine(" {");
AppendInfrastructureNullCheck(source);
source.Append(" return ");
AppendMappedFromUnderlying(
source,
type.ToDisplayString(s_typeDisplayFormat) + ".@" + property.Name,
mappedType);
source.AppendLine(";");
source.AppendLine(" }");
}
}
private static string AllocateFactoryMethodName(
HashSet<string> factoryMethodSignatures,
string preferredName,
string parameterSignature)
{
var methodName = preferredName;
var suffix = 2;
while (!factoryMethodSignatures.Add(methodName + "(" + parameterSignature + ")"))
{
methodName = preferredName + suffix.ToString(System.Globalization.CultureInfo.InvariantCulture);
suffix++;
}
return methodName;
}
private static string GetFactoryParameterSignature(List<MappedParameter> parameters)
{
var signature = new StringBuilder();
for (var index = 0; index < parameters.Count; index++)
{
if (index > 0)
{
signature.Append(',');
}
var parameter = parameters[index];
var typeName = parameter.Type.ExposedTypeName;
if (parameter.Parameter.Type.IsReferenceType ||
parameter.Type.Kind is MappedTypeKind.Proxy or MappedTypeKind.ProvisionableResource or MappedTypeKind.Collection)
{
typeName = typeName.TrimEnd('?');
}
signature.Append(typeName);
}
return signature.ToString();
}
private static SelectedConstructor? SelectConstructor(
INamedTypeSymbol type,
bool isProvisionableResource,
Dictionary<INamedTypeSymbol, string> proxyNames,
Dictionary<INamedTypeSymbol, string> collectionNames)
{
var constructors = new List<SelectedConstructor>();
foreach (var constructor in type.InstanceConstructors.Where(
static constructor => constructor.DeclaredAccessibility == Accessibility.Public))
{
var mappedParameters = new List<MappedParameter>();
var canMap = true;
foreach (var parameter in constructor.Parameters)
{
if (parameter.RefKind != RefKind.None ||
!TryMapType(parameter.Type, proxyNames, collectionNames, out var mappedType) ||
mappedType.Kind is MappedTypeKind.BicepValue or MappedTypeKind.Collection)
{
canMap = false;
break;
}
mappedParameters.Add(new MappedParameter(parameter, mappedType));
}
if (canMap)
{
constructors.Add(new SelectedConstructor(
constructor,
AllocateGeneratedParameterNames(mappedParameters, "infrastructure", "instance")));
}
}
if (constructors.Count == 0)
{
return null;
}
if (type.IsValueType && constructors.Any(static constructor => !constructor.Constructor.IsImplicitlyDeclared))
{
constructors.RemoveAll(static constructor => constructor.Constructor.IsImplicitlyDeclared);
}
if (!isProvisionableResource)
{
var parameterless = constructors.FirstOrDefault(static constructor => constructor.Parameters.Count == 0);
if (parameterless is not null)
{
return parameterless;
}
}
else
{
var resourceConstructors = constructors.Where(static constructor =>
constructor.Parameters.Count > 0 &&
constructor.Parameters[0].Parameter.Name == "bicepIdentifier" &&
constructor.Parameters[0].Parameter.Type.SpecialType == SpecialType.System_String).ToList();
if (resourceConstructors.Count > 0)
{
constructors = resourceConstructors;
}
}
return constructors
.OrderBy(static constructor => constructor.Parameters.Count(
static parameter => !parameter.Parameter.HasExplicitDefaultValue))
.ThenBy(static constructor => constructor.Parameters.Count)
.ThenBy(static constructor => constructor.Constructor.ToDisplayString(), StringComparer.Ordinal)
.First();
}
private static void AppendMappedParameter(
StringBuilder source,
MappedParameter mappedParameter,
bool includeDefaultValue)
{
AppendUnionAttribute(source, mappedParameter.Type, string.Empty);
source.Append(mappedParameter.Type.ExposedTypeName);
// Older SDKs have nullable-oblivious constructors such as
// AgentPoolSnapshot(string bicepIdentifier, string resourceVersion = null).
// Preserve that semantic default in the nullable-enabled generated signature.
if (includeDefaultValue &&
!mappedParameter.Type.ExposedTypeName.EndsWith("?", StringComparison.Ordinal) &&
(IsNullableFactoryProxyParameter(mappedParameter) ||
(mappedParameter.Parameter.Type.IsReferenceType &&
mappedParameter.Parameter is { HasExplicitDefaultValue: true, ExplicitDefaultValue: null })))
{
source.Append('?');
}
source.Append(" @").Append(mappedParameter.GeneratedName);
if (includeDefaultValue &&
mappedParameter.Parameter.HasExplicitDefaultValue &&
TryRenderDefaultValue(mappedParameter.Parameter, out var defaultValue))
{
source.Append(" = ").Append(defaultValue);
}
}
private static List<MappedParameter> AllocateGeneratedParameterNames(
List<MappedParameter> parameters,
params string[] reservedNames)
{
var namesRequiringTranslation = new HashSet<string>(reservedNames, StringComparer.Ordinal)
{
"arguments"
};
var usedNames = new HashSet<string>(
parameters
.Where(parameter => !namesRequiringTranslation.Contains(parameter.Parameter.Name))
.Select(static parameter => parameter.Parameter.Name),
StringComparer.Ordinal);
usedNames.UnionWith(reservedNames);
var mappedParameters = new List<MappedParameter>(parameters.Count);
foreach (var parameter in parameters)
{
var generatedName = parameter.Parameter.Name;
if (namesRequiringTranslation.Contains(generatedName))
{
// TypeScript AppHosts are ES modules, where strict mode forbids "arguments" as a
// parameter name. Factory methods also reserve their receiver and generated local
// names so valid Azure SDK constructors cannot produce duplicate C# declarations.
generatedName = GetUniqueGeneratedLocalName(
usedNames,
generatedName == "arguments" ? "args" : generatedName);
}
mappedParameters.Add(new MappedParameter(parameter.Parameter, parameter.Type, generatedName));
}
return mappedParameters;
}
private static bool IsNullableFactoryProxyParameter(MappedParameter parameter)
{
return parameter.Type.Kind is MappedTypeKind.Proxy or MappedTypeKind.ProvisionableResource &&
(parameter.Parameter.Type.NullableAnnotation == NullableAnnotation.Annotated ||
parameter.Parameter is { HasExplicitDefaultValue: true, ExplicitDefaultValue: null });
}
private static bool TryRenderDefaultValue(IParameterSymbol parameter, out string value)
{
var defaultValue = parameter.ExplicitDefaultValue;
if (defaultValue is null)
{
value = parameter.Type.IsValueType &&
!(parameter.Type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T })
? "default(" + parameter.Type.ToDisplayString(s_typeDisplayFormat) + ")"
: "null";
return true;
}
if (parameter.Type.TypeKind == TypeKind.Enum)
{
value = "(" + parameter.Type.ToDisplayString(s_typeDisplayFormat) + ")" +
Convert.ToString(defaultValue, System.Globalization.CultureInfo.InvariantCulture);
return true;
}
switch (defaultValue)
{
case string stringValue:
value = SymbolDisplay.FormatLiteral(stringValue, quote: true);
return true;
case char charValue:
value = SymbolDisplay.FormatLiteral(charValue, quote: true);
return true;
case bool boolValue:
value = boolValue ? "true" : "false";
return true;
case sbyte or byte or short or ushort or int:
value = Convert.ToString(defaultValue, System.Globalization.CultureInfo.InvariantCulture)!;
return true;
case uint uintValue:
value = uintValue.ToString(System.Globalization.CultureInfo.InvariantCulture) + "U";
return true;
case long longValue:
value = longValue.ToString(System.Globalization.CultureInfo.InvariantCulture) + "L";
return true;
case ulong ulongValue:
value = ulongValue.ToString(System.Globalization.CultureInfo.InvariantCulture) + "UL";
return true;
case float floatValue when float.IsNaN(floatValue):
value = "global::System.Single.NaN";
return true;
case float floatValue when float.IsPositiveInfinity(floatValue):
value = "global::System.Single.PositiveInfinity";
return true;
case float floatValue when float.IsNegativeInfinity(floatValue):
value = "global::System.Single.NegativeInfinity";
return true;
case float floatValue:
value = floatValue.ToString("G9", System.Globalization.CultureInfo.InvariantCulture) + "F";
return true;
case double doubleValue when double.IsNaN(doubleValue):
value = "global::System.Double.NaN";
return true;
case double doubleValue when double.IsPositiveInfinity(doubleValue):
value = "global::System.Double.PositiveInfinity";
return true;
case double doubleValue when double.IsNegativeInfinity(doubleValue):
value = "global::System.Double.NegativeInfinity";
return true;
case double doubleValue:
value = doubleValue.ToString("G17", System.Globalization.CultureInfo.InvariantCulture) + "D";
return true;
case decimal decimalValue:
value = decimalValue.ToString(System.Globalization.CultureInfo.InvariantCulture) + "M";
return true;
default:
value = string.Empty;
return false;
}
}
private static void AppendInfrastructureNullCheck(StringBuilder source)
{
source.AppendLine(" if (infrastructure is null)");
source.AppendLine(" {");
source.AppendLine(" throw new global::System.ArgumentNullException(nameof(infrastructure));");
source.AppendLine(" }");
source.AppendLine();
}
private static void AppendInfrastructureParameterDocumentation(StringBuilder source, string indentation)
{
source.Append(indentation)
.AppendLine("/// <param name=\"infrastructure\">The Azure resource infrastructure that owns the exported provisioning resources.</param>");
}
private static void AppendDocumentationSummary(StringBuilder source, string indentation, string summary)
{
AppendDocumentationElement(
source,
indentation,
new XElement("summary", summary),
parameterNames: null);
}
private static bool AppendDocumentationComment(
StringBuilder source,
ISymbol symbol,
string indentation,
IReadOnlyList<DocumentationParameter>? parameters = null,
bool includeReturns = false,
bool includeValue = false)
{
var xml = symbol.GetDocumentationCommentXml(expandIncludes: true, cancellationToken: default);
if (string.IsNullOrWhiteSpace(xml))
{
return false;
}
XElement root;
try
{
root = XElement.Parse("<root>" + xml + "</root>");
}
catch (System.Xml.XmlException)
{
return false;
}
root = root.Element("member") ?? root;
Dictionary<string, string>? parameterNames = null;
if (parameters is not null)
{
parameterNames = parameters.ToDictionary(
static parameter => parameter.SourceName,
static parameter => parameter.GeneratedName,
StringComparer.Ordinal);
}
var emittedDocumentation = AppendDocumentationElement(source, indentation, root.Element("summary"), parameterNames);
emittedDocumentation |= AppendDocumentationElement(source, indentation, root.Element("remarks"), parameterNames);
if (includeValue)
{
emittedDocumentation |= AppendDocumentationElement(source, indentation, root.Element("value"), parameterNames);
}
if (includeReturns)
{
emittedDocumentation |= AppendDocumentationElement(source, indentation, root.Element("returns"), parameterNames);
}
if (parameters is not null)
{
foreach (var parameter in parameters)
{
var parameterElement = root.Elements("param").FirstOrDefault(element =>
string.Equals(element.Attribute("name")?.Value, parameter.SourceName, StringComparison.Ordinal))
?? new XElement(
"param",
new XAttribute("name", parameter.SourceName),
$"The {parameter.SourceName} value.");
emittedDocumentation |= AppendDocumentationElement(
source,
indentation,
parameterElement,
parameterNames,
replacementName: parameter.GeneratedName);
}
}
return emittedDocumentation;
}
private static void AppendGeneratedFactoryDocumentation(
StringBuilder source,
INamedTypeSymbol type,
IReadOnlyList<MappedParameter> parameters,
bool isProvisionableResource)
{
AppendDocumentationElement(
source,
" ",
new XElement(
"summary",
$"{(isProvisionableResource ? "Adds" : "Creates")} a {type.Name} provisioning model."),
parameterNames: null);
foreach (var parameter in parameters)
{
AppendGeneratedParameterDocumentation(source, parameter);
}
}
private static void AppendGeneratedParameterDocumentation(
StringBuilder source,
IEnumerable<MappedParameter> parameters)
{
foreach (var parameter in parameters)
{
AppendGeneratedParameterDocumentation(source, parameter);
}
}
private static void AppendGeneratedParameterDocumentation(StringBuilder source, MappedParameter parameter)
{
AppendDocumentationElement(
source,
" ",
new XElement(
"param",
new XAttribute("name", parameter.GeneratedName),
$"The {parameter.Parameter.Name} value."),
parameterNames: null);
}
private static bool AppendDocumentationElement(
StringBuilder source,
string indentation,
XElement? element,
IReadOnlyDictionary<string, string>? parameterNames,
string? replacementName = null)
{
if (element is null)
{
return false;
}
// Rehydrate the XML so renamed parameters (for example, arguments -> args) are
// updated consistently while preserving safe XML documentation output.
var rewritten = RewriteDocumentationElement(element, parameterNames, replacementName);
foreach (var line in rewritten.ToString().Replace("\r\n", "\n").Split('\n'))
{
source.Append(indentation)
.Append("/// ")
.AppendLine(line);
}
return true;
}
private static XElement RewriteDocumentationElement(
XElement element,
IReadOnlyDictionary<string, string>? parameterNames,
string? replacementName)
{
var rewritten = new XElement(
element.Name,
element.Attributes().Select(attribute => RewriteDocumentationAttribute(element, attribute, parameterNames, replacementName)),
element.Nodes().Select(node => RewriteDocumentationNode(node, parameterNames)));
return rewritten;
}
private static XNode RewriteDocumentationNode(XNode node, IReadOnlyDictionary<string, string>? parameterNames)
{
return node switch
{
XElement element => RewriteDocumentationElement(element, parameterNames, replacementName: null),
XText text => new XText(text.Value),
XComment comment => new XComment(comment.Value),
_ => new XText(node.ToString())
};
}
private static XAttribute RewriteDocumentationAttribute(
XElement element,
XAttribute attribute,
IReadOnlyDictionary<string, string>? parameterNames,
string? replacementName)
{
if (attribute.Name.LocalName == "name" &&
element.Name.LocalName == "param" &&
replacementName is not null)
{
return new XAttribute(attribute.Name, replacementName);
}
if (attribute.Name.LocalName == "name" &&
element.Name.LocalName == "paramref" &&
parameterNames is not null &&
parameterNames.TryGetValue(attribute.Value, out var generatedName))
{
return new XAttribute(attribute.Name, generatedName);
}
return new XAttribute(attribute.Name, attribute.Value);
}
private static void AppendFactoryExportAttribute(
StringBuilder source,
string proxyName,
string methodName,
string suffix)
{
AppendMethodExportAttribute(
source,
FactoryClassName + "." + proxyName + "." + ToCamelCase(methodName) + "." + suffix,
methodName);
}
private static void AppendMethodExportAttribute(StringBuilder source, string capabilityId, string methodName)
{
AppendExperimentalAttribute(source);
source.Append(" [global::Aspire.Hosting.AspireExportAttribute(\"")
.Append(capabilityId)
.Append("\", MethodName = \"")
.Append(ToCamelCase(methodName))
.AppendLine("\")]");
}
private static void AppendExperimentalAttribute(StringBuilder source)
{
source.Append(" [global::System.Diagnostics.CodeAnalysis.ExperimentalAttribute(\"")
.Append(ExperimentalDiagnosticId)
.Append("\", UrlFormat = \"")
.Append(ExperimentalUrlFormat)
.AppendLine("\")]");
}
private static void AppendUnionAttribute(StringBuilder source, MappedType mappedType, string indentation)
{
if (mappedType is { Kind: MappedTypeKind.BicepValue, LiteralTypeName: not null })
{
source.Append(indentation)
.Append("[global::Aspire.Hosting.AspireUnionAttribute(typeof(")
.Append(BicepValueProxyTypeName)
.Append("), typeof(")
.Append(mappedType.LiteralTypeName)
.Append("))] ");
}
}
private static string ToCamelCase(string value)
{
return value.Length == 0
? value
: char.ToLowerInvariant(value[0]) + value.Substring(1);
}
private static string PluralizeIdentifier(string value)
{
if (value.Length > 1 &&
value.EndsWith("y", StringComparison.Ordinal) &&
"aeiouAEIOU".IndexOf(value[value.Length - 2]) < 0)
{
return value.Substring(0, value.Length - 1) + "ies";
}
return value + "s";
}
private static INamedTypeSymbol? GetProxyBaseType(
INamedTypeSymbol type,
Dictionary<INamedTypeSymbol, string> proxyNames)
{
for (var current = type.BaseType; current is not null; current = current.BaseType)
{
if (proxyNames.ContainsKey(current))
{
return current;
}
}
return null;
}
private static IEnumerable<IPropertySymbol> GetExportableProperties(
INamedTypeSymbol type,
bool includeInherited = true,
INamedTypeSymbol? stopBeforeType = null)
{
var seenPropertyNames = new HashSet<string>(StringComparer.Ordinal);
for (INamedTypeSymbol? current = type;
current is not null &&
!SymbolEqualityComparer.Default.Equals(current, stopBeforeType) &&
current.SpecialType is not SpecialType.System_Object and not SpecialType.System_ValueType;
current = includeInherited ? current.BaseType : null)
{
if (IsProvisionableFrameworkBase(current))
{
yield break;
}
foreach (var property in current.GetMembers().OfType<IPropertySymbol>())
{
if (!property.IsStatic &&
property.DeclaredAccessibility == Accessibility.Public &&
seenPropertyNames.Add(property.Name))
{
yield return property;
}
}
}
}
private static IEnumerable<IMethodSymbol> GetExportableMethods(
INamedTypeSymbol type,
bool includeInherited = true,
INamedTypeSymbol? stopBeforeType = null)
{
var seenSignatures = new HashSet<string>(StringComparer.Ordinal);
for (INamedTypeSymbol? current = type;
current is not null &&
!SymbolEqualityComparer.Default.Equals(current, stopBeforeType) &&
current.SpecialType is not SpecialType.System_Object and not SpecialType.System_ValueType;
current = includeInherited ? current.BaseType : null)
{
if (IsProvisionableFrameworkBase(current))
{
yield break;
}
foreach (var method in current.GetMembers().OfType<IMethodSymbol>())
{
if (!method.IsStatic &&
method.MethodKind == MethodKind.Ordinary &&
method.Name is not nameof(ToString) and not nameof(Equals) and not nameof(GetHashCode) &&
method.DeclaredAccessibility == Accessibility.Public &&
seenSignatures.Add(GetMethodSignature(method)))
{
yield return method;
}
}
}
}
private static string GetMethodSignature(IMethodSymbol method)
{
return method.Name + "(" + string.Join(
",",
method.Parameters.Select(static parameter =>
parameter.RefKind + ":" +
parameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))) + ")";
}
private static bool TryMapType(
ITypeSymbol type,
Dictionary<INamedTypeSymbol, string> proxyNames,
Dictionary<INamedTypeSymbol, string> collectionNames,
out MappedType mappedType)
{
if (type.SpecialType == SpecialType.System_Void)
{
mappedType = new MappedType("void", MappedTypeKind.Direct);
return true;
}
if (TryGetBicepValueArgument(type, out var bicepValueType))
{
var literalType = bicepValueType is INamedTypeSymbol
{
OriginalDefinition.SpecialType: SpecialType.System_Nullable_T,
TypeArguments.Length: 1
} nullableValueType
? nullableValueType.TypeArguments[0]
: bicepValueType.WithNullableAnnotation(NullableAnnotation.NotAnnotated);
var literalTypeName = IsSimpleType(bicepValueType)
? literalType.ToDisplayString(s_typeDisplayFormat).TrimEnd('?')
: null;
mappedType = new MappedType(
literalTypeName is null ? BicepValueProxyTypeName : "object",
MappedTypeKind.BicepValue,
literalTypeName,
bicepValueType.ToDisplayString(s_typeDisplayFormat));
return true;
}
if (IsSimpleType(type))
{
mappedType = new MappedType(type.ToDisplayString(s_typeDisplayFormat), MappedTypeKind.Direct);
return true;
}
var isNullableValueType = TryGetNullableValueType(type, out var nullableProxyValueType);
if (isNullableValueType)
{
type = nullableProxyValueType;
}
if (type is INamedTypeSymbol namedType)
{
if (proxyNames.TryGetValue(namedType, out var proxyName))
{
var isNullable = isNullableValueType || type.NullableAnnotation == NullableAnnotation.Annotated;
mappedType = new MappedType(
proxyName + (isNullable ? "?" : string.Empty),
MappedTypeKind.Proxy,
isNullable: isNullable,
isNullableValueType: isNullableValueType);
return true;
}
if (collectionNames.TryGetValue(namedType, out var collectionProxyName))
{
var isNullable = type.NullableAnnotation == NullableAnnotation.Annotated;
mappedType = new MappedType(collectionProxyName + (isNullable ? "?" : string.Empty), MappedTypeKind.Collection, isNullable: isNullable);
return true;
}
if (IsProvisionableResourceBase(namedType))
{
var isNullable = type.NullableAnnotation == NullableAnnotation.Annotated;
mappedType = new MappedType(
ProvisionableResourceProxyTypeName + (isNullable ? "?" : string.Empty),
MappedTypeKind.ProvisionableResource,
isNullable: isNullable);
return true;
}
}
mappedType = default;
return false;
}
private static bool TryGetBicepValueArgument(ITypeSymbol type, out ITypeSymbol valueType)
{
if (type is INamedTypeSymbol { IsGenericType: true, TypeArguments.Length: 1 } namedType &&
namedType.ConstructedFrom.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat) == BicepValueMetadataName)
{
valueType = namedType.TypeArguments[0];
return true;
}
valueType = null!;
return false;
}
private static bool TryGetNullableValueType(ITypeSymbol type, out ITypeSymbol valueType)
{
if (type is INamedTypeSymbol
{
OriginalDefinition.SpecialType: SpecialType.System_Nullable_T,
TypeArguments.Length: 1
} nullableType)
{
valueType = nullableType.TypeArguments[0];
return true;
}
valueType = null!;
return false;
}
private static bool TryGetCollection(
ITypeSymbol type,
out CollectionKind kind,
out ITypeSymbol elementType,
out INamedTypeSymbol collectionType)
{
if (type is INamedTypeSymbol { IsGenericType: true, TypeArguments.Length: 1 } namedType)
{
var metadataName = namedType.ConstructedFrom.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
if (metadataName == BicepListMetadataName)
{
kind = CollectionKind.List;
elementType = namedType.TypeArguments[0];
collectionType = namedType;
return true;
}
if (metadataName == BicepDictionaryMetadataName)
{
kind = CollectionKind.Dictionary;
elementType = namedType.TypeArguments[0];
collectionType = namedType;
return true;
}
}
kind = default;
elementType = null!;
collectionType = null!;
return false;
}
private static bool IsProvisionableResourceBase(INamedTypeSymbol type)
{
return type.MetadataName == "ProvisionableResource" &&
type.ContainingNamespace.ToDisplayString() == "Azure.Provisioning.Primitives";
}
private static bool IsSharedProxyType(INamedTypeSymbol type)
{
return type.ContainingAssembly.Name == CoreProvisioningAssemblyName ||
IsAzureCoreType(type, AzureLocationMetadataName) ||
IsAzureCoreType(type, ResourceIdentifierMetadataName) ||
IsAzureCoreType(type, ResourceTypeMetadataName);
}
private static bool IsAzureCoreType(INamedTypeSymbol type, string metadataName)
{
return type.ContainingAssembly.Name == AzureCoreAssemblyName &&
type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat) == metadataName;
}
private static bool IsProvisionableFrameworkBase(INamedTypeSymbol type)
{
return type.ContainingNamespace.ToDisplayString() == "Azure.Provisioning.Primitives" &&
type.MetadataName is
"Provisionable" or
"ProvisionableConstruct" or
"NamedProvisionableConstruct" or
"ProvisionableResource";
}
private static bool CanGenerateProxy(INamedTypeSymbol type)
{
return type.TypeKind is TypeKind.Class or TypeKind.Struct &&
!type.IsGenericType &&
!type.IsStatic;
}
private static bool IsMutableStruct(INamedTypeSymbol type)
{
return type.TypeKind == TypeKind.Struct && !type.IsReadOnly;
}
private static Location GetDiagnosticLocation(ISymbol symbol)
{
return symbol.Locations.FirstOrDefault(static location => location.IsInSource) ?? Location.None;
}
private static bool IsExcluded(
ISymbol member,
HashSet<string> excludedMemberNames)
{
return IsNativelyExcluded(member) ||
excludedMemberNames.Contains(member.Name) ||
excludedMemberNames.Contains(
member.ContainingType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat) +
"." +
member.Name);
}
private static bool IsNativelyExcluded(ISymbol member)
{
if (member is IPropertySymbol property &&
property.Type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat) == SystemDataMetadataName)
{
return true;
}
if (member is IMethodSymbol
{
Name: "GetResourceNameRequirements",
Parameters.Length: 0,
OverriddenMethod: { } overriddenMethod
})
{
for (var current = overriddenMethod; current is not null; current = current.OverriddenMethod)
{
if (IsProvisionableResourceBase(current.ContainingType))
{
return true;
}
}
}
return false;
}
private static bool IsProvisionableResource(INamedTypeSymbol type)
{
for (INamedTypeSymbol? current = type; current is not null; current = current.BaseType)
{
if (IsProvisionableResourceBase(current))
{
return true;
}
}
return false;
}
private static bool IsIPAddress(ITypeSymbol type)
{
return type.ToDisplayString(s_typeDisplayFormat) == "global::System.Net.IPAddress";
}
private static bool IsSimpleType(ITypeSymbol type)
{
if (type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } nullableType)
{
return IsSimpleType(nullableType.TypeArguments[0]);
}
if (type.TypeKind == TypeKind.Enum)
{
return true;
}
if (type.SpecialType is
SpecialType.System_String or
SpecialType.System_Char or
SpecialType.System_Boolean or
SpecialType.System_SByte or
SpecialType.System_Byte or
SpecialType.System_Int16 or
SpecialType.System_UInt16 or
SpecialType.System_Int32 or
SpecialType.System_UInt32 or
SpecialType.System_Int64 or
SpecialType.System_UInt64 or
SpecialType.System_Single or
SpecialType.System_Double or
SpecialType.System_Decimal)
{
return true;
}
var metadataName = type.WithNullableAnnotation(NullableAnnotation.NotAnnotated)
.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
return metadataName is
"System.DateTime" or
"System.DateTimeOffset" or
"System.DateOnly" or
"System.TimeOnly" or
"System.TimeSpan" or
"System.Guid" or
"System.Uri";
}
private static void AppendMappedFromUnderlying(StringBuilder source, string expression, MappedType mappedType)
{
switch (mappedType.Kind)
{
case MappedTypeKind.Direct:
source.Append(expression);
break;
case MappedTypeKind.BicepValue:
source.Append(BicepValueProxyTypeName).Append(".Create(").Append(expression).Append(')');
break;
case MappedTypeKind.Proxy:
case MappedTypeKind.Collection:
case MappedTypeKind.ProvisionableResource:
if (mappedType.IsNullable)
{
source.Append(expression).Append(" is null ? null : new ")
.Append(mappedType.ExposedTypeName.TrimEnd('?')).Append('(').Append(expression);
if (mappedType.IsNullableValueType)
{
source.Append(".Value");
}
source.Append(')');
}
else
{
source.Append("new ").Append(mappedType.ExposedTypeName).Append('(').Append(expression).Append(')');
}
break;
}
}
private static void AppendMappedToUnderlying(StringBuilder source, string expression, MappedType mappedType)
{
if (mappedType.Kind is MappedTypeKind.Proxy or MappedTypeKind.Collection or MappedTypeKind.ProvisionableResource)
{
source.Append(expression).Append(mappedType.IsNullable ? "?.Inner" : ".Inner");
}
else if (mappedType.Kind == MappedTypeKind.BicepValue)
{
source.Append(BicepValueProxyTypeName)
.Append(".Convert<")
.Append(mappedType.ValueTypeName)
.Append(">(")
.Append(expression)
.Append(')');
}
else
{
source.Append(expression);
}
}
private enum CollectionKind
{
List,
Dictionary
}
private enum MappedTypeKind
{
Direct,
BicepValue,
Proxy,
Collection,
ProvisionableResource
}
private sealed class DiscoveryResult
{
public DiscoveryResult(List<INamedTypeSymbol> types, List<CollectionShape> collections)
{
Types = types;
Collections = collections;
}
public List<INamedTypeSymbol> Types { get; }
public List<CollectionShape> Collections { get; }
}
private readonly struct ProxyRoot
{
public ProxyRoot(
INamedTypeSymbol type,
bool isInfrastructureRoot,
bool includeContainingAssemblyTypes,
ImmutableArray<string> excludedMemberNames)
{
Type = type;
IsInfrastructureRoot = isInfrastructureRoot;
IncludeContainingAssemblyTypes = includeContainingAssemblyTypes;
ExcludedMemberNames = excludedMemberNames;
}
public INamedTypeSymbol Type { get; }
public bool IsInfrastructureRoot { get; }
public bool IncludeContainingAssemblyTypes { get; }
public ImmutableArray<string> ExcludedMemberNames { get; }
}
private readonly struct CollectionShape
{
public CollectionShape(INamedTypeSymbol type, ITypeSymbol elementType, CollectionKind kind)
{
Type = type;
ElementType = elementType;
Kind = kind;
}
public INamedTypeSymbol Type { get; }
public ITypeSymbol ElementType { get; }
public CollectionKind Kind { get; }
}
private sealed class MappedMethod
{
public MappedMethod(
IMethodSymbol method,
MappedType returnType,
List<MappedParameter> parameters,
string signature)
{
Method = method;
ReturnType = returnType;
Parameters = parameters;
Signature = signature;
}
public IMethodSymbol Method { get; }
public MappedType ReturnType { get; }
public List<MappedParameter> Parameters { get; }
public string Signature { get; }
}
private sealed class SelectedConstructor
{
public SelectedConstructor(IMethodSymbol constructor, List<MappedParameter> parameters)
{
Constructor = constructor;
Parameters = parameters;
}
public IMethodSymbol Constructor { get; }
public List<MappedParameter> Parameters { get; }
}
private readonly struct MappedParameter
{
public MappedParameter(IParameterSymbol parameter, MappedType type)
: this(parameter, type, parameter.Name)
{
}
public MappedParameter(IParameterSymbol parameter, MappedType type, string generatedName)
{
Parameter = parameter;
Type = type;
GeneratedName = generatedName;
}
public IParameterSymbol Parameter { get; }
public MappedType Type { get; }
public string GeneratedName { get; }
}
private readonly struct DocumentationParameter
{
public DocumentationParameter(string sourceName, string generatedName)
{
SourceName = sourceName;
GeneratedName = generatedName;
}
public string SourceName { get; }
public string GeneratedName { get; }
}
private readonly struct MappedType
{
public MappedType(
string exposedTypeName,
MappedTypeKind kind,
string? literalTypeName = null,
string? valueTypeName = null,
bool isNullable = false,
bool isNullableValueType = false)
{
ExposedTypeName = exposedTypeName;
Kind = kind;
LiteralTypeName = literalTypeName;
ValueTypeName = valueTypeName;
IsNullable = isNullable;
IsNullableValueType = isNullableValueType;
}
public string ExposedTypeName { get; }
public MappedTypeKind Kind { get; }
public string? LiteralTypeName { get; }
public string? ValueTypeName { get; }
public bool IsNullable { get; }
public bool IsNullableValueType { get; }
}
}