File: Ats\AttributeDataReader.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Reflection;
 
namespace Aspire.Hosting.Ats;
 
/// <summary>
/// Provides full-name-based discovery of ATS attributes from <see cref="CustomAttributeData"/>,
/// so that third-party authors can define their own attribute types with the same full name
/// without requiring a package reference to Aspire.Hosting.
/// </summary>
internal static class AttributeDataReader
{
    private static readonly string s_aspireExportAttributeFullName = typeof(AspireExportAttribute).FullName!;
    private static readonly string s_aspireExportIgnoreAttributeFullName = typeof(AspireExportIgnoreAttribute).FullName!;
    private static readonly string s_aspireDtoAttributeFullName = typeof(AspireDtoAttribute).FullName!;
    private static readonly string s_aspireUnionAttributeFullName = typeof(AspireUnionAttribute).FullName!;
 
    // --- AspireExport lookup ---
 
    internal static AspireExportData? GetAspireExportData(Type type)
        => FindSingleAttribute<AspireExportData>(type.GetCustomAttributesData(), s_aspireExportAttributeFullName, ParseAspireExportData);
 
    internal static AspireExportData? GetAspireExportData(MethodInfo method)
        => FindSingleAttribute<AspireExportData>(method.GetCustomAttributesData(), s_aspireExportAttributeFullName, ParseAspireExportData);
 
    internal static AspireExportData? GetAspireExportData(PropertyInfo property)
        => FindSingleAttribute<AspireExportData>(property.GetCustomAttributesData(), s_aspireExportAttributeFullName, ParseAspireExportData);
 
    internal static IEnumerable<AspireExportData> GetAspireExportDataAll(Assembly assembly)
        => FindAllAttributes(assembly.GetCustomAttributesData(), s_aspireExportAttributeFullName, ParseAspireExportData);
 
    // --- AspireExportIgnore lookup ---
 
    internal static bool HasAspireExportIgnoreData(PropertyInfo property)
        => HasAttribute(property.GetCustomAttributesData(), s_aspireExportIgnoreAttributeFullName);
 
    internal static bool HasAspireExportIgnoreData(MethodInfo method)
        => HasAttribute(method.GetCustomAttributesData(), s_aspireExportIgnoreAttributeFullName);
 
    // --- AspireDto lookup ---
 
    internal static bool HasAspireDtoData(Type type)
        => HasAttribute(type.GetCustomAttributesData(), s_aspireDtoAttributeFullName);
 
    // --- AspireUnion lookup ---
 
    internal static AspireUnionData? GetAspireUnionData(ParameterInfo parameter)
        => FindSingleAttribute<AspireUnionData>(parameter.GetCustomAttributesData(), s_aspireUnionAttributeFullName, ParseAspireUnionData);
 
    internal static AspireUnionData? GetAspireUnionData(PropertyInfo property)
        => FindSingleAttribute<AspireUnionData>(property.GetCustomAttributesData(), s_aspireUnionAttributeFullName, ParseAspireUnionData);
 
    // --- Generic helpers ---
 
    private static bool HasAttribute(IList<CustomAttributeData> attributes, string attributeFullName)
    {
        for (var i = 0; i < attributes.Count; i++)
        {
            if (IsMatch(attributes[i], attributeFullName))
            {
                return true;
            }
        }
 
        return false;
    }
 
    private static T? FindSingleAttribute<T>(IList<CustomAttributeData> attributes, string attributeFullName, Func<CustomAttributeData, T> parser) where T : class
    {
        for (var i = 0; i < attributes.Count; i++)
        {
            if (IsMatch(attributes[i], attributeFullName))
            {
                return parser(attributes[i]);
            }
        }
 
        return null;
    }
 
    private static IEnumerable<T> FindAllAttributes<T>(IList<CustomAttributeData> attributes, string attributeFullName, Func<CustomAttributeData, T> parser) where T : class
    {
        for (var i = 0; i < attributes.Count; i++)
        {
            if (IsMatch(attributes[i], attributeFullName))
            {
                yield return parser(attributes[i]);
            }
        }
    }
 
    private static bool IsMatch(CustomAttributeData data, string attributeFullName)
    {
        return string.Equals(data.AttributeType.FullName, attributeFullName, StringComparison.Ordinal);
    }
 
    // --- Parsers ---
 
    private static AspireExportData ParseAspireExportData(CustomAttributeData data)
    {
        string? id = null;
        Type? type = null;
 
        // Match constructor arguments by signature (arity + type) rather than parameter name,
        // so third-party attribute copies with different parameter names still work.
        // The three recognized constructor signatures are:
        //   ()           — type export
        //   (string)     — capability export (the string is the method/capability id)
        //   (Type)       — assembly-level type export
        if (data.ConstructorArguments.Count == 1)
        {
            var arg = data.ConstructorArguments[0];
            if (arg.Value is string idValue)
            {
                id = idValue;
            }
            else if (arg.Value is Type typeValue)
            {
                type = typeValue;
            }
        }
 
        // Read named arguments
        string? description = null;
        string? methodName = null;
        var exposeProperties = false;
        var exposeMethods = false;
        var runSyncOnBackgroundThread = false;
 
        for (var i = 0; i < data.NamedArguments.Count; i++)
        {
            var named = data.NamedArguments[i];
            switch (named.MemberName)
            {
                case nameof(AspireExportData.Type):
                    if (named.TypedValue.Value is Type namedType)
                    {
                        type = namedType;
                    }
                    break;
                case nameof(AspireExportData.Description):
                    description = named.TypedValue.Value as string;
                    break;
                case nameof(AspireExportData.MethodName):
                    methodName = named.TypedValue.Value as string;
                    break;
                case nameof(AspireExportData.ExposeProperties):
                    if (named.TypedValue.Value is bool ep)
                    {
                        exposeProperties = ep;
                    }
                    break;
                case nameof(AspireExportData.ExposeMethods):
                    if (named.TypedValue.Value is bool em)
                    {
                        exposeMethods = em;
                    }
                    break;
                case nameof(AspireExportData.RunSyncOnBackgroundThread):
                    if (named.TypedValue.Value is bool rs)
                    {
                        runSyncOnBackgroundThread = rs;
                    }
                    break;
            }
        }
 
        return new AspireExportData
        {
            Id = id,
            Type = type,
            Description = description,
            MethodName = methodName,
            ExposeProperties = exposeProperties,
            ExposeMethods = exposeMethods,
            RunSyncOnBackgroundThread = runSyncOnBackgroundThread
        };
    }
 
    private static AspireUnionData ParseAspireUnionData(CustomAttributeData data)
    {
        // The constructor is AspireUnionAttribute(params Type[] types).
        // CustomAttributeData represents params as either:
        //   1. A single constructor argument of type Type[] (ReadOnlyCollection<CustomAttributeTypedArgument>)
        //   2. Multiple individual constructor arguments of type Type
        var types = new List<Type>();
 
        if (data.ConstructorArguments.Count == 1 &&
            data.ConstructorArguments[0].Value is IReadOnlyCollection<CustomAttributeTypedArgument> elements)
        {
            // params represented as a single array argument
            foreach (var element in elements)
            {
                if (element.Value is Type t)
                {
                    types.Add(t);
                }
            }
        }
        else
        {
            // params represented as individual arguments
            for (var i = 0; i < data.ConstructorArguments.Count; i++)
            {
                if (data.ConstructorArguments[i].Value is Type t)
                {
                    types.Add(t);
                }
            }
        }
 
        return new AspireUnionData
        {
            Types = [.. types]
        };
    }
}
 
/// <summary>
/// Name-based adapter for [AspireExport] attribute data, parsed from <see cref="CustomAttributeData"/>.
/// </summary>
internal sealed class AspireExportData
{
    /// <summary>
    /// Gets the method name / capability id from the constructor argument.
    /// </summary>
    public string? Id { get; init; }
 
    /// <summary>
    /// Gets the CLR type for assembly-level type exports.
    /// </summary>
    public Type? Type { get; init; }
 
    /// <summary>
    /// Gets a description of what this export does.
    /// </summary>
    public string? Description { get; init; }
 
    /// <summary>
    /// Gets the method name override for generated polyglot SDKs.
    /// </summary>
    public string? MethodName { get; init; }
 
    /// <summary>
    /// Gets whether to expose properties of this type as ATS capabilities.
    /// </summary>
    public bool ExposeProperties { get; init; }
 
    /// <summary>
    /// Gets whether to expose public instance methods of this type as ATS capabilities.
    /// </summary>
    public bool ExposeMethods { get; init; }
 
    /// <summary>
    /// Gets whether synchronous exported methods should be invoked on a background thread by the ATS dispatcher.
    /// </summary>
    public bool RunSyncOnBackgroundThread { get; init; }
}
 
/// <summary>
/// Name-based adapter for [AspireUnion] attribute data, parsed from <see cref="CustomAttributeData"/>.
/// </summary>
internal sealed class AspireUnionData
{
    /// <summary>
    /// Gets the CLR types that form the union.
    /// </summary>
    public required Type[] Types { get; init; }
}