diff --git a/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.AppHost/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.AppHost.csproj b/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.AppHost/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.AppHost.csproj
index 957385921..2e7412939 100644
--- a/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.AppHost/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.AppHost.csproj
+++ b/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.AppHost/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.AppHost.csproj
@@ -1,4 +1,4 @@
-
+
Exe
@@ -15,6 +15,7 @@
+
@@ -22,6 +23,9 @@
Always
+
+ Always
+
diff --git a/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.AppHost/Program.cs b/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.AppHost/Program.cs
index 09c57225e..68b4418ed 100644
--- a/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.AppHost/Program.cs
+++ b/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.AppHost/Program.cs
@@ -1,23 +1,33 @@
var builder = DistributedApplication.CreateBuilder(args);
-// Add a SQL Server container
-var sqlServer = builder
- .AddSqlServer("sql");
+var sqlScript = File.ReadAllText("./sql-server.sql");
-var sqlDatabase = sqlServer.AddDatabase("trek");
+var sqlDatabase = builder
+ .AddSqlServer("sql")
+ .WithDataVolume("trek-sql-data")
+ .AddDatabase("trek")
+ .WithCreationScript(sqlScript);
-// Populate the database with the schema and data
-sqlServer
- .WithBindMount("./sql-server", target: "/usr/config")
- .WithBindMount("../database", target: "/docker-entrypoint-initdb.d")
- .WithEntrypoint("/usr/config/entrypoint.sh");
+var dabConfig = new FileInfo("./dab-config.json");
-// Add Data API Builder using dab-config.json
var dab = builder.AddDataAPIBuilder("dab")
- .WaitFor(sqlServer)
+ .WithImageTag("1.7.86-rc")
+ .WithConfigFile(dabConfig)
+ .WaitFor(sqlDatabase)
.WithReference(sqlDatabase);
+var mcp = builder
+ .AddMcpInspector("mcp-inspector", options =>
+ {
+ options.InspectorVersion = "0.20.0";
+ })
+ .WithMcpServer(dab, transportType: McpTransportType.StreamableHttp)
+ .WithParentRelationship(dab)
+ .WithEnvironment("DANGEROUSLY_OMIT_AUTH", "true")
+ .WaitFor(dab);
+
builder.AddProject("blazorApp")
- .WithReference(dab);
+ .WithReference(dab)
+ .WaitFor(dab);
builder.Build().Run();
diff --git a/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.AppHost/dab-config.json b/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.AppHost/dab-config.json
index 69d8b687c..95b57589b 100644
--- a/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.AppHost/dab-config.json
+++ b/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.AppHost/dab-config.json
@@ -1,5 +1,5 @@
{
- "$schema": "https://github.com/Azure/data-api-builder/releases/download/v1.3.19/dab.draft.schema.json",
+ "$schema": "https://github.com/Azure/data-api-builder/releases/download/v1.7.86-rc/dab.draft.schema.json",
"data-source": {
"database-type": "mssql",
"connection-string": "@env('ConnectionStrings__trek')",
@@ -16,6 +16,9 @@
"path": "/graphql",
"allow-introspection": true
},
+ "mcp": {
+ "enabled": true
+ },
"host": {
"cors": {
"origins": [
diff --git a/examples/data-api-builder/database/create_database.sql b/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.AppHost/sql-server.sql
similarity index 100%
rename from examples/data-api-builder/database/create_database.sql
rename to examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.AppHost/sql-server.sql
diff --git a/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.AppHost/sql-server/configure-db.sh b/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.AppHost/sql-server/configure-db.sh
deleted file mode 100755
index ce7723793..000000000
--- a/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.AppHost/sql-server/configure-db.sh
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/bin/bash
-
-# set -x
-
-# Adapted from: https://github.com/microsoft/mssql-docker/blob/80e2a51d0eb1693f2de014fb26d4a414f5a5add5/linux/preview/examples/mssql-customize/configure-db.sh
-
-# Wait 60 seconds for SQL Server to start up by ensuring that
-# calling SQLCMD does not return an error code, which will ensure that sqlcmd is accessible
-# and that system and user databases return "0" which means all databases are in an "online" state
-# https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-databases-transact-sql?view=sql-server-2017
-
-dbstatus=1
-errcode=1
-start_time=$SECONDS
-end_by=$((start_time + 60))
-
-echo "Starting check for SQL Server start-up at $start_time, will end at $end_by"
-
-while [[ $SECONDS -lt $end_by && ( $errcode -ne 0 || ( -z "$dbstatus" || $dbstatus -ne 0 ) ) ]]; do
- dbstatus="$(/opt/mssql-tools18/bin/sqlcmd -h -1 -t 1 -U sa -P "$MSSQL_SA_PASSWORD" -C -Q "SET NOCOUNT ON; Select SUM(state) from sys.databases")"
- errcode=$?
- sleep 1
-done
-
-elapsed_time=$((SECONDS - start_time))
-echo "Stopped checking for SQL Server start-up after $elapsed_time seconds (dbstatus=$dbstatus,errcode=$errcode,seconds=$SECONDS)"
-
-if [[ $dbstatus -ne 0 ]] || [[ $errcode -ne 0 ]]; then
- echo "SQL Server took more than 60 seconds to start up or one or more databases are not in an ONLINE state"
- echo "dbstatus = $dbstatus"
- echo "errcode = $errcode"
- exit 1
-fi
-
-# Loop through the .sql files in the root of /docker-entrypoint-initdb.d and execute them with sqlcmd
-for f in $(find /docker-entrypoint-initdb.d -maxdepth 1 -type f -name "*.sql" | sort); do
- echo "- A -=- Processing $f file..."
- /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "$MSSQL_SA_PASSWORD" -C -d master -i "$f"
-done
-
-# Loop through each subdirectory in /docker-entrypoint-initdb.d
-for dir in $(find /docker-entrypoint-initdb.d -mindepth 1 -maxdepth 1 -type d | sort); do
- # Loop through the .sql files in each subdirectory and execute them with sqlcmd
- for f in $(find "$dir" -maxdepth 1 -type f -name "*.sql" | sort); do
- echo "- B -=- Processing $f file in directory $dir..."
- /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "$MSSQL_SA_PASSWORD" -C -d master -i "$f"
- done
-done
\ No newline at end of file
diff --git a/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.AppHost/sql-server/entrypoint.sh b/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.AppHost/sql-server/entrypoint.sh
deleted file mode 100755
index fda39d142..000000000
--- a/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.AppHost/sql-server/entrypoint.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/bin/bash
-
-# Start the script to create the DB and user
-/usr/config/configure-db.sh &
-
-# Start SQL Server
-/opt/mssql/bin/sqlservr
-
diff --git a/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.BlazorApp/Components/Pages/Home.razor b/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.BlazorApp/Components/Pages/Home.razor
index 292839671..50235678c 100644
--- a/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.BlazorApp/Components/Pages/Home.razor
+++ b/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.BlazorApp/Components/Pages/Home.razor
@@ -5,9 +5,7 @@
Home
-Aspire Data API Builder Integration
-
-Welcome to your demo app 🖖
+Data API Builder
@if (series is null)
{
@@ -15,7 +13,7 @@
}
else
{
- Here some StarTrek series
+ Here are some Star Trek series 🖖
@foreach (var s in series)
{
diff --git a/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.BlazorApp/GlobalSuppressions.cs b/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.BlazorApp/GlobalSuppressions.cs
new file mode 100644
index 000000000..a59ecde2b
--- /dev/null
+++ b/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.BlazorApp/GlobalSuppressions.cs
@@ -0,0 +1,8 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Performance", "CA1873:Avoid potentially expensive logging", Justification = "The additional allocation cost from logging in TrekApiClient.GetSeriesAsync is acceptable for this sample Blazor application and simplifies the example code.", Scope = "member", Target = "~M:CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.BlazorApp.TrekApiClient.GetSeriesAsync~System.Threading.Tasks.Task{System.Collections.Generic.List{CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.BlazorApp.Series}}")]
diff --git a/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.BlazorApp/TrekApiClient.cs b/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.BlazorApp/TrekApiClient.cs
index 6d75dabd4..cec3462c4 100644
--- a/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.BlazorApp/TrekApiClient.cs
+++ b/examples/data-api-builder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.BlazorApp/TrekApiClient.cs
@@ -1,45 +1,56 @@
-using System.Net.Http;
-using System.Text.Json.Serialization;
-using System.Text.Json;
-
+using System.Text.Json.Serialization;
namespace CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.BlazorApp;
-public class TrekApiClient
+public class TrekApiClient(HttpClient httpClient, ILogger logger)
{
- private readonly HttpClient httpClient;
- private readonly ILogger logger;
-
- public TrekApiClient(HttpClient httpClient, ILogger logger)
- {
- this.httpClient = httpClient;
- this.logger = logger;
- }
-
public async Task> GetSeriesAsync()
{
- try
+ var response = await httpClient.GetAsync($"api/series");
+ response.EnsureSuccessStatusCode();
+ var result = await response.Content.ReadFromJsonAsync>();
+
+ if(result is null)
{
- var result = await httpClient.GetFromJsonAsync($"api/series");
- return result?.value ?? new List();
+ logger.LogError("Failed to deserialize response from Data API Builder.");
+ throw new Exception("Failed to deserialize response from Data API Builder.");
}
- catch (Exception ex)
+
+ if (result.Error is not null)
{
- logger.LogError(ex, "An error occurred while fetching series.");
- return new List();
+ logger.LogError("API error: {Code} - {Message}", result.Error.Code, result.Error.Message);
+ throw new Exception($"{result.Error.Code}: {result.Error.Message}");
}
+
+ return result.Value ?? [];
}
}
-public class SeriesList
+public class DabResponse
{
- public List value { get; set; } = new List();
+ [JsonPropertyName("value")]
+ public List? Value { get; set; }
+
+ [JsonPropertyName("nextLink")]
+ public string? NextLink { get; set; }
+
+ [JsonPropertyName("error")]
+ public DabError? Error { get; set; }
+}
+
+public class DabError
+{
+ [JsonPropertyName("code")]
+ public string Code { get; set; } = string.Empty;
+
+ [JsonPropertyName("message")]
+ public string Message { get; set; } = string.Empty;
+
+ [JsonPropertyName("status")]
+ public int Status { get; set; }
}
public class Series
{
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
- public int Id { get; set; }
public required string Name { get; set; }
-
}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.csproj b/src/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.csproj
index f96e06307..cfb0ffbc8 100644
--- a/src/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.csproj
+++ b/src/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.csproj
@@ -10,7 +10,6 @@
-
\ No newline at end of file
diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder/DataApiBuilderContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder/DataApiBuilderContainerImageTags.cs
index 5f166f95d..a9fe443f0 100644
--- a/src/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder/DataApiBuilderContainerImageTags.cs
+++ b/src/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder/DataApiBuilderContainerImageTags.cs
@@ -1,8 +1,16 @@
namespace CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder;
+///
+/// Container image tags for the Data API Builder container.
+///
internal static class DataApiBuilderContainerImageTags
{
+ /// The container registry.
public const string Registry = "mcr.microsoft.com";
+
+ /// The container image name.
public const string Image = "azure-databases/data-api-builder";
- public const string Tag = "1.6.77";
+
+ /// The default container image tag.
+ public const string Tag = "1.6.87";
}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder/DataApiBuilderContainerResource.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder/DataApiBuilderContainerResource.cs
index 14c912e15..f05f54852 100644
--- a/src/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder/DataApiBuilderContainerResource.cs
+++ b/src/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder/DataApiBuilderContainerResource.cs
@@ -1,17 +1,21 @@
namespace Aspire.Hosting.ApplicationModel;
///
-/// A resource that represents Data Api Builder.
+/// A resource that represents Data API Builder.
///
/// The name of the resource.
/// An optional container entrypoint.
-
-public class DataApiBuilderContainerResource(string name, string? entrypoint = null)
+public sealed class DataApiBuilderContainerResource(string name, string? entrypoint = null)
: ContainerResource(name, entrypoint), IResourceWithServiceDiscovery
{
internal const string HttpEndpointName = "http";
- internal const string HttpsEndpointName = "https";
internal const int HttpEndpointPort = 5000;
- internal const int HttpsEndpointPort = 5001;
+
+ private EndpointReference? _primaryEndpoint;
+
+ ///
+ /// Gets the primary HTTP endpoint for the Data API Builder container.
+ ///
+ public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, HttpEndpointName);
}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder/DataApiBuilderHostingExtension.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder/DataApiBuilderHostingExtension.cs
index 461fea0b4..8fa33b308 100644
--- a/src/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder/DataApiBuilderHostingExtension.cs
+++ b/src/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder/DataApiBuilderHostingExtension.cs
@@ -1,25 +1,19 @@
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Diagnostics.HealthChecks;
using Aspire.Hosting.ApplicationModel;
using CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder;
namespace Aspire.Hosting;
///
-/// Provides extension methods for adding DataApiBuilder api to an .
+/// Provides extension methods for adding Data API Builder to an .
///
public static class DataApiBuilderHostingExtension
{
///
- /// Adds a DataAPIBuilder application to the application model. Executes the pre-built containerized DataAPIBuilder engine.
+ /// Adds a Data API Builder application to the application model. Executes the pre-built containerized Data API Builder engine.
///
/// The to add the resource to.
/// The name of the resource.
- /// The path to the config or schema file(s) for Data API Builder."
- ///
- /// At this time, this Aspire DAB integration only supports HTTPS ports.
- /// You can deploy DAB with HTTPS and custom certs in production.
- ///
+ /// The path to the config or schema file(s) for Data API Builder.
/// A reference to the .
public static IResourceBuilder AddDataAPIBuilder(this IDistributedApplicationBuilder builder,
[ResourceName] string name,
@@ -29,27 +23,25 @@ public static IResourceBuilder AddDataAPIBuilde
}
///
- /// Adds a DataAPIBuilder application to the application model. Executes the pre-built containerized DataAPIBuilder engine.
+ /// Adds a Data API Builder application to the application model. Executes the pre-built containerized Data API Builder engine.
///
/// The to add the resource to.
/// The name of the resource.
- /// The HTTP port number for the Data API Builder container."
- /// The path to the config or schema file(s) for Data API Builder."
- ///
- /// At this time, this Aspire DAB integration only supports HTTPS ports.
- /// You can deploy DAB with HTTPS and custom certs in production.
- ///
+ /// The HTTP port number for the Data API Builder container.
+ /// The path to the config or schema file(s) for Data API Builder.
/// A reference to the .
public static IResourceBuilder AddDataAPIBuilder(this IDistributedApplicationBuilder builder,
[ResourceName] string name,
int? httpPort = null,
params string[] configFilePaths)
{
- ArgumentNullException.ThrowIfNull("Service name must be specified.", nameof(name));
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentException.ThrowIfNullOrEmpty(name);
var resource = new DataApiBuilderContainerResource(name);
var rb = builder.AddResource(resource)
+ .WithIconName("Drag")
.WithImage(DataApiBuilderContainerImageTags.Image)
.WithImageTag(DataApiBuilderContainerImageTags.Tag)
.WithImageRegistry(DataApiBuilderContainerImageTags.Registry)
@@ -58,12 +50,6 @@ public static IResourceBuilder AddDataAPIBuilde
name: DataApiBuilderContainerResource.HttpEndpointName)
.WithDataApiBuilderDefaults();
- // Use default config file path if no paths are provided
- if (configFilePaths is [])
- {
- configFilePaths = ["./dab-config.json"];
- }
-
foreach (var configFilePath in configFilePaths)
{
var configFileName = File.Exists(configFilePath)
@@ -76,8 +62,124 @@ public static IResourceBuilder AddDataAPIBuilde
return rb;
}
+ ///
+ /// Adds one or more Data API Builder configuration files to the container as read-only bind mounts.
+ /// This method can be called multiple times to add additional files.
+ ///
+ /// The to configure.
+ /// One or more objects pointing to config or schema files.
+ /// A reference to the for chaining.
+ /// Thrown when or is .
+ /// Thrown when a specified file does not exist.
+ /// Thrown when a file with the same name is already mounted.
+ public static IResourceBuilder WithConfigFile(
+ this IResourceBuilder builder,
+ params FileInfo[] configFiles)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(configFiles);
+
+ foreach (var file in configFiles)
+ {
+ ArgumentNullException.ThrowIfNull(file);
+
+ if (!file.Exists)
+ {
+ throw new FileNotFoundException($"Config file not found: {file.FullName}");
+ }
+
+ string targetPath = $"/App/{file.Name}";
+ ThrowIfMountTargetExists(builder, targetPath, file.FullName);
+
+ builder.WithBindMount(file.FullName, targetPath, isReadOnly: true);
+ }
+
+ return builder;
+ }
+
+ ///
+ /// Adds all files from one or more directories to the container as individual read-only bind mounts.
+ /// Each file in the directory is mounted individually (not as a folder mount). Only top-level files are included.
+ /// This method can be called multiple times to add files from additional directories.
+ ///
+ /// The to configure.
+ /// One or more objects pointing to directories containing config files.
+ /// A reference to the for chaining.
+ /// Thrown when or is .
+ /// Thrown when a specified directory does not exist.
+ /// Thrown when a file with the same name is already mounted.
+ public static IResourceBuilder WithConfigFolder(
+ this IResourceBuilder builder,
+ params DirectoryInfo[] configFolders)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(configFolders);
+
+ foreach (var folder in configFolders)
+ {
+ ArgumentNullException.ThrowIfNull(folder);
+
+ if (!folder.Exists)
+ {
+ throw new DirectoryNotFoundException($"Config directory not found: {folder.FullName}");
+ }
+
+ foreach (var file in folder.GetFiles())
+ {
+ string targetPath = $"/App/{file.Name}";
+ ThrowIfMountTargetExists(builder, targetPath, file.FullName);
+
+ builder.WithBindMount(file.FullName, targetPath, isReadOnly: true);
+ }
+ }
+
+ return builder;
+ }
+
+ private static void ThrowIfMountTargetExists(
+ IResourceBuilder builder,
+ string targetPath,
+ string sourceDescription)
+ {
+ if (builder.Resource.TryGetAnnotationsOfType(out var existingMounts))
+ {
+ foreach (var mount in existingMounts)
+ {
+ if (string.Equals(mount.Target, targetPath, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new InvalidOperationException(
+ $"A config file is already mounted to '{targetPath}'. " +
+ $"The file '{sourceDescription}' conflicts with an existing mount. " +
+ $"Each config file must have a unique filename.");
+ }
+ }
+ }
+ }
+
private static IResourceBuilder WithDataApiBuilderDefaults(
this IResourceBuilder builder) =>
builder.WithOtlpExporter()
- .WithHttpHealthCheck("/health");
+ .WithHttpHealthCheck("/health")
+ .WithUrls(context =>
+ {
+ context.Urls.Clear();
+ context.Urls.Add(new()
+ {
+ Url = "/swagger",
+ DisplayText = "Swagger",
+ Endpoint = context.GetEndpoint(DataApiBuilderContainerResource.HttpEndpointName)
+ });
+ context.Urls.Add(new()
+ {
+ Url = "/graphql",
+ DisplayText = "GraphQL",
+ Endpoint = context.GetEndpoint(DataApiBuilderContainerResource.HttpEndpointName)
+ });
+ context.Urls.Add(new()
+ {
+ Url = "/health",
+ DisplayText = "Health",
+ Endpoint = context.GetEndpoint(DataApiBuilderContainerResource.HttpEndpointName)
+ });
+ });
}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder/README.md b/src/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder/README.md
index 9df60a084..45c69d75b 100644
--- a/src/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder/README.md
+++ b/src/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder/README.md
@@ -1,13 +1,15 @@
-# CommunityToolkit.Hosting.Azure.DataApiBuilder
+# CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder
## Overview
-This Aspire Integration runs [Data API builder](https://aka.ms/dab/docs) in a container. Data API builder generates secure, feature-rich REST and GraphQL endpoints for Tables, Views and Stored Procedures performing CRUD (Create, Read, Update, Delete, Execute) operations against Azure SQL Database, SQL Server, PostgreSQL, MySQL and Azure CosmosDB.
+This Aspire Integration runs [Data API builder](https://aka.ms/dab/docs) in a container. Data API builder generates secure, feature-rich REST and GraphQL endpoints for Tables, Views and Stored Procedures performing CRUD (Create, Read, Update, Delete, Execute) operations against Azure SQL Database, SQL Server, PostgreSQL, MySQL and Azure CosmosDB.
## Usage
### Example 1: Single data source
+The docs for a basic configuration file are at [MS Learn](https://learn.microsoft.com/en-us/azure/data-api-builder/configuration/).
+
```csharp
var builder = DistributedApplication.CreateBuilder(args);
@@ -15,7 +17,10 @@ var sqlDatabase = builder
.AddSqlServer("your-server-name")
.AddDatabase("your-database-name");
+var dabConfig = new FileInfo("./dab-config.json");
+
var dab = builder.AddDataAPIBuilder("dab")
+ .WithConfigFile(dabConfig)
.WithReference(sqlDatabase)
.WaitFor(sqlDatabase);
@@ -28,6 +33,8 @@ builder.Build().Run();
### Example 2: Multiple data sources
+The docs for multi-source configuration are at [MS Learn](https://learn.microsoft.com/en-us/azure/data-api-builder/concept/config/multi-data-source).
+
```csharp
var builder = DistributedApplication.CreateBuilder(args);
@@ -39,9 +46,11 @@ var sqlDatabase2 = builder
.AddSqlServer("your-server-name")
.AddDatabase("your-database-name");
-var dab = builder.AddDataAPIBuilder("dab",
- "./dab-config-1.json",
- "./dab-config-2.json")
+var dabConfig1 = new FileInfo("./dab-config-1.json");
+var dabConfig2 = new FileInfo("./dab-config-2.json");
+
+var dab = builder.AddDataAPIBuilder("dab")
+ .WithConfigFile(dabConfig1, dabConfig2)
.WithReference(sqlDatabase1)
.WithReference(sqlDatabase2)
.WaitFor(sqlDatabase1)
@@ -54,7 +63,7 @@ var app = builder
builder.Build().Run();
```
-> Note: All files are mounted/copied to the same `/App` folder.
+> Note: All files are mounted/copied to the same `/App` folder. Each config file must have a unique filename. If a duplicate filename is detected, a friendly `InvalidOperationException` is thrown.
### Example 3: Cosmos DB and a schema file
@@ -65,9 +74,11 @@ var cosmosdb = builder
.AddAzureCosmosDB("myNewCosmosAccountName")
.AddDatabase("myCosmosDatabaseName");
-var dab = builder.AddDataAPIBuilder("dab",
- "./dab-config.json",
- "./schema.graphql")
+var dabConfig = new FileInfo("./dab-config.json");
+var dabSchema = new FileInfo("./schema.graphql");
+
+var dab = builder.AddDataAPIBuilder("dab")
+ .WithConfigFile(dabConfig, dabSchema)
.WithReference(cosmosdb)
.WaitFor(cosmosdb);
@@ -80,6 +91,8 @@ builder.Build().Run();
### Example 4: Connection string-only
+Sometimes your SQL Server is installed locally or part of your development environment and doesn't need to be created by Aspire. In these cases, you can use the `AddConnectionString` method. This also works for any data source type supported.
+
```csharp
var builder = DistributedApplication.CreateBuilder(args);
@@ -87,6 +100,7 @@ var sqlDatabase = builder
.AddConnectionString("your-cs-name");
var dab = builder.AddDataAPIBuilder("dab")
+ .WithConfigFile(new FileInfo("./dab-config.json"))
.WithReference(sqlDatabase);
var app = builder
@@ -96,28 +110,93 @@ var app = builder
builder.Build().Run();
```
-### Configuration
+### Example 5: Custom image tag
+
+In some cases, including those times when you want to use a release candidate (RC) or a private build of the Data API builder container image, you may want to specify a custom image or image tag. For a custom image tag, you can do this with the `WithImageTag` method, a standard method in Aspire.
+
+```csharp
+var builder = DistributedApplication.CreateBuilder(args);
+
+var sqlDatabase = builder
+ .AddConnectionString("your-cs-name");
+
+var dab = builder.AddDataAPIBuilder("dab")
+ .WithConfigFile(new FileInfo("./dab-config.json"))
+ .WithReference(sqlDatabase)
+ .WithImageTag("1.7.86-rc"); // specify a custom image tag
+
+var app = builder
+ .AddProject()
+ .WithReference(dab);
+
+builder.Build().Run();
+```
+
-- `name` - The name of the resource.
-- `port` - The optional port number for the Data API builder container. Defaults to `random`.
-- `configFilePaths` - Opiotnal paths to the config/schema file(s) for Data API builder. Default is `./dab-config.json`.
+### Example 6: Testing with MCP Inspector
-### Data API builder Container Image Configuration
+Because version 1.7 and later of Data API builder has MCP capability for agentic applications, you can test and debug your instance with the MCP Inspector which is available through Aspire's CommunityToolkit.Aspire.Hosting.McpInspector version 13.1.1 or later.
-You can specify custom registry/image/tag values by using the `WithImageRegistry`/`WithImage`/`WithImageTag` methods:
```csharp
+var builder = DistributedApplication.CreateBuilder(args);
+
+var sqlDatabase = builder
+ .AddConnectionString("your-cs-name");
+
var dab = builder.AddDataAPIBuilder("dab")
- .WithImageRegistry("mcr.microsoft.com")
- .WithImage("azure-databases/data-api-builder")
- .WithImageTag("latest");
+ .WithConfigFile(new FileInfo("./dab-config.json"))
+ .WithReference(sqlDatabase)
+ .WithImageTag("1.7.86-rc"); // specify a custom image tag
+
+var mcp = builder
+ .AddMcpInspector("mcp-inspector", options =>
+ {
+ options.InspectorVersion = "0.20.0";
+ })
+ .WithMcpServer(dab, transportType: McpTransportType.StreamableHttp)
+ .WithParentRelationship(dab)
+ .WithEnvironment("DANGEROUSLY_OMIT_AUTH", "true")
+ .WaitFor(dab);
+
+var app = builder
+ .AddProject()
+ .WithReference(dab);
+
+builder.Build().Run();
```
-### OpenTelemetry Instrumentation
+## Toolkit Documentation
+
+The following methods are available for configuring the Data API builder container in Aspire:
+
+| Method | Parameter | Description |
+|-|-|-|
+|AddDataAPIBuilder() || Adds a Data API builder container to the application.|
+|| string name | The name of the resource. |
+|| int httpPort | Optional HTTP port number for the Data API builder container. Defaults to a random port. |
+| WithConfigFile() || Adds one or more config or schema files to the container. |
+|| FileInfo[] files | The config or schema file(s) to add. |
+| WithConfigFolder() || Adds all files from the specified folder(s) to the container. |
+|| DirectoryInfo[] folders | The folder(s) from which to add all top-level config or schema files. |
+
+### Health Checks
-The Data API builder integration automatically configures OpenTelemetry (OTEL) instrumentation for distributed tracing and metrics. The integration uses the standard `.WithOtlpExporter()` method which sets up the necessary OTEL environment variables that Data API builder automatically recognizes.
+If your Data API builder configuration requires authentication (e.g., EasyAuth, JWT, or any provider other than `Simulator`), the `/health` endpoint may return a non-200 status even when the service is otherwise healthy. In development, consider using the `Simulator` authentication provider in your `dab-config.json` to avoid health check failures:
+>
+> ```json
+> "authentication": {
+> "provider": "Simulator"
+> }
+> ```
-To enable OTEL telemetry in Data API builder, add the following configuration to your `dab-config.json` file:
+For more information about Data API builder health checks, see the [official documentation](https://learn.microsoft.com/azure/data-api-builder/concept/monitor/health-checks).
+
+For more information about Data API builder's Simulator authentication provider, see the [official documentation](https://learn.microsoft.com/en-us/azure/data-api-builder/concept/security/how-to-authenticate-simulator).
+
+### OpenTelemetry Instrumentation
+
+Aspire automatically injects `OTEL_EXPORTER_OTLP_ENDPOINT` and `OTEL_SERVICE_NAME` into the Data API builder container via `.WithOtlpExporter()`. To enable OTEL telemetry in Data API builder, add the following configuration to your `dab-config.json` file:
```json
{
@@ -127,28 +206,19 @@ To enable OTEL telemetry in Data API builder, add the following configuration to
"enabled": true,
"service-name": "@env('OTEL_SERVICE_NAME')",
"endpoint": "@env('OTEL_EXPORTER_OTLP_ENDPOINT')",
- "exporter-protocol": "grpc",
- "headers": "@env('OTEL_EXPORTER_OTLP_HEADERS')"
+ "exporter-protocol": "grpc"
}
}
}
}
```
-The configuration includes the following settings:
-- `enabled`: Enables/disables OTEL telemetry (default: `false`)
-- `service-name`: Logical name for the service in traces. Uses the `@env('OTEL_SERVICE_NAME')` syntax to reference the environment variable automatically set by Aspire
-- `endpoint`: OTEL collector endpoint URL. Uses `@env('OTEL_EXPORTER_OTLP_ENDPOINT')` to reference the Aspire-provided endpoint
-- `exporter-protocol`: Protocol for exporting telemetry. Set to `grpc` for efficient binary transport
-- `headers`: Custom headers for OTEL export. Uses `@env('OTEL_EXPORTER_OTLP_HEADERS')` to reference Aspire-provided headers
-
-With this configuration, Data API builder will:
-- Export traces and metrics to the Aspire dashboard via OTLP (OpenTelemetry Protocol)
-- Automatically use the OTEL endpoint provided by the Aspire app host
-- Include telemetry for REST and GraphQL operations, database queries, and system metrics
+> **Warning:** Do **not** add `"headers": "@env('OTEL_EXPORTER_OTLP_HEADERS')"` unless your OTLP endpoint requires authentication (e.g., a cloud APM service). Aspire does not inject `OTEL_EXPORTER_OTLP_HEADERS`, and DAB requires all `@env()` references to resolve — an unset variable causes a fatal deserialization crash loop. If you need headers, set the environment variable explicitly on the container:
+>
+> ```csharp
+> builder.AddDataAPIBuilder("dab")
+> .WithEnvironment("OTEL_EXPORTER_OTLP_HEADERS", "api-key=your-key");
+> ```
For more information about Data API builder telemetry, see the [official documentation](https://learn.microsoft.com/azure/data-api-builder/concept/monitor/open-telemetry).
-## Known Issues
-
-The current implementation of the Data API builder Aspire integration does not support HTTPS endpoints. However, this is only a dev-time consideration. Service discovery when published can use HTTPS without any problems.
diff --git a/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/AppHostTests.cs
index 8b1b0d52a..d0b5b427a 100644
--- a/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/AppHostTests.cs
+++ b/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/AppHostTests.cs
@@ -29,8 +29,8 @@ public async Task CanGetSeries()
var response = await httpClient.GetAsync("/api/series");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- var series = await response.Content.ReadFromJsonAsync();
+ var series = await response.Content.ReadFromJsonAsync>();
Assert.NotNull(series);
- Assert.Equal(5, series.value.Count);
+ Assert.Equal(5, series.Value!.Count);
}
}
\ No newline at end of file
diff --git a/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests.csproj
index 04dde9f22..c03d30f4c 100644
--- a/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests.csproj
+++ b/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests.csproj
@@ -12,10 +12,19 @@
-
+
Always
-
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
Always
diff --git a/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/ContainerResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/ContainerResourceCreationTests.cs
index 61c57534f..71c8ba1f2 100644
--- a/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/ContainerResourceCreationTests.cs
+++ b/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/ContainerResourceCreationTests.cs
@@ -8,7 +8,7 @@ public void AddDataAPIBuilderBuilderShouldNotBeNull()
{
IDistributedApplicationBuilder builder = null!;
- Assert.Throws(() => builder.AddDataAPIBuilder("dab"));
+ Assert.Throws(() => builder.AddDataAPIBuilder("dab"));
}
[Fact]
@@ -35,7 +35,10 @@ public void AddDataAPIBuilderContainerDetailsSetOnResource()
Assert.NotNull(resource);
Assert.Equal("dab", resource.Name);
- Assert.True(resource.TryGetLastAnnotation(out ContainerImageAnnotation? imageAnnotations));
+ Assert.True(resource.TryGetLastAnnotation(out ContainerImageAnnotation? imageAnnotation));
+ Assert.Equal(DataApiBuilderContainerImageTags.Registry, imageAnnotation.Registry);
+ Assert.Equal(DataApiBuilderContainerImageTags.Image, imageAnnotation.Image);
+ Assert.Equal(DataApiBuilderContainerImageTags.Tag, imageAnnotation.Tag);
// verify ports
@@ -43,17 +46,14 @@ public void AddDataAPIBuilderContainerDetailsSetOnResource()
var http = endpoints.Where(x => x.Name == DataApiBuilderContainerResource.HttpEndpointName).Single();
Assert.Equal(DataApiBuilderContainerResource.HttpEndpointPort, http.TargetPort);
-
- // var https = endpoints.Where(x => x.Name == DataApiBuilderContainerResource.HttpsEndpointName).Single();
- // Assert.Equal(DataApiBuilderContainerResource.HttpsEndpointPort, https.TargetPort);
}
[Fact]
- public void AddDataAPIBuilderContainer_DefaultFile_NoEx()
+ public void AddDataAPIBuilderContainer_NoConfigPaths_NoMounts()
{
IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
- // defaults to ./dab-config.json which exists in this test project root
+ // no config paths specified, no auto-default mount
builder.AddDataAPIBuilder("dab");
using var app = builder.Build();
@@ -62,11 +62,7 @@ public void AddDataAPIBuilderContainer_DefaultFile_NoEx()
var resource = Assert.Single(appModel.Resources.OfType());
- Assert.True(resource.TryGetAnnotationsOfType(out var configFileAnnotations));
-
- var annotation = Assert.Single(configFileAnnotations);
- Assert.EndsWith("dab-config.json", annotation.Source);
- Assert.Equal("/App/dab-config.json", annotation.Target);
+ Assert.False(resource.TryGetAnnotationsOfType(out _));
}
[Fact]
@@ -95,7 +91,7 @@ public void AddDataAPIBuilderContainer_ValidFile_NoEx()
IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
// file exists in test project root
- builder.AddDataAPIBuilder("dab", configFilePaths: "./dab-config.json");
+ builder.AddDataAPIBuilder("dab", configFilePaths: "./dab-config-anonymous.json");
using var app = builder.Build();
@@ -106,8 +102,8 @@ public void AddDataAPIBuilderContainer_ValidFile_NoEx()
Assert.True(resource.TryGetAnnotationsOfType(out var configFileAnnotations));
var annotation = Assert.Single(configFileAnnotations);
- Assert.EndsWith("dab-config.json", annotation.Source);
- Assert.Equal("/App/dab-config.json", annotation.Target);
+ Assert.EndsWith("dab-config-anonymous.json", annotation.Source);
+ Assert.Equal("/App/dab-config-anonymous.json", annotation.Target);
}
[Fact]
@@ -116,7 +112,7 @@ public void AddDataAPIBuilderContainer_ValidFileWithPort_NoEx()
IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
// file exists in test project root
- builder.AddDataAPIBuilder("dab", httpPort: 1234, configFilePaths: "./dab-config.json");
+ builder.AddDataAPIBuilder("dab", httpPort: 1234, configFilePaths: "./dab-config-anonymous.json");
using var app = builder.Build();
@@ -133,8 +129,8 @@ public void AddDataAPIBuilderContainer_ValidFileWithPort_NoEx()
Assert.True(resource.TryGetAnnotationsOfType(out var configFileAnnotations));
var configAnnotation = Assert.Single(configFileAnnotations);
- Assert.EndsWith("dab-config.json", configAnnotation.Source);
- Assert.Equal("/App/dab-config.json", configAnnotation.Target);
+ Assert.EndsWith("dab-config-anonymous.json", configAnnotation.Source);
+ Assert.Equal("/App/dab-config-anonymous.json", configAnnotation.Target);
}
[Fact]
@@ -152,7 +148,7 @@ public void AddDataAPIBuilderContainer_ValidFiles_NoEx()
IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
// both files exist in test project root
- builder.AddDataAPIBuilder("dab", "./dab-config.json", "./dab-config-2.json");
+ builder.AddDataAPIBuilder("dab", "./dab-config-anonymous.json", "./dab-config-anonymous-2.json");
using var app = builder.Build();
@@ -167,13 +163,13 @@ public void AddDataAPIBuilderContainer_ValidFiles_NoEx()
configFileAnnotations,
a =>
{
- Assert.EndsWith("dab-config.json", a.Source);
- Assert.Equal("/App/dab-config.json", a.Target);
+ Assert.EndsWith("dab-config-anonymous.json", a.Source);
+ Assert.Equal("/App/dab-config-anonymous.json", a.Target);
},
a =>
{
- Assert.EndsWith("dab-config-2.json", a.Source);
- Assert.Equal("/App/dab-config-2.json", a.Target);
+ Assert.EndsWith("dab-config-anonymous-2.json", a.Source);
+ Assert.Equal("/App/dab-config-anonymous-2.json", a.Target);
});
}
@@ -183,6 +179,518 @@ public void AddDataAPIBuilderContainer_InvalidFiles_NoEx()
IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
// some (not all) files exist in test project root
- Assert.Throws(() => builder.AddDataAPIBuilder("dab", "./dab-config.json", "./dab-config-2.json", Guid.NewGuid().ToString()));
+ Assert.Throws(() => builder.AddDataAPIBuilder("dab", "./dab-config-anonymous.json", "./dab-config-anonymous-2.json", Guid.NewGuid().ToString()));
+ }
+
+ [Fact]
+ public void AddDataAPIBuilderContainer_HasHealthCheckAnnotation()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ builder.AddDataAPIBuilder("dab");
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var resource = Assert.Single(appModel.Resources.OfType());
+
+ Assert.True(resource.TryGetAnnotationsOfType(out var healthCheckAnnotations));
+ Assert.NotEmpty(healthCheckAnnotations);
+ }
+
+ [Fact]
+ public void AddDataAPIBuilderContainer_PrimaryEndpointIsAccessible()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ var dab = builder.AddDataAPIBuilder("dab");
+
+ Assert.NotNull(dab.Resource.PrimaryEndpoint);
+ Assert.Equal(DataApiBuilderContainerResource.HttpEndpointName, dab.Resource.PrimaryEndpoint.EndpointName);
+ }
+
+ [Fact]
+ public void AddDataAPIBuilderContainer_WithImageTag_OverridesDefault()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ builder.AddDataAPIBuilder("dab")
+ .WithImageTag("custom-tag");
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+ var resource = Assert.Single(appModel.Resources.OfType());
+
+ var imageAnnotation = resource.Annotations.OfType().Single();
+ Assert.Equal("custom-tag", imageAnnotation.Tag);
+ Assert.Equal(DataApiBuilderContainerImageTags.Image, imageAnnotation.Image);
+ Assert.Equal(DataApiBuilderContainerImageTags.Registry, imageAnnotation.Registry);
+ }
+
+ [Fact]
+ public void AddDataAPIBuilderContainer_WithImage_OverridesDefault()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ builder.AddDataAPIBuilder("dab")
+ .WithImage("custom-image");
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+ var resource = Assert.Single(appModel.Resources.OfType());
+
+ var imageAnnotation = resource.Annotations.OfType().Single();
+ Assert.Equal("custom-image", imageAnnotation.Image);
+ }
+
+ [Fact]
+ public void AddDataAPIBuilderContainer_WithImageRegistry_OverridesDefault()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ builder.AddDataAPIBuilder("dab")
+ .WithImageRegistry("custom.registry.io");
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+ var resource = Assert.Single(appModel.Resources.OfType());
+
+ var imageAnnotation = resource.Annotations.OfType().Single();
+ Assert.Equal("custom.registry.io", imageAnnotation.Registry);
+ Assert.Equal(DataApiBuilderContainerImageTags.Image, imageAnnotation.Image);
+ Assert.Equal(DataApiBuilderContainerImageTags.Tag, imageAnnotation.Tag);
+ }
+
+ [Fact]
+ public void AddDataAPIBuilderContainer_WithAllImageOverrides()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ builder.AddDataAPIBuilder("dab")
+ .WithImageRegistry("custom.registry.io")
+ .WithImage("custom-image")
+ .WithImageTag("custom-tag");
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+ var resource = Assert.Single(appModel.Resources.OfType());
+
+ var imageAnnotation = resource.Annotations.OfType().Single();
+ Assert.Equal("custom.registry.io", imageAnnotation.Registry);
+ Assert.Equal("custom-image", imageAnnotation.Image);
+ Assert.Equal("custom-tag", imageAnnotation.Tag);
+ }
+
+ [Fact]
+ public void AddDataAPIBuilderContainer_BindMountsAreReadOnly()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ builder.AddDataAPIBuilder("dab", configFilePaths: "./dab-config-anonymous.json");
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+ var resource = Assert.Single(appModel.Resources.OfType());
+
+ Assert.True(resource.TryGetAnnotationsOfType(out var mountAnnotations));
+ var mount = Assert.Single(mountAnnotations);
+ Assert.True(mount.IsReadOnly);
+ }
+
+ [Fact]
+ public void AddDataAPIBuilderContainer_DefaultPortIsNotFixed()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ builder.AddDataAPIBuilder("dab");
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+ var resource = Assert.Single(appModel.Resources.OfType());
+
+ Assert.True(resource.TryGetAnnotationsOfType(out var endpoints));
+ var http = endpoints.Single(x => x.Name == DataApiBuilderContainerResource.HttpEndpointName);
+ Assert.Null(http.Port);
+ Assert.Equal(DataApiBuilderContainerResource.HttpEndpointPort, http.TargetPort);
+ }
+
+ [Fact]
+ public void AddDataAPIBuilderContainer_ImplementsServiceDiscovery()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ var dab = builder.AddDataAPIBuilder("dab");
+
+ Assert.IsAssignableFrom(dab.Resource);
+ }
+
+ [Fact]
+ public void AddDataAPIBuilderContainer_AuthenticatedConfig_NoEx()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ builder.AddDataAPIBuilder("dab", configFilePaths: "./dab-config-authenticated.json");
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var resource = Assert.Single(appModel.Resources.OfType());
+
+ Assert.True(resource.TryGetAnnotationsOfType(out var configFileAnnotations));
+
+ var annotation = Assert.Single(configFileAnnotations);
+ Assert.EndsWith("dab-config-authenticated.json", annotation.Source);
+ Assert.Equal("/App/dab-config-authenticated.json", annotation.Target);
+ }
+
+ [Fact]
+ public void AddDataAPIBuilderContainer_AuthenticatedAndAnonymousConfigs_NoEx()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ builder.AddDataAPIBuilder("dab", "./dab-config-anonymous.json", "./dab-config-authenticated.json");
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var resource = Assert.Single(appModel.Resources.OfType());
+
+ Assert.True(resource.TryGetAnnotationsOfType(out var configFileAnnotations));
+
+ Assert.Equal(2, configFileAnnotations.Count());
+ Assert.Collection(
+ configFileAnnotations,
+ a =>
+ {
+ Assert.EndsWith("dab-config-anonymous.json", a.Source);
+ Assert.Equal("/App/dab-config-anonymous.json", a.Target);
+ },
+ a =>
+ {
+ Assert.EndsWith("dab-config-authenticated.json", a.Source);
+ Assert.Equal("/App/dab-config-authenticated.json", a.Target);
+ });
+ }
+
+ [Fact]
+ public void WithConfigFile_SingleFile_NoDefault_MountsCorrectly()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ // Use configFilePaths to skip the default, then use WithConfigFile
+ builder.AddDataAPIBuilder("dab", configFilePaths: "./dab-config-anonymous-2.json")
+ .WithConfigFile(new FileInfo("./dab-config-authenticated.json"));
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+ var resource = Assert.Single(appModel.Resources.OfType());
+
+ Assert.True(resource.TryGetAnnotationsOfType(out var mounts));
+ Assert.Equal(2, mounts.Count());
+ Assert.Contains(mounts, m => m.Target == "/App/dab-config-anonymous-2.json");
+ Assert.Contains(mounts, m => m.Target == "/App/dab-config-authenticated.json");
+ }
+
+ [Fact]
+ public void WithConfigFile_MultipleFiles_MountsAll()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ builder.AddDataAPIBuilder("dab", configFilePaths: "./dab-config-anonymous.json")
+ .WithConfigFile(
+ new FileInfo("./dab-config-anonymous-2.json"),
+ new FileInfo("./dab-config-authenticated.json"));
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+ var resource = Assert.Single(appModel.Resources.OfType());
+
+ Assert.True(resource.TryGetAnnotationsOfType(out var mounts));
+ Assert.Equal(3, mounts.Count());
+ Assert.Contains(mounts, m => m.Target == "/App/dab-config-anonymous.json");
+ Assert.Contains(mounts, m => m.Target == "/App/dab-config-anonymous-2.json");
+ Assert.Contains(mounts, m => m.Target == "/App/dab-config-authenticated.json");
+ }
+
+ [Fact]
+ public void WithConfigFile_CalledMultipleTimes_IsAdditive()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ builder.AddDataAPIBuilder("dab", configFilePaths: "./dab-config-anonymous.json")
+ .WithConfigFile(new FileInfo("./dab-config-anonymous-2.json"))
+ .WithConfigFile(new FileInfo("./dab-config-authenticated.json"));
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+ var resource = Assert.Single(appModel.Resources.OfType());
+
+ Assert.True(resource.TryGetAnnotationsOfType(out var mounts));
+ Assert.Equal(3, mounts.Count());
+ }
+
+ [Fact]
+ public void WithConfigFile_MountsAreReadOnly()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ builder.AddDataAPIBuilder("dab", configFilePaths: "./dab-config-anonymous.json")
+ .WithConfigFile(new FileInfo("./dab-config-anonymous-2.json"));
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+ var resource = Assert.Single(appModel.Resources.OfType());
+
+ Assert.True(resource.TryGetAnnotationsOfType(out var mounts));
+ Assert.All(mounts, m => Assert.True(m.IsReadOnly));
+ }
+
+ [Fact]
+ public void WithConfigFile_NonExistentFile_ThrowsFileNotFoundException()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ var nonExistent = new FileInfo(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".json"));
+
+ Assert.Throws(() =>
+ builder.AddDataAPIBuilder("dab")
+ .WithConfigFile(nonExistent));
+ }
+
+ [Fact]
+ public void WithConfigFile_DuplicateFile_ThrowsInvalidOperationException()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ var ex = Assert.Throws(() =>
+ builder.AddDataAPIBuilder("dab", configFilePaths: "./dab-config-anonymous.json")
+ .WithConfigFile(new FileInfo("./dab-config-anonymous.json")));
+
+ Assert.Contains("/App/dab-config-anonymous.json", ex.Message);
+ Assert.Contains("already mounted", ex.Message);
+ }
+
+ [Fact]
+ public void WithConfigFile_DuplicateAcrossCalls_ThrowsInvalidOperationException()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ var ex = Assert.Throws(() =>
+ builder.AddDataAPIBuilder("dab", configFilePaths: "./dab-config-anonymous.json")
+ .WithConfigFile(new FileInfo("./dab-config-anonymous-2.json"))
+ .WithConfigFile(new FileInfo("./dab-config-anonymous-2.json")));
+
+ Assert.Contains("/App/dab-config-anonymous-2.json", ex.Message);
+ }
+
+ [Fact]
+ public void WithConfigFile_NullBuilder_ThrowsArgumentNullException()
+ {
+ IResourceBuilder builder = null!;
+
+ Assert.Throws(() =>
+ builder.WithConfigFile(new FileInfo("./dab-config-anonymous.json")));
+ }
+
+
+ [Fact]
+ public void WithConfigFolder_MountsAllFilesInDirectory()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ builder.AddDataAPIBuilder("dab", configFilePaths: "./dab-config-anonymous.json")
+ .WithConfigFolder(new DirectoryInfo("./config-folder"));
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+ var resource = Assert.Single(appModel.Resources.OfType());
+
+ Assert.True(resource.TryGetAnnotationsOfType(out var mounts));
+
+ // 1 from AddDataAPIBuilder + 2 from config-folder
+ Assert.Equal(3, mounts.Count());
+ Assert.Contains(mounts, m => m.Target == "/App/dab-config-anonymous.json");
+ Assert.Contains(mounts, m => m.Target == "/App/dab-folder-config-anonymous.json");
+ Assert.Contains(mounts, m => m.Target == "/App/dab-folder-config-anonymous-2.json");
+ }
+
+ [Fact]
+ public void WithConfigFolder_MountsAreReadOnly()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ builder.AddDataAPIBuilder("dab", configFilePaths: "./dab-config-anonymous.json")
+ .WithConfigFolder(new DirectoryInfo("./config-folder"));
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+ var resource = Assert.Single(appModel.Resources.OfType());
+
+ Assert.True(resource.TryGetAnnotationsOfType(out var mounts));
+ Assert.All(mounts, m => Assert.True(m.IsReadOnly));
+ }
+
+ [Fact]
+ public void WithConfigFolder_MountsIndividualFilesNotFolders()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ builder.AddDataAPIBuilder("dab", configFilePaths: "./dab-config-anonymous.json")
+ .WithConfigFolder(new DirectoryInfo("./config-folder"));
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+ var resource = Assert.Single(appModel.Resources.OfType());
+
+ Assert.True(resource.TryGetAnnotationsOfType(out var mounts));
+
+ // Every mount target should be a file path (not a directory)
+ Assert.All(mounts, m =>
+ {
+ Assert.StartsWith("/App/", m.Target);
+ Assert.Contains(".", m.Target); // has a file extension
+ Assert.Equal(ContainerMountType.BindMount, m.Type);
+ });
+ }
+
+ [Fact]
+ public void WithConfigFolder_CalledMultipleTimes_IsAdditive()
+ {
+ // Create a temp directory with a unique file to avoid collisions
+ string tempDir = Path.Combine(Path.GetTempPath(), "dab-test-" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(tempDir);
+ string tempFile = Path.Combine(tempDir, "dab-temp-config.json");
+ File.WriteAllText(tempFile, "{}");
+
+ try
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ builder.AddDataAPIBuilder("dab", configFilePaths: "./dab-config-anonymous.json")
+ .WithConfigFolder(new DirectoryInfo("./config-folder"))
+ .WithConfigFolder(new DirectoryInfo(tempDir));
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+ var resource = Assert.Single(appModel.Resources.OfType());
+
+ Assert.True(resource.TryGetAnnotationsOfType(out var mounts));
+
+ // 1 default + 2 from config-folder + 1 from tempDir
+ Assert.Equal(4, mounts.Count());
+ Assert.Contains(mounts, m => m.Target == "/App/dab-temp-config.json");
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void WithConfigFolder_NonExistentDirectory_ThrowsDirectoryNotFoundException()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ var nonExistent = new DirectoryInfo(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()));
+
+ Assert.Throws(() =>
+ builder.AddDataAPIBuilder("dab")
+ .WithConfigFolder(nonExistent));
+ }
+
+ [Fact]
+ public void WithConfigFolder_EmptyDirectory_IsNoOp()
+ {
+ string emptyDir = Path.Combine(Path.GetTempPath(), "dab-empty-" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(emptyDir);
+
+ try
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ builder.AddDataAPIBuilder("dab", configFilePaths: "./dab-config-anonymous.json")
+ .WithConfigFolder(new DirectoryInfo(emptyDir));
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+ var resource = Assert.Single(appModel.Resources.OfType());
+
+ Assert.True(resource.TryGetAnnotationsOfType(out var mounts));
+
+ // Only the default config file mount
+ Assert.Single(mounts);
+ }
+ finally
+ {
+ Directory.Delete(emptyDir, true);
+ }
+ }
+
+ [Fact]
+ public void WithConfigFolder_DuplicateWithExistingMount_ThrowsInvalidOperationException()
+ {
+ // config-folder contains dab-folder-config-anonymous.json - use WithConfigFile to mount it first, then folder
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ var folderDir = new DirectoryInfo("./config-folder");
+ var fileInFolder = folderDir.GetFiles().First();
+
+ var ex = Assert.Throws(() =>
+ builder.AddDataAPIBuilder("dab", configFilePaths: "./dab-config-anonymous.json")
+ .WithConfigFile(new FileInfo(fileInFolder.FullName))
+ .WithConfigFolder(folderDir));
+
+ Assert.Contains("already mounted", ex.Message);
+ }
+
+ [Fact]
+ public void WithConfigFolder_NullBuilder_ThrowsArgumentNullException()
+ {
+ IResourceBuilder builder = null!;
+
+ Assert.Throws(() =>
+ builder.WithConfigFolder(new DirectoryInfo("./config-folder")));
+ }
+
+ [Fact]
+ public void WithConfigFile_And_WithConfigFolder_Combined()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ builder.AddDataAPIBuilder("dab", configFilePaths: "./dab-config-anonymous.json")
+ .WithConfigFile(new FileInfo("./dab-config-authenticated.json"))
+ .WithConfigFolder(new DirectoryInfo("./config-folder"));
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+ var resource = Assert.Single(appModel.Resources.OfType());
+
+ Assert.True(resource.TryGetAnnotationsOfType(out var mounts));
+
+ // 1 from AddDataAPIBuilder + 1 from WithConfigFile + 2 from WithConfigFolder
+ Assert.Equal(4, mounts.Count());
+ Assert.Contains(mounts, m => m.Target == "/App/dab-config-anonymous.json");
+ Assert.Contains(mounts, m => m.Target == "/App/dab-config-authenticated.json");
+ Assert.Contains(mounts, m => m.Target == "/App/dab-folder-config-anonymous.json");
+ Assert.Contains(mounts, m => m.Target == "/App/dab-folder-config-anonymous-2.json");
}
}
diff --git a/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/config-folder/dab-folder-config-anonymous-2.json b/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/config-folder/dab-folder-config-anonymous-2.json
new file mode 100644
index 000000000..fcaa327c5
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/config-folder/dab-folder-config-anonymous-2.json
@@ -0,0 +1,26 @@
+{
+ "$schema": "https://github.com/Azure/data-api-builder/releases/download/v1.6.87/dab.draft.schema.json",
+ "data-source": {
+ "database-type": "mssql",
+ "connection-string": "missing-on-purpose"
+ },
+ "runtime": {
+ "host": {
+ "mode": "development"
+ }
+ },
+ "entities": {
+ "Genre": {
+ "source": {
+ "object": "[dbo].[Genre]",
+ "type": "table"
+ },
+ "permissions": [
+ {
+ "role": "anonymous",
+ "actions": [ "*" ]
+ }
+ ]
+ }
+ }
+}
diff --git a/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/config-folder/dab-folder-config-anonymous.json b/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/config-folder/dab-folder-config-anonymous.json
new file mode 100644
index 000000000..a094020a6
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/config-folder/dab-folder-config-anonymous.json
@@ -0,0 +1,26 @@
+{
+ "$schema": "https://github.com/Azure/data-api-builder/releases/download/v1.6.87/dab.draft.schema.json",
+ "data-source": {
+ "database-type": "mssql",
+ "connection-string": "missing-on-purpose"
+ },
+ "runtime": {
+ "host": {
+ "mode": "development"
+ }
+ },
+ "entities": {
+ "Director": {
+ "source": {
+ "object": "[dbo].[Director]",
+ "type": "table"
+ },
+ "permissions": [
+ {
+ "role": "anonymous",
+ "actions": [ "*" ]
+ }
+ ]
+ }
+ }
+}
diff --git a/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/dab-config-2.json b/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/dab-config-anonymous-2.json
similarity index 91%
rename from tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/dab-config-2.json
rename to tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/dab-config-anonymous-2.json
index 924bde438..008003aa4 100644
--- a/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/dab-config-2.json
+++ b/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/dab-config-anonymous-2.json
@@ -1,5 +1,5 @@
{
- "$schema": "https://github.com/Azure/data-api-builder/releases/download/v0.9.7/dab.draft.schema.json",
+ "$schema": "https://github.com/Azure/data-api-builder/releases/download/v1.6.87/dab.draft.schema.json",
"data-source": {
"database-type": "mssql",
"connection-string": "missing-on-purpose"
diff --git a/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/dab-config.json b/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/dab-config-anonymous.json
similarity index 91%
rename from tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/dab-config.json
rename to tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/dab-config-anonymous.json
index 924bde438..008003aa4 100644
--- a/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/dab-config.json
+++ b/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/dab-config-anonymous.json
@@ -1,5 +1,5 @@
{
- "$schema": "https://github.com/Azure/data-api-builder/releases/download/v0.9.7/dab.draft.schema.json",
+ "$schema": "https://github.com/Azure/data-api-builder/releases/download/v1.6.87/dab.draft.schema.json",
"data-source": {
"database-type": "mssql",
"connection-string": "missing-on-purpose"
diff --git a/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/dab-config-authenticated.json b/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/dab-config-authenticated.json
new file mode 100644
index 000000000..38ea95aba
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/dab-config-authenticated.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "https://github.com/Azure/data-api-builder/releases/download/v1.6.87/dab.draft.schema.json",
+ "data-source": {
+ "database-type": "mssql",
+ "connection-string": "missing-on-purpose"
+ },
+ "runtime": {
+ "host": {
+ "mode": "development",
+ "authentication": {
+ "provider": "Simulator"
+ }
+ }
+ },
+ "entities": {
+ "Actor": {
+ "source": {
+ "object": "[dbo].[Actor]",
+ "type": "table"
+ },
+ "permissions": [
+ {
+ "role": "authenticated",
+ "actions": [ "*" ]
+ }
+ ]
+ }
+ }
+}