File: Project\ConnectionBuilderExtensions.cs
Web Access
Project: src\src\Aspire.Hosting.Foundry\Aspire.Hosting.Foundry.csproj (Aspire.Hosting.Foundry)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Azure;
using Aspire.Hosting.Foundry;
using Azure.Provisioning;
using Azure.Provisioning.CognitiveServices;
using Azure.Provisioning.Expressions;
using Azure.Provisioning.KeyVault;
using Azure.Provisioning.Search;
using Azure.Provisioning.Storage;
 
namespace Aspire.Hosting;
 
/// <summary>
/// Extension methods for adding Microsoft Foundry project connection resources to the distributed application model.
/// </summary>
public static class AzureCognitiveServicesProjectConnectionsBuilderExtensions
{
    private const string BingAccountsResourceVersion = "2020-06-10";
 
    /// <summary>
    /// Adds a Microsoft Foundry project connection resource to a project. This is a low level
    /// interface that requires the caller to specify all connection properties.
    /// </summary>
    /// <param name="builder">The <see cref="IResourceBuilder{T}"/> for the parent Microsoft Foundry project resource.</param>
    /// <param name="name">The name of the Microsoft Foundry project connection resource.</param>
    /// <param name="configureProperties">Action to customize the resource.</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/> for the Microsoft Foundry project connection resource.</returns>
    /// <remarks>This method is not available in polyglot app hosts.</remarks>
    [AspireExportIgnore(Reason = "The configureProperties callback returns Azure provisioning types that are not ATS-compatible.")]
    public static IResourceBuilder<AzureCognitiveServicesProjectConnectionResource> AddConnection(
        this IResourceBuilder<AzureCognitiveServicesProjectResource> builder,
        [ResourceName] string name,
        Func<AzureResourceInfrastructure, CognitiveServicesConnectionProperties> configureProperties)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(name);
 
        void configureInfrastructure(AzureResourceInfrastructure infrastructure)
        {
            var aspireResource = (AzureCognitiveServicesProjectConnectionResource)infrastructure.AspireResource;
            var projectBicepId = aspireResource.Parent.GetBicepIdentifier();
            var project = aspireResource.Parent.AddAsExistingResource(infrastructure);
 
            var connection = AzureProvisioningResource.CreateExistingOrNewProvisionableResource(
                infrastructure,
                (identifier, resourceName) =>
                {
                    var resource = aspireResource.FromExisting(identifier);
                    resource.Parent = project;
                    resource.Name = resourceName;
                    return resource;
                },
                infra =>
                {
                    var resource = new CognitiveServicesProjectConnection(aspireResource.GetBicepIdentifier(), AzureCognitiveServicesProjectConnectionResource.ResourceVersion)
                    {
                        Parent = project,
                        Name = name,
                        Properties = configureProperties(infra)
                    };
                    return resource;
                });
            if (aspireResource.Parent.KeyVaultConn is not null)
            {
                var keyVaultConn = aspireResource.Parent.KeyVaultConn.AddAsExistingResource(infrastructure);
                connection.DependsOn.Add(keyVaultConn);
            }
            infrastructure.Add(new ProvisioningOutput("name", typeof(string)) { Value = connection.Name });
            infrastructure.Add(new ProvisioningOutput("id", typeof(string)) { Value = connection.Id });
        }
        var connectionResource = new AzureCognitiveServicesProjectConnectionResource(name, configureInfrastructure, builder.Resource);
        return builder.ApplicationBuilder.AddResource(connectionResource);
    }
 
    /// <summary>
    /// Adds CosmosDB to a project as a connection
    /// </summary>
    [AspireExportIgnore(Reason = "Raw AzureCosmosDBResource parameters are not ATS-compatible. Use the resource-builder overload instead.")]
    public static IResourceBuilder<AzureCognitiveServicesProjectConnectionResource> AddConnection(
        this IResourceBuilder<AzureCognitiveServicesProjectResource> builder,
        AzureCosmosDBResource db)
    {
        ArgumentNullException.ThrowIfNull(builder);
        if (db.IsEmulator())
        {
            throw new InvalidOperationException("Cannot create a Microsoft Foundry project connection to an emulator Cosmos DB instance.");
        }
        return builder.AddConnection($"connection-{Guid.NewGuid():N}", (infra) => new AadAuthTypeConnectionProperties()
        {
            Category = CognitiveServicesConnectionCategory.CosmosDB,
            Target = db.ConnectionStringOutput.AsProvisioningParameter(infra),
            IsSharedToAll = true,
            Metadata =
            {
                { "ApiType", "Azure" },
                { "ResourceId", db.Id.AsProvisioningParameter(infra) }
            }
        });
    }
 
    /// <summary>
    /// Adds CosmosDB to a project as a connection
    /// </summary>
    [AspireExport("addCosmosConnection")]
    public static IResourceBuilder<AzureCognitiveServicesProjectConnectionResource> AddConnection(
        this IResourceBuilder<AzureCognitiveServicesProjectResource> builder,
        IResourceBuilder<AzureCosmosDBResource> db)
    {
        return builder.AddConnection(db.Resource);
    }
 
    /// <summary>
    /// Adds an Azure Storage account to a project as a connection.
    /// </summary>
    /// <returns></returns>
    [AspireExportIgnore(Reason = "Raw AzureStorageResource parameters are not ATS-compatible. Use the resource-builder overload instead.")]
    public static IResourceBuilder<AzureCognitiveServicesProjectConnectionResource> AddConnection(
        this IResourceBuilder<AzureCognitiveServicesProjectResource> builder,
        AzureStorageResource storage)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(storage);
        if (storage.IsEmulator())
        {
            throw new InvalidOperationException("Cannot create a Microsoft Foundry project connection to an emulator Storage account.");
        }
        return builder.AddConnection($"connection-{Guid.NewGuid():N}", (infra) => new AadAuthTypeConnectionProperties()
        {
            Category = CognitiveServicesConnectionCategory.AzureBlob,
            Target = storage.BlobEndpoint.AsProvisioningParameter(infra),
            IsSharedToAll = true,
            Metadata =
            {
                { "ApiType", "Azure" },
                { "ResourceId", storage.Id.AsProvisioningParameter(infra) }
            }
        });
    }
 
    /// <summary>
    /// Adds an Azure Storage account to a project as a connection.
    /// </summary>
    [AspireExport("addStorageConnection")]
    public static IResourceBuilder<AzureCognitiveServicesProjectConnectionResource> AddConnection(
        this IResourceBuilder<AzureCognitiveServicesProjectResource> builder,
        IResourceBuilder<AzureStorageResource> storage)
    {
        builder.WithRoleAssignments(storage, StorageBuiltInRole.StorageBlobDataContributor);
        return builder.AddConnection(storage.Resource);
    }
 
    /// <summary>
    /// Adds a container registry connection to the Microsoft Foundry project.
    /// </summary>
    /// <returns></returns>
    [AspireExportIgnore(Reason = "Raw AzureContainerRegistryResource parameters are not ATS-compatible. Use the resource-builder overload instead.")]
    public static IResourceBuilder<AzureCognitiveServicesProjectConnectionResource> AddConnection(
        this IResourceBuilder<AzureCognitiveServicesProjectResource> builder,
        AzureContainerRegistryResource registry)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(registry);
        if (registry.IsEmulator())
        {
            throw new InvalidOperationException("Cannot create a Microsoft Foundry project connection to an emulator Container Registry");
        }
        return builder.AddConnection($"connection-{Guid.NewGuid():N}", (infra) => new ManagedIdentityAuthTypeConnectionProperties()
        {
            Category = CognitiveServicesConnectionCategory.ContainerRegistry,
            Target = registry.RegistryEndpoint.AsProvisioningParameter(infra),
            IsSharedToAll = true,
            Credentials = new CognitiveServicesConnectionManagedIdentity(){
                ClientId = "aiprojectidentityprincipleaid",
                ResourceId = registry.NameOutputReference.AsProvisioningParameter(infra)
            },
            Metadata =
            {
                { "ApiType", "Azure" },
                { "ResourceId", registry.NameOutputReference.AsProvisioningParameter(infra) }
            }
        });
    }
 
    /// <summary>
    /// Adds a container registry connection to the Microsoft Foundry project.
    /// </summary>
    /// <returns></returns>
    [AspireExport("addContainerRegistryConnection")]
    public static IResourceBuilder<AzureCognitiveServicesProjectConnectionResource> AddConnection(
        this IResourceBuilder<AzureCognitiveServicesProjectResource> builder,
        IResourceBuilder<AzureContainerRegistryResource> registry)
    {
        return builder.AddConnection(registry.Resource);
    }
 
    /// <summary>
    /// Adds an Azure AI Search connection to a Microsoft Foundry project.
    /// </summary>
    [AspireExportIgnore(Reason = "Raw AzureSearchResource parameters are not ATS-compatible. Use the resource-builder overload instead.")]
    public static IResourceBuilder<AzureCognitiveServicesProjectConnectionResource> AddConnection(
        this IResourceBuilder<AzureCognitiveServicesProjectResource> builder,
        AzureSearchResource search)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(search);
 
        return builder.AddConnection($"connection-{Guid.NewGuid():N}", (infra) =>
        {
            var searchService = (SearchService)search.AddAsExistingResource(infra);
            return new AadAuthTypeConnectionProperties()
            {
                Category = CognitiveServicesConnectionCategory.CognitiveSearch,
                Target = BicepFunction.Interpolate($"https://{searchService.Name}.search.windows.net"),
                Metadata =
                {
                    { "ApiType", "Azure" },
                    { "ResourceId", searchService.Id },
                    { "location", searchService.Location }
                }
            };
        });
    }
 
    /// <summary>
    /// Adds an Azure AI Search connection to a Microsoft Foundry project.
    /// </summary>
    [AspireExport("addSearchConnection")]
    public static IResourceBuilder<AzureCognitiveServicesProjectConnectionResource> AddConnection(
        this IResourceBuilder<AzureCognitiveServicesProjectResource> builder,
        IResourceBuilder<AzureSearchResource> search)
    {
        builder.WithRoleAssignments(search,
            SearchBuiltInRole.SearchIndexDataReader,
            SearchBuiltInRole.SearchServiceContributor);
        return builder.AddConnection(search.Resource);
    }
 
    /// <summary>
    /// Adds a Key Vault connection to the Microsoft Foundry project.
    /// </summary>
    /// <remarks>
    /// This connection allows the Microsoft Foundry project to store secrets for various other connections.
    /// As such, we recommend adding this connection *before* any others, so that those connections
    /// can leverage the Key Vault connection for secret storage.
    /// </remarks>
    [AspireExport("addKeyVaultConnection")]
    public static IResourceBuilder<AzureCognitiveServicesProjectConnectionResource> AddConnection(
        this IResourceBuilder<AzureCognitiveServicesProjectResource> builder,
        IResourceBuilder<AzureKeyVaultResource> keyVault)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(keyVault);
        if (keyVault.Resource.IsEmulator())
        {
            throw new InvalidOperationException("Cannot create a Microsoft Foundry project connection to an emulator Key Vault.");
        }
        builder.WithRoleAssignments(keyVault, KeyVaultBuiltInRole.KeyVaultSecretsOfficer);
        // Configuration based on https://github.com/azure-ai-foundry/foundry-samples/blob/9551912af4d4fdb8ea73e996145e940a7e369c84/infrastructure/infrastructure-setup-bicep/01-connections/connection-key-vault.bicep
        // We use a custom subclass because Azure.Provisioning.CognitiveServices does not support the "AzureKeyVault" connection category yet (as of 2026-01-06).
        // We also swap `ManagedIdentity` auth type for `AccountManagedIdentity`, because the latter seems to be an error in the Bicep template.
        return builder.AddConnection($"{keyVault.Resource.Name}-{Guid.NewGuid():N}", (infra) =>
        {
            var vault = (KeyVaultService)keyVault.Resource.AddAsExistingResource(infra);
            return new AzureKeyVaultConnectionProperties()
            {
                Target = vault.Id,
                IsSharedToAll = true,
                Metadata =
                {
                    { "ApiType", "Azure" },
                    { "ResourceId", vault.Id },
                    { "location", vault.Location }
                }
            };
            });
    }
 
    /// <summary>
    /// Adds a connection to a Microsoft Foundry project.
    /// </summary>
    [AspireExport("addConnection")]
    internal static IResourceBuilder<AzureCognitiveServicesProjectConnectionResource> AddConnectionForPolyglot(
        this IResourceBuilder<AzureCognitiveServicesProjectResource> builder,
        [AspireUnion(
            typeof(IResourceBuilder<AzureCosmosDBResource>),
            typeof(IResourceBuilder<AzureStorageResource>),
            typeof(IResourceBuilder<AzureContainerRegistryResource>),
            typeof(IResourceBuilder<AzureKeyVaultResource>))]
        object resource)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(resource);
 
        return resource switch
        {
            IResourceBuilder<AzureCosmosDBResource> cosmosDb => builder.AddConnection(cosmosDb),
            IResourceBuilder<AzureStorageResource> storage => builder.AddConnection(storage),
            IResourceBuilder<AzureContainerRegistryResource> registry => builder.AddConnection(registry),
            IResourceBuilder<AzureKeyVaultResource> keyVault => builder.AddConnection(keyVault),
            _ => throw new ArgumentException(
                "Resource must be a Cosmos DB, Storage, Container Registry, or Key Vault resource builder.",
                nameof(resource))
        };
    }
 
    /// <summary>
    /// Adds a Grounding with Bing Search connection to a Microsoft Foundry project.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The Bing Search resource (<c>Microsoft.Bing/accounts</c>) cannot be provisioned through Aspire
    /// or Bicep. It must be created manually in the
    /// <a href="https://portal.azure.com">Azure portal</a>.
    /// </para>
    /// <para>
    /// Once the Bing resource exists, pass its resource ID to this method. The connection is
    /// created in the Foundry project using API key authentication with
    /// <c>category: "ApiKey"</c> and <c>metadata.Type: "bing_grounding"</c>.
    /// </para>
    /// </remarks>
    /// <param name="builder">The <see cref="IResourceBuilder{T}"/> for the parent Microsoft Foundry project resource.</param>
    /// <param name="name">The name of the connection resource.</param>
    /// <param name="bingResourceId">
    /// The full Azure resource ID of the Bing Search resource
    /// (e.g., <c>/subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.Bing/accounts/{name}</c>).
    /// </param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/> for the connection resource.</returns>
    /// <ats-returns>The resource builder.</ats-returns>
    [AspireExport]
    public static IResourceBuilder<BingGroundingConnectionResource> AddBingGroundingConnection(
        this IResourceBuilder<AzureCognitiveServicesProjectResource> builder,
        [ResourceName] string name,
        string bingResourceId)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(name);
        ArgumentException.ThrowIfNullOrEmpty(bingResourceId);
 
        return builder.AddBingConnection(name, (infra) =>
        {
            return new BingGroundingConnectionProperties()
            {
                Target = "https://api.bing.microsoft.com/",
                UseWorkspaceManagedIdentity = false,
                IsSharedToAll = false,
                SharedUserList = [],
                PeRequirement = ManagedPERequirement.NotRequired,
                PeStatus = ManagedPEStatus.NotApplicable,
                CredentialsKey = (BicepValue<string>)new MemberExpression(
                    new FunctionCallExpression(
                        new IdentifierExpression("listKeys"),
                        new StringLiteralExpression(bingResourceId),
                        new StringLiteralExpression(BingAccountsResourceVersion)),
                    "key1"),
                Metadata =
                {
                    { "type", "bing_grounding" },
                    { "ApiType", "Azure" },
                    { "ResourceId", bingResourceId }
                }
            };
        });
    }
 
    /// <summary>
    /// Adds a Grounding with Bing Search connection to a Microsoft Foundry project using a
    /// parameter resource for the Bing resource ID.
    /// </summary>
    /// <remarks>
    /// <para>
    /// This overload allows the Bing resource ID to be supplied as a parameter (e.g., from user secrets
    /// or configuration) rather than a hardcoded string. The parameter value is resolved at deployment time
    /// and embedded in the Bicep template.
    /// </para>
    /// </remarks>
    /// <param name="builder">The <see cref="IResourceBuilder{T}"/> for the parent Microsoft Foundry project resource.</param>
    /// <param name="name">The name of the connection resource.</param>
    /// <param name="bingResourceId">
    /// A parameter resource containing the full Azure resource ID of the Bing Search resource.
    /// </param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/> for the connection resource.</returns>
    /// <ats-returns>The resource builder.</ats-returns>
    [AspireExport("addBingGroundingConnectionFromParameter")]
    public static IResourceBuilder<BingGroundingConnectionResource> AddBingGroundingConnection(
        this IResourceBuilder<AzureCognitiveServicesProjectResource> builder,
        [ResourceName] string name,
        IResourceBuilder<ParameterResource> bingResourceId)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(name);
        ArgumentNullException.ThrowIfNull(bingResourceId);
 
        return builder.AddBingConnection(name, (infra) =>
        {
            var resourceIdParam = bingResourceId.AsProvisioningParameter(infra);
            return new BingGroundingConnectionProperties()
            {
                Target = "https://api.bing.microsoft.com/",
                UseWorkspaceManagedIdentity = false,
                IsSharedToAll = false,
                SharedUserList = [],
                PeRequirement = ManagedPERequirement.NotRequired,
                PeStatus = ManagedPEStatus.NotApplicable,
                CredentialsKey = (BicepValue<string>)new MemberExpression(
                    new FunctionCallExpression(
                        new IdentifierExpression("listKeys"),
                        resourceIdParam.Value.Compile(),
                        new StringLiteralExpression(BingAccountsResourceVersion)),
                    "key1"),
                Metadata =
                {
                    { "type", "bing_grounding" },
                    { "ApiType", "Azure" },
                    { "ResourceId", resourceIdParam }
                }
            };
        });
    }
 
    private static IResourceBuilder<BingGroundingConnectionResource> AddBingConnection(
        this IResourceBuilder<AzureCognitiveServicesProjectResource> builder,
        string name,
        Func<AzureResourceInfrastructure, CognitiveServicesConnectionProperties> configureProperties)
    {
        void configureInfrastructure(AzureResourceInfrastructure infrastructure)
        {
            var aspireResource = (BingGroundingConnectionResource)infrastructure.AspireResource;
            var account = aspireResource.Parent.Parent.AddAsExistingResource(infrastructure);
 
            var connection = AzureProvisioningResource.CreateExistingOrNewProvisionableResource(
                infrastructure,
                (identifier, resourceName) =>
                {
                    var resource = aspireResource.FromExisting(identifier);
                    resource.Parent = account;
                    resource.Name = resourceName;
                    return resource;
                },
                infra =>
                {
                    var resource = new CognitiveServicesConnection(aspireResource.GetBicepIdentifier(), AzureCognitiveServicesProjectConnectionResource.ResourceVersion)
                    {
                        Parent = account,
                        Name = name,
                        Properties = configureProperties(infra)
                    };
                    return resource;
                });
            if (aspireResource.Parent.KeyVaultConn is not null)
            {
                var keyVaultConn = aspireResource.Parent.KeyVaultConn.AddAsExistingResource(infrastructure);
                connection.DependsOn.Add(keyVaultConn);
            }
            infrastructure.Add(new ProvisioningOutput("name", typeof(string)) { Value = connection.Name });
            infrastructure.Add(new ProvisioningOutput("id", typeof(string)) { Value = connection.Id });
        }
        var connectionResource = new BingGroundingConnectionResource(name, configureInfrastructure, builder.Resource);
        return builder.ApplicationBuilder.AddResource(connectionResource);
    }
}