diff --git a/.aspire/settings.json b/.aspire/settings.json
new file mode 100644
index 0000000000..61dd69a516
--- /dev/null
+++ b/.aspire/settings.json
@@ -0,0 +1,3 @@
+{
+ "appHostPath": "../src/Aspire.AppHost/Aspire.AppHost.csproj"
+}
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
index a224f6bcbb..fbe6bd70b1 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -15,3 +15,6 @@
*.verified.txt text eol=lf working-tree-encoding=UTF-8
*.verified.xml text eol=lf working-tree-encoding=UTF-8
*.verified.json text eol=lf working-tree-encoding=UTF-8
+
+# This can't be enabled as there are tests that rely on the CRLF endings in files'
+#*.cs text eol=lf
diff --git a/.pipelines/azure-pipelines.yml b/.pipelines/azure-pipelines.yml
index a29cd29de2..88ce6bb186 100644
--- a/.pipelines/azure-pipelines.yml
+++ b/.pipelines/azure-pipelines.yml
@@ -9,6 +9,18 @@ trigger:
- gh-readonly-queue/main
- release/*
+pr:
+ branches:
+ include:
+ - main
+ - release/*
+ paths:
+ exclude:
+ - samples/**
+ - docs/**
+ - '*.md'
+ - templates/**
+
variables:
# DebugArguments: ' --filter "TestCategory!=ABC" --verbosity normal '
# ReleaseArguments: ' --filter "TestCategory!=ABC" --verbosity normal '
diff --git a/.pipelines/cosmos-pipelines.yml b/.pipelines/cosmos-pipelines.yml
index 2f95f45211..61d066aac7 100644
--- a/.pipelines/cosmos-pipelines.yml
+++ b/.pipelines/cosmos-pipelines.yml
@@ -13,6 +13,18 @@ trigger:
- gh-readonly-queue/main
- release/*
+pr:
+ branches:
+ include:
+ - main
+ - release/*
+ paths:
+ exclude:
+ - samples/**
+ - docs/**
+ - '*.md'
+ - templates/**
+
strategy:
matrix:
windows:
diff --git a/.pipelines/dwsql-pipelines.yml b/.pipelines/dwsql-pipelines.yml
index 2ae2730747..767c6c7df9 100644
--- a/.pipelines/dwsql-pipelines.yml
+++ b/.pipelines/dwsql-pipelines.yml
@@ -16,6 +16,18 @@ trigger:
exclude:
- docs
+pr:
+ branches:
+ include:
+ - main
+ - release/*
+ paths:
+ exclude:
+ - samples/**
+ - docs/**
+ - '*.md'
+ - templates/**
+
jobs:
- job: linux
pool:
diff --git a/.pipelines/mssql-pipelines.yml b/.pipelines/mssql-pipelines.yml
index 81955bd889..af823db200 100644
--- a/.pipelines/mssql-pipelines.yml
+++ b/.pipelines/mssql-pipelines.yml
@@ -16,6 +16,18 @@ trigger:
exclude:
- docs
+pr:
+ branches:
+ include:
+ - main
+ - release/*
+ paths:
+ exclude:
+ - samples/**
+ - docs/**
+ - '*.md'
+ - templates/**
+
jobs:
- job: linux
condition: false # Disable until we resolve flakey pipeline issue. https://github.com/Azure/data-api-builder/issues/2010
@@ -110,7 +122,7 @@ jobs:
displayName: 'Run MsSql Integration Tests'
inputs:
command: test
- arguments: '--filter "TestCategory=MsSql" --no-build --configuration $(buildConfiguration) --collect "XPlat Code coverage"'
+ arguments: '--filter "TestCategory=MsSql&FullyQualifiedName!~ConfigurationHotReloadTests" --no-build --configuration $(buildConfiguration) --collect "XPlat Code coverage"'
projects: '**/*Tests/*.csproj'
- task: CmdLine@2
@@ -234,11 +246,20 @@ jobs:
targetFiles: 'src/out/tests/*/dab-config.MsSql.json'
- task: DotNetCoreCLI@2
- displayName: 'Run MsSql Integration Tests'
+ displayName: 'MsSql Integration Tests'
+ inputs:
+ command: test
+ arguments: '--filter "TestCategory=MsSql&FullyQualifiedName!~ConfigurationHotReloadTests" --no-build --configuration $(buildConfiguration) --collect "XPlat Code coverage"'
+ projects: '**/*Tests/*.csproj'
+
+
+ - task: DotNetCoreCLI@2
+ displayName: 'Hot-Reload Tests'
inputs:
command: test
- arguments: '--filter "TestCategory=MsSql" --no-build --configuration $(buildConfiguration) --collect "XPlat Code coverage"'
+ arguments: '--filter "TestCategory=MsSql&FullyQualifiedName~ConfigurationHotReloadTests" --no-build --configuration $(buildConfiguration) --collect "XPlat Code coverage" --logger "console;verbosity=detailed"'
projects: '**/*Tests/*.csproj'
+ timeoutInMinutes: 45
- task: PublishCodeCoverageResults@1
displayName: 'Publish code coverage'
diff --git a/.pipelines/mysql-pipelines.yml b/.pipelines/mysql-pipelines.yml
index 2623415a70..7377d5b69c 100644
--- a/.pipelines/mysql-pipelines.yml
+++ b/.pipelines/mysql-pipelines.yml
@@ -14,6 +14,18 @@ trigger:
- gh-readonly-queue/main
- release/*
+pr:
+ branches:
+ include:
+ - main
+ - release/*
+ paths:
+ exclude:
+ - samples/**
+ - docs/**
+ - '*.md'
+ - templates/**
+
jobs:
- job: linux
pool:
diff --git a/.pipelines/pg-pipelines.yml b/.pipelines/pg-pipelines.yml
index 6618dab734..d6b7600e1f 100644
--- a/.pipelines/pg-pipelines.yml
+++ b/.pipelines/pg-pipelines.yml
@@ -9,6 +9,18 @@ trigger:
- gh-readonly-queue/main
- release/*
+pr:
+ branches:
+ include:
+ - main
+ - release/*
+ paths:
+ exclude:
+ - samples/**
+ - docs/**
+ - '*.md'
+ - templates/**
+
jobs:
- job: linux
pool:
diff --git a/.pipelines/unittest-pipelines.yml b/.pipelines/unittest-pipelines.yml
index b82562f780..56357a181c 100644
--- a/.pipelines/unittest-pipelines.yml
+++ b/.pipelines/unittest-pipelines.yml
@@ -1,6 +1,26 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
+trigger:
+ batch: true
+ branches:
+ include:
+ - main
+ - gh-readonly-queue/main
+ - release/*
+
+pr:
+ branches:
+ include:
+ - main
+ - release/*
+ paths:
+ exclude:
+ - samples/**
+ - docs/**
+ - '*.md'
+ - templates/**
+
pool:
vmImage: 'ubuntu-latest' # examples of other options: 'macOS-10.15', 'windows-2019'
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ab98fbe8cf..a9fd633f27 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -95,7 +95,7 @@ We use `dotnet format` to enforce code conventions. It is run automatically in C
#### Enforcing code style with git hooks
-You can copy paste the following commands to install a git pre-commit hook. This will cause a commit to fail if you forgot to run `dotnet format`. If you have run on save enabled in your editor this is not necessary.
+You can copy paste the following commands to install a git pre-commit hook (creates a pre-commit file in your .git folder, which isn't shown in vs code). This will cause a commit to fail if you forgot to run `dotnet format`. If you have run on save enabled in your editor this is not necessary.
```bash
cat > .git/hooks/pre-commit << __EOF__
@@ -112,17 +112,42 @@ if [ "\$(get_files)" = '' ]; then
fi
get_files |
- xargs dotnet format src/Azure.DataApiBuilder.Service.sln \\
- --check \\
- --fix-whitespace --fix-style warn --fix-analyzers warn \\
+ xargs dotnet format src/Azure.DataApiBuilder.sln \\
+ --verify-no-changes
--include \\
|| {
get_files |
- xargs dotnet format src/Azure.DataApiBuilder.Service.sln \\
- --fix-whitespace --fix-style warn --fix-analyzers warn \\
+ xargs dotnet format src/Azure.DataApiBuilder.sln \\
--include
exit 1
}
__EOF__
chmod +x .git/hooks/pre-commit
```
+
+The file should look like this
+
+``` bash
+#!/bin/bash
+set -euo pipefail
+
+get_files() {
+ git diff --cached --name-only --diff-filter=ACMR | \
+ grep '\.cs$'
+}
+
+if [ "$(get_files)" = '' ]; then
+ exit 0
+fi
+
+get_files |
+ xargs dotnet format src/Azure.DataApiBuilder.sln \
+ --verify-no-changes \
+ --include \
+ || {
+ get_files |
+ xargs dotnet format src/Azure.DataApiBuilder.sln \
+ --include
+ exit 1
+}
+```
diff --git a/README.md b/README.md
index 1746a62482..ec9d83ca73 100644
--- a/README.md
+++ b/README.md
@@ -7,9 +7,9 @@
[What's new?](https://learn.microsoft.com/azure/data-api-builder/whats-new)
-## Community
+## Join the community
-Join the Data API builder community! This sign up will help us maintain a list of interested developers to be part of our roadmap and to help us better understand the different ways DAB is being used. Sign up [here](https://forms.office.com/pages/responsepage.aspx?id=v4j5cvGGr0GRqy180BHbR1S1JdzGAxhDrefV-tBYtwZUNE1RWVo0SUVMTkRESUZLMVVOS0wwUFNVRy4u).
+Want to be part of our priorities and roadmap? Sign up [here](https://forms.office.com/pages/responsepage.aspx?id=v4j5cvGGr0GRqy180BHbR1S1JdzGAxhDrefV-tBYtwZUNE1RWVo0SUVMTkRESUZLMVVOS0wwUFNVRy4u).

@@ -17,199 +17,299 @@ Join the Data API builder community! This sign up will help us maintain a list o
Data API builder (DAB) is an open-source, no-code tool that creates secure, full-featured REST and GraphQL endpoints for your database. It’s a CRUD data API engine that runs in a container—on Azure, any other cloud, or on-premises. DAB is built for developers with integrated tooling, telemetry, and other productivity features.
-```mermaid
-erDiagram
- DATA_API_BUILDER ||--|{ DATA_API : "Provides"
- DATA_API_BUILDER {
- container true "Microsoft Container Repository"
- open-source true "MIT license / any cloud or on-prem."
- objects true "Supports: Table / View / Stored Procedure"
- developer true "Swagger / Nitro (fka Banana Cake Pop)"
- otel true "Open Telemetry / Structured Logs / Health Endpoints"
- security true "EntraId / EasyAuth / OAuth / JWT / Anonymous"
- cache true "Level1 (in-memory) / Level2 (redis)"
- policy true "Item policy / Database policy / Claims policy"
- hot_reload true "Dynamically controllable log levels"
- }
- DATA_API ||--o{ DATASOURCE : "Queries"
- DATA_API {
- REST true "$select / $filter / $orderby"
- GraphQL true "relationships / multiple mutations"
- }
- DATASOURCE {
- MS_SQL Supported
- PostgreSQL Supported
- Cosmos_DB Supported
- MySQL Supported
- SQL_DW Supported
- }
- CLIENT ||--o{ DATA_API : "Consumes"
- CLIENT {
- Transport HTTP "HTTP / HTTPS"
- Syntax JSON "Standard payloads"
- Mobile Supported "No requirement"
- Web Supported "No requirement"
- Desktop Supported "No requirement"
- Language Any "No requirement"
- Framework None "Not required"
- Library None "Not required"
- ORM None "Not required"
- Driver None "Not required"
- }
-```
+> [!IMPORTANT]
+> Data API builder (DAB) is open source and always free.
+
+### Which databases does Data API builder support?
+
+| | Azure SQL | SQL Server | SQLDW | Cosmos DB | PostgreSQL | MySQL |
+| :-----------: | :-------: | :--------: | :---: | :-------: | :--------: | :---: |
+| **Supported** | Yes | Yes | Yes | Yes | Yes | Yes |
+
+### Which environments does Data API builder support?
+
+| | On-Prem | Azure | AWS | GCP | Other |
+| :-----------: | :-----: | :---: | :--: | :--: | :---: |
+| **Supported** | Yes | Yes | Yes | Yes | Yes |
+
+### Which endpoints does Data API builder support?
+
+| | REST | GraphQL | MCP |
+| :-----------: | :--: | :-----: | :---------: |
+| **Supported** | Yes | Yes | Coming soon |
+
+## Getting started
+
+Use the [Getting Started](https://learn.microsoft.com/azure/data-api-builder/get-started/get-started-with-data-api-builder) tutorial to quickly explore the core tools and concepts.
-## Getting Started
+### 1. Install the `dotnet` [command line](https://get.dot.net)
-Use the [Getting Started](https://learn.microsoft.com/azure/data-api-builder/get-started/get-started-with-data-api-builder) tutorial to quickly explore the core tools and concepts. It gives you hands-on experience with how DAB makes you more efficient by removing boilerplate code.
+https://get.dot.net
-**1. Install the DAB CLI**
+> [!NOTE]
+> You may already have .NET installed!
-The [DAB CLI](https://aka.ms/dab/docs) is a cross-platform .NET tool. Install the [.NET SDK](https://get.dot.net) before running:
+The Data API builder (DAB) command line requires the .NET runtime version 8 or later.
+#### Validate your installation
+
+```sh
+dotnet --version
```
+
+### 2. Install the `dab` command line
+
+The Data API builder (DAB) command line is cross-platform and intended for local developer use.
+
+```sh
dotnet tool install microsoft.dataapibuilder -g
```
-**2. Create your initial configuration file**
+#### Validate your installation
+
+```sh
+dab --version
+```
+
+### 3. Create your database (example: Azure SQL database / T-SQL)
+
+This example uses a single table for simplicity.
+
+```sql
+CREATE TABLE dbo.Todo
+(
+ Id INT PRIMARY KEY IDENTITY,
+ Title NVARCHAR(500) NOT NULL,
+ IsCompleted BIT NOT NULL DEFAULT 0
+);
+INSERT dbo.Todo (Title, IsCompleted)
+VALUES
+ ('Walk the dog', 0),
+ ('Feed the fish', 0),
+ ('Clean the cat', 1);
+```
+
+### 4. Prepare your connection string
-DAB requires a JSON configuration file. Edit manually or with the CLI. Use `dab --help` for syntax options.
+Data API builder (DAB) supports `.env` files for testing process-level environment variables.
+#### PowerShell (Windows)
+
+```ps
+echo "my-connection-string=$env:database_connection_string" > .env
```
+
+#### cmd.exe (Windows)
+
+```cmd
+echo my-connection-string=%database_connection_string% > .env
+```
+
+#### bash (macOS/Linux)
+
+```bash
+echo "my-connection-string=$database_connection_string" > .env
+```
+
+#### Resulting .env file
+
+The file `.env` is automatically created through this process. These are the resulting contents:
+
+```
+"my-connection-string=$env:database_connection_string"
+```
+> [!NOTE]
+> Be sure and replace `database_connection_string` with your actual database connection string.
+
+> [!IMPORTANT]
+> Adding `.env` to your `.gitignore` file will help ensure your secrets are not added to source control.
+
+### 5. Create your initial configuration file
+
+Data API builder (DAB) requires a JSON configuration file. Use `dab --help` for syntax options.
+
+```sh
dab init
--database-type mssql
--connection-string "@env('my-connection-string')"
--host-mode development
```
-**3. Add your first table**
+> [!NOTE]
+> Including `--host-mode development` enables Swagger for REST and Nitro for GraphQL.
-DAB supports tables, views, and stored procedures. It works with SQL Server, Azure Cosmos DB, PostgreSQL, MySQL, and SQL Data Warehouse. Security is engine-level, but permissions are per entity.
+#### Resulting configuration
+The file `dab-config.json` is automatically created through this process. These are the resulting contents:
+
+```json
+{
+ "$schema": "https://github.com/Azure/data-api-builder/releases/download/v1.5.56/dab.draft.schema.json",
+ "data-source": {
+ "database-type": "mssql",
+ "connection-string": "@env('my-connection-string')",
+ "options": {
+ "set-session-context": false
+ }
+ },
+ "runtime": {
+ "rest": {
+ "enabled": true,
+ "path": "/api",
+ "request-body-strict": true
+ },
+ "graphql": {
+ "enabled": true,
+ "path": "/graphql",
+ "allow-introspection": true
+ },
+ "host": {
+ "cors": {
+ "origins": [],
+ "allow-credentials": false
+ },
+ "authentication": {
+ "provider": "AppService"
+ },
+ "mode": "development"
+ }
+ },
+ "entities": { }
+}
```
-dab add Actor
- --source "dbo.Actor"
+### 6. Add your table to the configuration
+
+```sh
+dab add Todo
+ --source "dbo.Todo"
--permissions "anonymous:*"
```
-**4. Run Data API builder**
+> [!NOTE]
+> DAB supports tables, views, and stored procedures. When the type is not specified, the default is `table`.
+
+#### Resulting configuration
+
+The `entities` section of the configuration is no longer empty:
+
+```json
+{
+ "entities": {
+ "Todo": {
+ "source": {
+ "object": "dbo.Todo",
+ "type": "table"
+ },
+ "graphql": {
+ "enabled": true,
+ "type": {
+ "singular": "Todo",
+ "plural": "Todos"
+ }
+ },
+ "rest": {
+ "enabled": true
+ },
+ "permissions": [
+ {
+ "role": "anonymous",
+ "actions": [
+ {
+ "action": "*"
+ }
+ ]
+ }
+ ]
+ }
+ }
+}
+```
-In `production`, DAB runs in a container. In `development`, it’s self-hosted locally with hot reload, Swagger, and Nitro (fka Banana Cake Pop) support.
+### 7. Run Data API builder
-```
+In `production`, DAB runs in a container. In `development`, it’s locally self-hosted.
+
+```sh
dab start
```
-> **Note**: Before you run `dab start`, make sure your connection string is stored in an environment variable called `my-connection-string`. This is required for `@env('my-connection-string')` in your config file to work. The easiest way is to create a `.env` file with `name=value` pairs—DAB will load these automatically at runtime.
+> [!IMPORTANT]
+> The DAB CLI assumes your configuration file is called `dab-config.json` and is in the local folder.
-**5. Access your data source**
+### 8. Access your data!
-By default, DAB enables both REST and GraphQL. REST supports `$select`, `$filter`, and `$orderBy`. GraphQL uses config-defined relationships.
+By default, DAB enables both REST and GraphQL.
+```sh
+GET http://localhost:5000/api/Todo
```
-GET http://localhost:5000/api/Actor
-```
-
-### Walk-through video
-
-
-
+> [!NOTE]
+> Change the URL to match your port if it is different.
-Demo source code: [startrek](https://aka.ms/dab/startrek)
+#### Other things you should try
-## Overview
-
-| Category | Features |
-|----------------|----------|
-| **Database Objects** | • NoSQL collections
• RDBMS tables, views, stored procedures |
-| **Data Sources** | • SQL Server & Azure SQL
• Azure Cosmos DB
• PostgreSQL
• MySQL |
-| **REST** | • `$select` for projection
• `$filter` for filtering
• `$orderBy` for sorting |
-| **GraphQL** | • Relationship navigation
• Data aggregation
• Multiple mutations |
-| **Telemetry** | • Structured logs
• OpenTelemetry
• Application Insights
• Health endpoints |
-| **Advanced** | • Pagination
• Level 1 (in-memory) cache |
-| **Authentication** | • OAuth2/JWT
• EasyAuth
• Entra ID |
-| **Authorization** | • Role-based support
• Entity permissions
• Database policies |
-| **Developer** | • Cross-platform CLI
• Swagger (REST)
• Nitro [previously Banana Cake Pop] (GraphQL)
• Open Source
• Configuration Hot Reload |
+* DAB’s Health endpoint: `http://localhost:5000/health`
+* DAB’s Swagger UI: `http://localhost:5000/swagger`
+* DAB’s Nitro UI: `http://localhost:5000/graphql`
## How does it work?
-This diagram shows how DAB works. DAB dynamically creates endpoints from your config file. It translates HTTP requests to SQL, returns JSON, and auto-pages results.
+DAB dynamically creates endpoints and translates requests to SQL, returning JSON.
```mermaid
sequenceDiagram
- actor Client
-
- box Data API builder (DAB)
- participant Endpoint
- participant QueryBuilder
- end
-
- participant Configuration as Configuration File
-
- box Data Source
- participant DB
- end
-
- Endpoint->>Endpoint: Start
- activate Endpoint
- Endpoint->>Configuration: Request
- Configuration-->>Endpoint: Configuration
- Endpoint->>DB: Request
- DB-->>Endpoint: Metadata
- Note over Endpoint, DB: Some configuration is validated against the metadata
- Endpoint-->>Endpoint: Configure
- deactivate Endpoint
- Client-->>Endpoint: HTTP Request
- activate Endpoint
- critical
- Endpoint-->>Endpoint: Authenticate
- Endpoint-->>Endpoint: Authorize
- end
- Endpoint->>QueryBuilder: Request
- QueryBuilder-->>Endpoint: SQL
- alt Cache
- Endpoint-->>Endpoint: Use Cache
- else Query
- Endpoint-->>DB: Request
- Note over Endpoint, DB: Query is automatically throttled and results paginated
- DB->>Endpoint: Results
- Note over Endpoint, DB: Results are automatically cached for use in next request
- end
- Endpoint->>Client: HTTP 200
- deactivate Endpoint
-```
-
-Because DAB is stateless, it can scale up or out using any container size. It builds a feature-rich API like you would from scratch—but now you don’t have to.
-
-## Additional Resources
-
-- [Online Documentation](https://aka.ms/dab/docs)
-- [Official Samples](https://aka.ms/dab/samples)
-- [Known Issues](https://learn.microsoft.com/azure/data-api-builder/known-issues)
-- [Feature Roadmap](https://github.com/Azure/data-api-builder/discussions/1377)
+ actor Client as Client
+ participant Endpoint as Endpoint
+ participant QueryBuilder as QueryBuilder
+ participant DB as Database
+
+ %% Initialization / Warming up section (light grey)
+ rect rgba(120,120,120,0.10)
+ Endpoint -->>+ Endpoint: Read Config
+ Endpoint ->> DB: Query Metadata
+ DB -->> Endpoint: Metadata Response
+ Endpoint ->>- Endpoint: Start Engine
+ end
+
+ %% Request/Response section (very light purple)
+ rect rgba(180,150,255,0.11)
+ Client ->>+ Endpoint: HTTP Request
+ Endpoint ->> Endpoint: Authorize
+ Endpoint ->> QueryBuilder: Invoke
+ QueryBuilder -->> Endpoint: SQL Query
+ Endpoint ->> DB: Submit Query
+ DB -->> Endpoint: Data Response
+ Endpoint -->>- Client: HTTP Response
+ end
+```
-#### References
+## Additional resources
-- [Microsoft REST API Guidelines](https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md)
-- [Microsoft Azure REST API Guidelines](https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md)
-- [GraphQL Specification](https://graphql.org/)
+* [Online Documentation](https://aka.ms/dab/docs)
+* [Official Samples](https://aka.ms/dab/samples)
+* [Known Issues](https://learn.microsoft.com/azure/data-api-builder/known-issues)
+* [Feature Roadmap](https://github.com/Azure/data-api-builder/discussions/1377)
-### How to Contribute
+#### References
-To contribute, see these documents:
+* [Microsoft REST API Guidelines](https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md)
+* [Microsoft Azure REST API Guidelines](https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md)
+* [GraphQL Specification](https://graphql.org/)
-- [Code of Conduct](./CODE_OF_CONDUCT.md)
-- [Security](./SECURITY.md)
-- [Contributing](./CONTRIBUTING.md)
+### How to contribute
-### License
+To contribute, see these documents:
-**Data API builder for Azure Databases** is licensed under the MIT License. See [LICENSE](./LICENSE.txt) for details.
+* [Code of Conduct](./CODE_OF_CONDUCT.md)
+* [Security](./SECURITY.md)
+* [Contributing](./CONTRIBUTING.md)
+* [MIT License](./LICENSE.txt)
-### Third-Party Component Notice
+### Third-party component notice
-Nitro (fka Banana Cake Pop by ChilliCream, Inc.) may optionally store work in its cloud service via your ChilliCream account. Microsoft is not affiliated with or endorsing this service. Use at your discretion.
+Nitro (formerly Banana Cake Pop by ChilliCream, Inc.) may optionally store work in its cloud service via your ChilliCream account. Microsoft is not affiliated with or endorsing this service. Use at your discretion.
### Trademarks
-This project may use trademarks or logos. Use of Microsoft trademarks must follow Microsoft’s [Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks). Use of third-party marks is subject to their policies.
+This project may use trademarks or logos. Use of Microsoft trademarks must follow Microsoft’s [Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks). Use of third-party marks is subject to their policies.
\ No newline at end of file
diff --git a/docs/media/dab-aifoundry-architecture.png b/docs/media/dab-aifoundry-architecture.png
new file mode 100644
index 0000000000..6823ef6418
Binary files /dev/null and b/docs/media/dab-aifoundry-architecture.png differ
diff --git a/docs/testing-guide/ai-foundry-integration.md b/docs/testing-guide/ai-foundry-integration.md
new file mode 100644
index 0000000000..0165b725f6
--- /dev/null
+++ b/docs/testing-guide/ai-foundry-integration.md
@@ -0,0 +1,275 @@
+# Deploying SQL MCP Server implemented in Data API builder and Integrating with Azure AI Foundry
+
+This document provides an end‑to‑end guide to stand up a **SQL MCP Server** with **Model Context Protocol (MCP)** tools implemented in **Data API builder (DAB)** container that also exposes **REST** and **GraphQL** endpoints, and to integrate those MCP tools with an **Azure AI Foundry Agent**.
+
+
+
+## 1. Architecture Overview
+
+**Components**
+- **Azure SQL Database** hosting domain tables and stored procedures.
+- **DAB container** (Azure Container Instances in this guide) that:
+ - reads `dab-config.json` from an **Azure Files** share at startup,
+ - exposes **REST**, **GraphQL**, and **MCP** endpoints.
+- **Azure Storage (Files)** to store and version `dab-config.json`.
+- **Azure AI Foundry Agent** configured with an **MCP tool** pointing to the SQL MCP Server endpoint.
+
+**Flow**
+1. DAB starts in ACI → reads `dab-config.json` from the mounted Azure Files share.
+2. DAB exposes `/api` (REST), `/graphql` (GraphQL), and `/mcp` (MCP).
+3. Azure AI Foundry Agent invokes MCP tools to read/update data via DAB’s surface (tables, views and stored procedures).
+
+
+## 2. Prerequisites
+- Azure Subscription with permissions for Resource Groups, Storage, ACI, and Azure SQL.
+- Azure SQL Database provisioned and reachable from ACI.
+- Azure CLI (`az`) and .NET SDK installed locally.
+- DAB CLI version **1.7.81 or later**.
+- Outbound network access from ACI to your Azure SQL server.
+
+
+## 3. Prepare the Database
+
+You need to create the necessary tables and stored procedures in your Azure SQL Database. Below is an example of how to create a simple `Products` table and a stored procedure to retrieve products by category.
+
+**Example:**
+
+1. Connect to your Azure SQL Database using Azure Data Studio, SQL Server Management Studio, or the Azure Portal's Query Editor.
+
+2. Run the following SQL script to create a sample table and stored procedure:
+
+```sql
+-- Create Products table
+CREATE TABLE Products (
+ ProductID INT IDENTITY(1,1) PRIMARY KEY,
+ Name NVARCHAR(100) NOT NULL,
+ Category NVARCHAR(50) NOT NULL,
+ Price DECIMAL(10,2) NOT NULL
+);
+
+-- Create stored procedure to get products by category
+CREATE PROCEDURE GetProductsByCategory
+ @Category NVARCHAR(50)
+AS
+BEGIN
+ SET NOCOUNT ON;
+ SELECT ProductID, Name, Category, Price
+ FROM Products
+ WHERE Category = @Category;
+END;
+```
+
+## 4. Install DAB CLI and Bootstrap Configuration
+
+```
+dotnet tool install --global Microsoft.DataApiBuilder --version 1.7.81
+export DATABASE_CONNECTION_STRING="Server=.database.windows.net;Database=;User ID=;Password=;Encrypt=True;"
+
+dab init \
+ --database-type "mssql" \
+ --connection-string "@env('DATABASE_CONNECTION_STRING')" \
+ --host-mode "Development" \
+ --rest.enabled true \
+ --graphql.enabled true \
+ --mcp.enabled true \
+ --mcp.path "/mcp"
+
+```
+
+## 5. Add all required entities (tables and stored procedures) to `dab-config.json` and enable MCP tools in the config
+
+Here is how to add a table entity and a stored procedure to your `dab-config.json`, and ensure MCP tools are enabled:
+
+1. **Open your `dab-config.json` file.**
+
+2. **Add an entity (table) definition** under the `"entities"` section. For example, to expose a `Customers` table:
+ ```
+ "entities": {
+ "Customers": {
+ "source": "Customers",
+ "rest": true,
+ "graphql": true,
+ "mcp": true,
+ "permissions": [
+ {
+ "role": "anonymous",
+ "actions": [ "read", "create", "update", "delete" ]
+ }
+ ]
+ }
+ }
+ ```
+
+3. **Add a stored procedure** under the "entities" section. For example, to expose a stored procedure called GetCustomerOrders:
+
+ ```
+ "GetCustomerOrders": {
+ "source": {
+ "object": "GetCustomerOrders",
+ "type": "stored-procedure"
+ },
+ "rest": true,
+ "graphql": true,
+ "mcp": true,
+ "permissions": [
+ {
+ "role": "anonymous",
+ "actions": [ "execute" ]
+ }
+ ]
+ }
+ ```
+
+Note: Make sure the "entities" section is a valid JSON object. If you have multiple entities, separate them with commas.
+
+4. **Ensure MCP is enabled in the "runtime" section:**
+
+```
+"runtime": {
+ "rest": { "enabled": true },
+ "graphql": { "enabled": true },
+ "mcp": {
+ "enabled": true,
+ "path": "/mcp"
+ }
+}
+```
+
+5. **Example dab-config.json structure:**
+
+```
+{
+ "data-source": {
+ "database-type": "mssql",
+ "connection-string": "@env('DATABASE_CONNECTION_STRING')"
+ },
+ "runtime": {
+ "rest": { "enabled": true },
+ "graphql": { "enabled": true },
+ "mcp": {
+ "enabled": true,
+ "path": "/mcp"
+ }
+ },
+ "entities": {
+ "Customers": {
+ "source": "Customers",
+ "rest": true,
+ "graphql": true,
+ "mcp": true,
+ "permissions": [
+ {
+ "role": "anonymous",
+ "actions": [ "read", "create", "update", "delete" ]
+ }
+ ]
+ },
+ "GetCustomerOrders": {
+ "source": {
+ "object": "GetCustomerOrders",
+ "type": "stored-procedure"
+ },
+ "rest": true,
+ "graphql": true,
+ "mcp": true,
+ "permissions": [
+ {
+ "role": "anonymous",
+ "actions": [ "execute" ]
+ }
+ ]
+ }
+ }
+}
+```
+
+6. **Save the file.**
+
+## 6. Store dab-config.json in Azure Files
+
+1. **Create a Storage Account** (if you don't have one):
+az storage account create
+--name
+--resource-group
+--location
+--sku Standard_LRS
+
+
+2. **Create a File Share**:
+az storage share create
+--name
+--account-name
+
+
+3. **Upload `dab-config.json` to the File Share**:
+az storage file upload
+--account-name
+--share-name
+--source ./dab-config.json
+--path dab-config.json
+
+
+4. **Retrieve the Storage Account key** (needed for mounting in ACI):
+az storage account keys list
+--account-name
+--resource-group
+
+Use the value of `key1` or `key2` as `` in the next step.
+
+
+## 7. Deploy DAB to Azure Container Instances
+
+```
+az container create \
+ --resource-group \
+ --name dab-mcp-demo \
+ --image mcr.microsoft.com/azure-databases/data-api-builder:1.7.81-rc \
+ --dns-name-label \
+ --ports 5000 \
+ --location \
+ --environment-variables DAB_CONFIG_PATH="/aci/dab-config.json" \
+ --azure-file-volume-share-name \
+ --azure-file-volume-account-name \
+ --azure-file-volume-account-key \
+ --azure-file-volume-mount-path "/aci" \
+ --os-type Linux \
+ --cpu 1 \
+ --memory 1.5 \
+ --command-line "dotnet Azure.DataApiBuilder.Service.dll --ConfigFileName $DAB_CONFIG_PATH --LogLevel Debug"
+```
+
+## 8. Integrate with Azure AI Foundry
+
+Follow these steps to connect your SQL MCP endpoint deployed in DAB to Azure AI Foundry and test the integration:
+
+1. **Create or Open a Project**
+ - Navigate to the [Azure AI Foundry portal](https://ai.azure.com/foundry) and sign in.
+ - On the dashboard, click **Projects** in the left navigation pane.
+ - To create a new project, click **New Project**, enter a name (e.g., `DAB-MCP-Demo`), and click **Create**.
+ - To use an existing project, select it from the list.
+
+2. **Add an Agent**
+ - Within your project, go to the **Agents** tab.
+ - Click **Add Agent**.
+ - Enter an agent name (e.g., `DAB-MCP-Agent`).
+ - (Optional) Add a description.
+ - Click **Create**.
+
+3. **Configure the MCP Tool**
+ - In the agent's configuration page, go to the **Tools** section.
+ - Click **Add Tool** and select **MCP** from the tool type dropdown.
+ - In the **MCP Endpoint URL** field, enter your SQL MCP endpoint in DAB, e.g., `http:///mcp`.
+ - (Optional) Configure authentication if your endpoint requires it.
+ - Click **Save** to add the tool.
+
+4. **Test in Playground**
+ - Go to the **Playground** tab in your project.
+ - Select the agent you created from the agent dropdown.
+ - In the input box, enter a prompt that will trigger the MCP tool, such as:
+ ```
+ Get all records from the Customers entity.
+ ```
+ - Click **Run**.
+ - The agent should invoke the MCP tool, which will call your DAB MCP endpoint and return the results.
+ - **Expected Result:** You should see the data returned from your DAB instance displayed in the Playground output panel.
+ - If there are errors, check the DAB container logs and ensure the MCP endpoint is reachable from Azure AI Foundry.
diff --git a/docs/Testing/mcp-inspector-testing.md b/docs/testing-guide/mcp-inspector-testing.md
similarity index 100%
rename from docs/Testing/mcp-inspector-testing.md
rename to docs/testing-guide/mcp-inspector-testing.md
diff --git a/global.json b/global.json
index 7e9f2f6bb4..1bdb496ef0 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "8.0.414",
+ "version": "8.0.417",
"rollForward": "latestFeature"
}
}
diff --git a/samples/aspire/.aspire/settings.json b/samples/aspire/.aspire/settings.json
new file mode 100644
index 0000000000..bbfbabae61
--- /dev/null
+++ b/samples/aspire/.aspire/settings.json
@@ -0,0 +1,5 @@
+{
+ "appHostPath": "../apphost.cs",
+ "features.minimumSdkCheckEnabled": true,
+ "features.singleFileAppHostEnabled": true
+}
\ No newline at end of file
diff --git a/samples/aspire/.gitignore b/samples/aspire/.gitignore
new file mode 100644
index 0000000000..f7ed5b1225
--- /dev/null
+++ b/samples/aspire/.gitignore
@@ -0,0 +1,5 @@
+# Do not ignore anything in this folder
+!*
+
+# Explicitly include dab-config.json
+!dab-config.json
diff --git a/samples/aspire/apphost.cs b/samples/aspire/apphost.cs
new file mode 100644
index 0000000000..0e44c700a8
--- /dev/null
+++ b/samples/aspire/apphost.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#:sdk Aspire.AppHost.Sdk@13.0.0
+#:package Aspire.Hosting.SqlServer@13.0.0
+#:package CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects@9.8.1-beta.420
+#:package CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder@9.8.1-beta.420
+#:package CommunityToolkit.Aspire.Hosting.McpInspector@9.8.0
+
+var builder = DistributedApplication.CreateBuilder(args);
+
+var options = new
+{
+ SqlScript = "database.sql",
+ SqlDatabase = "StarTrek",
+ DabConfig = "dab-config.json",
+ DabImage = "1.7.81-rc",
+ SqlCmdrImage = "latest"
+};
+
+var sqlServer = builder
+ .AddSqlServer("sql-server")
+ .WithDataVolume("sql-data-volume")
+ .WithLifetime(ContainerLifetime.Persistent);
+
+var sqlDatabase = sqlServer
+ .AddDatabase("sql-database", options.SqlDatabase)
+ .WithCreationScript(File.ReadAllText(new FileInfo(options.SqlScript).FullName));
+
+var dabServer = builder
+ .AddContainer("data-api", image: "azure-databases/data-api-builder", tag: options.DabImage)
+ .WithImageRegistry("mcr.microsoft.com")
+ .WithBindMount(source: new FileInfo(options.DabConfig).FullName, target: "/App/dab-config.json", isReadOnly: true)
+ .WithHttpEndpoint(targetPort: 5000, name: "http")
+ .WithEnvironment("MSSQL_CONNECTION_STRING", sqlDatabase)
+ .WithUrls(context =>
+ {
+ context.Urls.Clear();
+ context.Urls.Add(new() { Url = "/graphql", DisplayText = "Nitro", Endpoint = context.GetEndpoint("http") });
+ context.Urls.Add(new() { Url = "/swagger", DisplayText = "Swagger", Endpoint = context.GetEndpoint("http") });
+ context.Urls.Add(new() { Url = "/health", DisplayText = "Health", Endpoint = context.GetEndpoint("http") });
+ })
+ .WithOtlpExporter()
+ .WithParentRelationship(sqlDatabase)
+ .WithHttpHealthCheck("/health")
+ .WaitFor(sqlDatabase);
+
+var sqlCommander = builder
+ .AddContainer("sql-cmdr", "jerrynixon/sql-commander", options.SqlCmdrImage)
+ .WithImageRegistry("docker.io")
+ .WithHttpEndpoint(targetPort: 8080, name: "http")
+ .WithEnvironment("ConnectionStrings__db", sqlDatabase)
+ .WithUrls(context =>
+ {
+ context.Urls.Clear();
+ context.Urls.Add(new() { Url = "/", DisplayText = "Commander", Endpoint = context.GetEndpoint("http") });
+ })
+ .WithParentRelationship(sqlDatabase)
+ .WithHttpHealthCheck("/health")
+ .WaitFor(sqlDatabase);
+
+var mcpInspector = builder
+ .AddMcpInspector("mcp-inspector")
+ .WithMcpServer(dabServer)
+ .WithParentRelationship(dabServer)
+ .WithEnvironment("NODE_TLS_REJECT_UNAUTHORIZED", "0")
+ .WaitFor(dabServer)
+ .WithUrls(context =>
+ {
+ context.Urls.First().DisplayText = "Inspector";
+ });
+
+await builder.Build().RunAsync();
\ No newline at end of file
diff --git a/samples/aspire/dab-config.cmd b/samples/aspire/dab-config.cmd
new file mode 100644
index 0000000000..6cc89703cc
--- /dev/null
+++ b/samples/aspire/dab-config.cmd
@@ -0,0 +1,66 @@
+@echo off
+
+del /q dab-config.json 2>nul
+
+set MSSQL_CONNECTION_STRING=%MSSQL_CONNECTION_STRING%
+
+dab init --database-type mssql --connection-string "@env('MSSQL_CONNECTION_STRING')" --config dab-config.json --host-mode development
+
+dab add Series --source dbo.Series --source.type table --permissions "anonymous:*" --description "Star Trek series"
+dab update Series --fields.name Id --fields.description "Primary key" --fields.primary-key true
+dab update Series --fields.name Name --fields.description "Series name" --fields.primary-key false
+
+dab add Actor --source dbo.Actor --source.type table --permissions "anonymous:*" --description "An actor in the franchise"
+dab update Actor --fields.name Id --fields.description "Primary key" --fields.primary-key true
+dab update Actor --fields.name FirstName --fields.description "Given name" --fields.primary-key false
+dab update Actor --fields.name LastName --fields.description "Family name" --fields.primary-key false
+dab update Actor --fields.name BirthYear --fields.description "Year of birth" --fields.primary-key false
+dab update Actor --fields.name FullName --fields.description "Computed full name" --fields.primary-key false
+
+dab add Species --source dbo.Species --source.type table --permissions "anonymous:*" --description "Alien species in Star Trek"
+dab update Species --fields.name Id --fields.description "Primary key" --fields.primary-key true
+dab update Species --fields.name Name --fields.description "Species name" --fields.primary-key false
+
+dab add Character --source dbo.Character --source.type table --permissions "anonymous:*" --description "A fictional character portrayed by an actor"
+dab update Character --fields.name Id --fields.description "Primary key" --fields.primary-key true
+dab update Character --fields.name Name --fields.description "Character name" --fields.primary-key false
+dab update Character --fields.name ActorId --fields.description "Foreign key to Actor" --fields.primary-key false
+dab update Character --fields.name Stardate --fields.description "Birth stardate" --fields.primary-key false
+
+dab add Series_Character --source dbo.Series_Character --source.type table --permissions "anonymous:*" --description "Characters appearing in series"
+dab update Series_Character --fields.name SeriesId --fields.description "Foreign key to Series" --fields.primary-key true
+dab update Series_Character --fields.name CharacterId --fields.description "Foreign key to Character" --fields.primary-key true
+dab update Series_Character --fields.name Role --fields.description "Character role in series" --fields.primary-key false
+
+dab add Character_Species --source dbo.Character_Species --source.type table --permissions "anonymous:*" --description "Species composition of characters"
+dab update Character_Species --fields.name CharacterId --fields.description "Foreign key to Character" --fields.primary-key true
+dab update Character_Species --fields.name SpeciesId --fields.description "Foreign key to Species" --fields.primary-key true
+
+dab update Character --relationship Character_Actor --target.entity Actor --cardinality one --relationship.fields "ActorId:Id"
+dab update Character --relationship Character_Series --cardinality many --target.entity Series_Character --relationship.fields "Id:CharacterId"
+dab update Character --relationship Character_Species --cardinality many --target.entity Character_Species --relationship.fields "Id:CharacterId"
+
+dab update Actor --relationship Actor_Characters --cardinality many --target.entity Character --relationship.fields "Id:ActorId"
+
+dab update Species --relationship Species_Characters --cardinality many --target.entity Character_Species --relationship.fields "Id:SpeciesId"
+
+dab update Series --relationship Series_Characters --cardinality many --target.entity Series_Character --relationship.fields "Id:SeriesId"
+
+dab update Series_Character --relationship SeriesCharacter_Series --cardinality one --target.entity Series --relationship.fields "SeriesId:Id"
+dab update Series_Character --relationship SeriesCharacter_Character --cardinality one --target.entity Character --relationship.fields "CharacterId:Id"
+
+dab update Character_Species --relationship CharacterSpecies_Character --cardinality one --target.entity Character --relationship.fields "CharacterId:Id"
+dab update Character_Species --relationship CharacterSpecies_Species --cardinality one --target.entity Species --relationship.fields "SpeciesId:Id"
+
+dab add SeriesActors --source dbo.SeriesActors --source.type view --source.key-fields "Id,SeriesId" --permissions "anonymous:*" --description "Actors appearing in each series"
+dab update SeriesActors --fields.name Id --fields.description "Actor id" --fields.primary-key true
+dab update SeriesActors --fields.name SeriesId --fields.description "Series id" --fields.primary-key true
+dab update SeriesActors --fields.name Actor --fields.description "Actor name" --fields.primary-key false
+dab update SeriesActors --fields.name BirthYear --fields.description "Year of birth" --fields.primary-key false
+dab update SeriesActors --fields.name Series --fields.description "Series name" --fields.primary-key false
+
+dab add GetSeriesActors --source dbo.GetSeriesActors --source.type stored-procedure --permissions "anonymous:*" --description "Return actors in a series"
+dab update GetSeriesActors --parameters.name seriesId --parameters.description "Series identifier" --parameters.required false --parameters.default 1
+dab update GetSeriesActors --parameters.name top --parameters.description "Limit rows" --parameters.required false --parameters.default 5
+
+echo DAB configuration complete!
diff --git a/samples/aspire/dab-config.json b/samples/aspire/dab-config.json
new file mode 100644
index 0000000000..0d3ea6f48b
--- /dev/null
+++ b/samples/aspire/dab-config.json
@@ -0,0 +1,534 @@
+{
+ "$schema": "https://github.com/Azure/data-api-builder/releases/download/v1.7.81/dab.draft.schema.json",
+ "data-source": {
+ "database-type": "mssql",
+ "connection-string": "@env('MSSQL_CONNECTION_STRING')",
+ "options": {
+ "set-session-context": false
+ }
+ },
+ "runtime": {
+ "rest": {
+ "enabled": true,
+ "path": "/api",
+ "request-body-strict": true
+ },
+ "graphql": {
+ "enabled": true,
+ "path": "/graphql",
+ "allow-introspection": true
+ },
+ "mcp": {
+ "enabled": true,
+ "path": "/mcp"
+ },
+ "host": {
+ "cors": {
+ "origins": [],
+ "allow-credentials": false
+ },
+ "authentication": {
+ "provider": "StaticWebApps"
+ },
+ "mode": "development"
+ }
+ },
+ "entities": {
+ "Series": {
+ "description": "Star Trek series",
+ "source": {
+ "object": "dbo.Series",
+ "type": "table"
+ },
+ "fields": [
+ {
+ "name": "Id",
+ "description": "Primary key",
+ "primary-key": true
+ },
+ {
+ "name": "Name",
+ "description": "Series name",
+ "primary-key": false
+ }
+ ],
+ "graphql": {
+ "enabled": true,
+ "type": {
+ "singular": "Series",
+ "plural": "Series"
+ }
+ },
+ "rest": {
+ "enabled": true
+ },
+ "permissions": [
+ {
+ "role": "anonymous",
+ "actions": [
+ {
+ "action": "*"
+ }
+ ]
+ }
+ ],
+ "relationships": {
+ "Series_Characters": {
+ "cardinality": "many",
+ "target.entity": "Series_Character",
+ "source.fields": [
+ "Id"
+ ],
+ "target.fields": [
+ "SeriesId"
+ ],
+ "linking.source.fields": [],
+ "linking.target.fields": []
+ }
+ }
+ },
+ "Actor": {
+ "description": "An actor in the franchise",
+ "source": {
+ "object": "dbo.Actor",
+ "type": "table"
+ },
+ "fields": [
+ {
+ "name": "Id",
+ "description": "Primary key",
+ "primary-key": true
+ },
+ {
+ "name": "FirstName",
+ "description": "Given name",
+ "primary-key": false
+ },
+ {
+ "name": "LastName",
+ "description": "Family name",
+ "primary-key": false
+ },
+ {
+ "name": "BirthYear",
+ "description": "Year of birth",
+ "primary-key": false
+ },
+ {
+ "name": "FullName",
+ "description": "Computed full name",
+ "primary-key": false
+ }
+ ],
+ "graphql": {
+ "enabled": true,
+ "type": {
+ "singular": "Actor",
+ "plural": "Actors"
+ }
+ },
+ "rest": {
+ "enabled": true
+ },
+ "permissions": [
+ {
+ "role": "anonymous",
+ "actions": [
+ {
+ "action": "*"
+ }
+ ]
+ }
+ ],
+ "relationships": {
+ "Actor_Characters": {
+ "cardinality": "many",
+ "target.entity": "Character",
+ "source.fields": [
+ "Id"
+ ],
+ "target.fields": [
+ "ActorId"
+ ],
+ "linking.source.fields": [],
+ "linking.target.fields": []
+ }
+ }
+ },
+ "Species": {
+ "description": "Alien species in Star Trek",
+ "source": {
+ "object": "dbo.Species",
+ "type": "table"
+ },
+ "fields": [
+ {
+ "name": "Id",
+ "description": "Primary key",
+ "primary-key": true
+ },
+ {
+ "name": "Name",
+ "description": "Species name",
+ "primary-key": false
+ }
+ ],
+ "graphql": {
+ "enabled": true,
+ "type": {
+ "singular": "Species",
+ "plural": "Species"
+ }
+ },
+ "rest": {
+ "enabled": true
+ },
+ "permissions": [
+ {
+ "role": "anonymous",
+ "actions": [
+ {
+ "action": "*"
+ }
+ ]
+ }
+ ],
+ "relationships": {
+ "Species_Characters": {
+ "cardinality": "many",
+ "target.entity": "Character_Species",
+ "source.fields": [
+ "Id"
+ ],
+ "target.fields": [
+ "SpeciesId"
+ ],
+ "linking.source.fields": [],
+ "linking.target.fields": []
+ }
+ }
+ },
+ "Character": {
+ "description": "A fictional character portrayed by an actor",
+ "source": {
+ "object": "dbo.Character",
+ "type": "table"
+ },
+ "fields": [
+ {
+ "name": "Id",
+ "description": "Primary key",
+ "primary-key": true
+ },
+ {
+ "name": "Name",
+ "description": "Character name",
+ "primary-key": false
+ },
+ {
+ "name": "ActorId",
+ "description": "Foreign key to Actor",
+ "primary-key": false
+ },
+ {
+ "name": "Stardate",
+ "description": "Birth stardate",
+ "primary-key": false
+ }
+ ],
+ "graphql": {
+ "enabled": true,
+ "type": {
+ "singular": "Character",
+ "plural": "Characters"
+ }
+ },
+ "rest": {
+ "enabled": true
+ },
+ "permissions": [
+ {
+ "role": "anonymous",
+ "actions": [
+ {
+ "action": "*"
+ }
+ ]
+ }
+ ],
+ "relationships": {
+ "Character_Actor": {
+ "cardinality": "one",
+ "target.entity": "Actor",
+ "source.fields": [
+ "ActorId"
+ ],
+ "target.fields": [
+ "Id"
+ ],
+ "linking.source.fields": [],
+ "linking.target.fields": []
+ },
+ "Character_Series": {
+ "cardinality": "many",
+ "target.entity": "Series_Character",
+ "source.fields": [
+ "Id"
+ ],
+ "target.fields": [
+ "CharacterId"
+ ],
+ "linking.source.fields": [],
+ "linking.target.fields": []
+ },
+ "Character_Species": {
+ "cardinality": "many",
+ "target.entity": "Character_Species",
+ "source.fields": [
+ "Id"
+ ],
+ "target.fields": [
+ "CharacterId"
+ ],
+ "linking.source.fields": [],
+ "linking.target.fields": []
+ }
+ }
+ },
+ "Series_Character": {
+ "description": "Characters appearing in series",
+ "source": {
+ "object": "dbo.Series_Character",
+ "type": "table"
+ },
+ "fields": [
+ {
+ "name": "SeriesId",
+ "description": "Foreign key to Series",
+ "primary-key": true
+ },
+ {
+ "name": "CharacterId",
+ "description": "Foreign key to Character",
+ "primary-key": true
+ },
+ {
+ "name": "Role",
+ "description": "Character role in series",
+ "primary-key": false
+ }
+ ],
+ "graphql": {
+ "enabled": true,
+ "type": {
+ "singular": "Series_Character",
+ "plural": "Series_Characters"
+ }
+ },
+ "rest": {
+ "enabled": true
+ },
+ "permissions": [
+ {
+ "role": "anonymous",
+ "actions": [
+ {
+ "action": "*"
+ }
+ ]
+ }
+ ],
+ "relationships": {
+ "SeriesCharacter_Series": {
+ "cardinality": "one",
+ "target.entity": "Series",
+ "source.fields": [
+ "SeriesId"
+ ],
+ "target.fields": [
+ "Id"
+ ],
+ "linking.source.fields": [],
+ "linking.target.fields": []
+ },
+ "SeriesCharacter_Character": {
+ "cardinality": "one",
+ "target.entity": "Character",
+ "source.fields": [
+ "CharacterId"
+ ],
+ "target.fields": [
+ "Id"
+ ],
+ "linking.source.fields": [],
+ "linking.target.fields": []
+ }
+ }
+ },
+ "Character_Species": {
+ "description": "Species composition of characters",
+ "source": {
+ "object": "dbo.Character_Species",
+ "type": "table"
+ },
+ "fields": [
+ {
+ "name": "CharacterId",
+ "description": "Foreign key to Character",
+ "primary-key": true
+ },
+ {
+ "name": "SpeciesId",
+ "description": "Foreign key to Species",
+ "primary-key": true
+ }
+ ],
+ "graphql": {
+ "enabled": true,
+ "type": {
+ "singular": "Character_Species",
+ "plural": "Character_Species"
+ }
+ },
+ "rest": {
+ "enabled": true
+ },
+ "permissions": [
+ {
+ "role": "anonymous",
+ "actions": [
+ {
+ "action": "*"
+ }
+ ]
+ }
+ ],
+ "relationships": {
+ "CharacterSpecies_Character": {
+ "cardinality": "one",
+ "target.entity": "Character",
+ "source.fields": [
+ "CharacterId"
+ ],
+ "target.fields": [
+ "Id"
+ ],
+ "linking.source.fields": [],
+ "linking.target.fields": []
+ },
+ "CharacterSpecies_Species": {
+ "cardinality": "one",
+ "target.entity": "Species",
+ "source.fields": [
+ "SpeciesId"
+ ],
+ "target.fields": [
+ "Id"
+ ],
+ "linking.source.fields": [],
+ "linking.target.fields": []
+ }
+ }
+ },
+ "SeriesActors": {
+ "description": "Actors appearing in each series",
+ "source": {
+ "object": "dbo.SeriesActors",
+ "type": "view"
+ },
+ "fields": [
+ {
+ "name": "Id",
+ "description": "Actor id",
+ "primary-key": true
+ },
+ {
+ "name": "SeriesId",
+ "description": "Series id",
+ "primary-key": true
+ },
+ {
+ "name": "Actor",
+ "description": "Actor name",
+ "primary-key": false
+ },
+ {
+ "name": "BirthYear",
+ "description": "Year of birth",
+ "primary-key": false
+ },
+ {
+ "name": "Series",
+ "description": "Series name",
+ "primary-key": false
+ }
+ ],
+ "graphql": {
+ "enabled": true,
+ "type": {
+ "singular": "SeriesActors",
+ "plural": "SeriesActors"
+ }
+ },
+ "rest": {
+ "enabled": true
+ },
+ "permissions": [
+ {
+ "role": "anonymous",
+ "actions": [
+ {
+ "action": "*"
+ }
+ ]
+ }
+ ]
+ },
+ "GetSeriesActors": {
+ "description": "Return actors in a series",
+ "source": {
+ "object": "dbo.GetSeriesActors",
+ "type": "stored-procedure",
+ "parameters": [
+ {
+ "name": "top",
+ "description": "Limit rows",
+ "required": false,
+ "default": "5"
+ },
+ {
+ "name": "seriesId",
+ "description": "Series identifier",
+ "required": false,
+ "default": "1"
+ }
+ ]
+ },
+ "fields": [],
+ "graphql": {
+ "enabled": true,
+ "operation": "mutation",
+ "type": {
+ "singular": "GetSeriesActors",
+ "plural": "GetSeriesActors"
+ }
+ },
+ "rest": {
+ "enabled": true,
+ "methods": [
+ "post"
+ ]
+ },
+ "permissions": [
+ {
+ "role": "anonymous",
+ "actions": [
+ {
+ "action": "*"
+ }
+ ]
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/aspire/database.sql b/samples/aspire/database.sql
new file mode 100644
index 0000000000..ca17dadb99
--- /dev/null
+++ b/samples/aspire/database.sql
@@ -0,0 +1,465 @@
+USE [Master]
+
+GO
+IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'StarTrek')
+BEGIN
+ CREATE DATABASE StarTrek;
+END
+
+GO
+USE [StarTrek]
+
+GO
+IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Actor')
+BEGIN
+ RETURN; -- assume all tables exist
+END
+
+GO
+-- Drop tables in reverse order of creation due to foreign key dependencies
+DROP TABLE IF EXISTS Character_Species;
+DROP TABLE IF EXISTS Series_Character;
+DROP TABLE IF EXISTS Character;
+DROP TABLE IF EXISTS Species;
+DROP TABLE IF EXISTS Actor;
+DROP TABLE IF EXISTS Series;
+DROP VIEW IF EXISTS SeriesActors;
+DROP PROC IF EXISTS GetSeriesActors;
+
+GO
+-- create tables
+CREATE TABLE Series (
+ Id INT PRIMARY KEY,
+ Name NVARCHAR(255) NOT NULL
+);
+
+GO
+CREATE TABLE Actor (
+ Id INT PRIMARY KEY,
+ FirstName NVARCHAR(100) NOT NULL,
+ LastName NVARCHAR(100) NOT NULL,
+ [BirthYear] INT NOT NULL,
+ FullName AS (RTRIM(LTRIM(FirstName + ' ' + LastName))) PERSISTED
+);
+
+GO
+CREATE TABLE Species (
+ Id INT PRIMARY KEY,
+ Name NVARCHAR(255) NOT NULL
+);
+
+GO
+CREATE TABLE Character (
+ Id INT PRIMARY KEY,
+ Name NVARCHAR(255) NOT NULL,
+ ActorId INT NOT NULL,
+ Stardate DECIMAL(10, 2),
+ FOREIGN KEY (ActorId) REFERENCES Actor(Id)
+);
+
+GO
+CREATE TABLE Series_Character (
+ SeriesId INT,
+ CharacterId INT,
+ Role VARCHAR(500),
+ FOREIGN KEY (SeriesId) REFERENCES Series(Id),
+ FOREIGN KEY (CharacterId) REFERENCES Character(Id),
+ PRIMARY KEY (SeriesId, CharacterId)
+);
+
+GO
+CREATE TABLE Character_Species (
+ CharacterId INT,
+ SpeciesId INT,
+ FOREIGN KEY (CharacterId) REFERENCES Character(Id),
+ FOREIGN KEY (SpeciesId) REFERENCES Species(Id),
+ PRIMARY KEY (CharacterId, SpeciesId)
+);
+
+GO
+-- create data
+INSERT INTO Series (Id, Name) VALUES
+ (1, 'Star Trek'),
+ (2, 'Star Trek: The Next Generation'),
+ (3, 'Star Trek: Voyager'),
+ (4, 'Star Trek: Deep Space Nine'),
+ (5, 'Star Trek: Enterprise'),
+ (6, 'Star Trek: Discovery'),
+ (7, 'Star Trek: Strange New Worlds'),
+ (8, 'Star Trek: Lower Decks'),
+ (9, 'Star Trek: Prodigy'),
+ (10, 'Star Trek: Starfleet Academy'),
+ (11, 'Star Trek: Section 31');
+
+GO
+INSERT INTO Species (Id, Name) VALUES
+ (1, 'Human'),
+ (2, 'Vulcan'),
+ (3, 'Android'),
+ (4, 'Klingon'),
+ (5, 'Betazoid'),
+ (6, 'Hologram'),
+ (7, 'Bajoran'),
+ (8, 'Changeling'),
+ (9, 'Trill'),
+ (10, 'Ferengi'),
+ (11, 'Denobulan'),
+ (12, 'Borg'),
+ (13, 'Kelpien'),
+ (14, 'Illyrian'),
+ (15, 'Brikar'),
+ (16, 'Medusan'),
+ (17, 'Vau N''Akat'),
+ (18, 'Deltan'),
+ (19, 'Chameloid'),
+ (20, 'Nanokin');
+
+GO
+INSERT INTO Actor (Id, FirstName, LastName, [BirthYear]) VALUES
+ (1, 'William', 'Shatner', 1931),
+ (2, 'Leonard', 'Nimoy', 1931),
+ (3, 'DeForest', 'Kelley', 1920),
+ (4, 'James', 'Doohan', 1920),
+ (5, 'Nichelle', 'Nichols', 1932),
+ (6, 'George', 'Takei', 1937),
+ (7, 'Walter', 'Koenig', 1936),
+ (8, 'Patrick', 'Stewart', 1940),
+ (9, 'Jonathan', 'Frakes', 1952),
+ (10, 'Brent', 'Spiner', 1949),
+ (11, 'Michael', 'Dorn', 1952),
+ (12, 'Gates', 'McFadden', 1949),
+ (13, 'Marina', 'Sirtis', 1955),
+ (14, 'LeVar', 'Burton', 1957),
+ (15, 'Kate', 'Mulgrew', 1955),
+ (16, 'Robert', 'Beltran', 1953),
+ (17, 'Tim', 'Russ', 1956),
+ (18, 'Roxann', 'Dawson', 1958),
+ (19, 'Robert', 'Duncan McNeill', 1964),
+ (20, 'Garrett', 'Wang', 1968),
+ (21, 'Robert', 'Picardo', 1953),
+ (22, 'Jeri', 'Ryan', 1968),
+ (23, 'Avery', 'Brooks', 1948),
+ (24, 'Nana', 'Visitor', 1957),
+ (25, 'Rene', 'Auberjonois', 1940),
+ (26, 'Terry', 'Farrell', 1963),
+ (27, 'Alexander', 'Siddig', 1965),
+ (28, 'Armin', 'Shimerman', 1949),
+ (29, 'Cirroc', 'Lofton', 1978),
+ (30, 'Scott', 'Bakula', 1954),
+ (31, 'Jolene', 'Blalock', 1975),
+ (32, 'John', 'Billingsley', 1960),
+ (33, 'Connor', 'Trinneer', 1969),
+ (34, 'Dominic', 'Keating', 1962),
+ (35, 'Linda', 'Park', 1978),
+ (36, 'Anthony', 'Montgomery', 1971),
+ (37, 'Sonequa', 'Martin-Green', 1985),
+ (38, 'Doug', 'Jones', 1960),
+ (39, 'Anthony', 'Rapp', 1971),
+ (40, 'Mary', 'Wiseman', 1985),
+ (41, 'Wilson', 'Cruz', 1974),
+ -- Strange New Worlds cast
+ (42, 'Anson', 'Mount', 1973),
+ (43, 'Ethan', 'Peck', 1986),
+ (44, 'Rebecca', 'Romijn', 1972),
+ (45, 'Celia', 'Rose Gooding', 1999),
+ (46, 'Jess', 'Bush', 1991),
+ -- Lower Decks cast
+ (47, 'Tawny', 'Newsome', 1983),
+ (48, 'Jack', 'Quaid', 1992),
+ (49, 'Noel', 'Wells', 1986),
+ (50, 'Eugene', 'Cordero', 1986),
+ (51, 'Dawnn', 'Lewis', 1961),
+ -- Prodigy cast
+ (52, 'Brett', 'Gray', 1996),
+ (53, 'Ella', 'Purnell', 1996),
+ (54, 'Jason', 'Mantzoukas', 1972),
+ (55, 'Angus', 'Imrie', 1994),
+ (56, 'Dee Bradley', 'Baker', 1962),
+ -- Starfleet Academy cast
+ (57, 'Holly', 'Hunter', 1958),
+ (58, 'Paul', 'Giamatti', 1967),
+ (59, 'Kerrice', 'Brooks', 1990),
+ (60, 'Bella', 'Shepard', 2005),
+ (61, 'George', 'Hawkins', 1999),
+ -- Section 31 cast
+ (62, 'Michelle', 'Yeoh', 1962),
+ (63, 'Omari', 'Hardwick', 1974),
+ (64, 'Sam', 'Richardson', 1984),
+ (65, 'Robert', 'Kazinsky', 1983),
+ (66, 'Kacey', 'Rohl', 1991);
+
+INSERT INTO Character (Id, Name, ActorId, Stardate) VALUES
+ (1, 'James T. Kirk', 1, 2233.04),
+ (2, 'Spock', 2, 2230.06),
+ (3, 'Leonard McCoy', 3, 2227.00),
+ (4, 'Montgomery Scott', 4, 2222.00),
+ (5, 'Uhura', 5, 2233.00),
+ (6, 'Hikaru Sulu', 6, 2237.00),
+ (7, 'Pavel Chekov', 7, 2245.00),
+ (8, 'Jean-Luc Picard', 8, 2305.07),
+ (9, 'William Riker', 9, 2335.08),
+ (10, 'Data', 10, 2336.00),
+ (11, 'Worf', 11, 2340.00),
+ (12, 'Beverly Crusher', 12, 2324.00),
+ (13, 'Deanna Troi', 13, 2336.00),
+ (14, 'Geordi La Forge', 14, 2335.02),
+ (15, 'Kathryn Janeway', 15, 2336.05),
+ (16, 'Chakotay', 16, 2329.00),
+ (17, 'Tuvok', 17, 2264.00),
+ (18, 'B''Elanna Torres', 18, 2349.00),
+ (19, 'Tom Paris', 19, 2346.00),
+ (20, 'Harry Kim', 20, 2349.00),
+ (21, 'The Doctor', 21, 2371.00),
+ (22, 'Seven of Nine', 22, 2348.00),
+ (23, 'Benjamin Sisko', 23, 2332.00),
+ (24, 'Kira Nerys', 24, 2343.00),
+ (25, 'Odo', 25, 2337.00),
+ (26, 'Ezri Dax', 26, 2368.00),
+ (27, 'Jadzia Dax', 26, 2341.00),
+ (28, 'Julian Bashir', 27, 2341.00),
+ (29, 'Quark', 28, 2333.00),
+ (30, 'Jake Sisko', 29, 2355.00),
+ (31, 'Jonathan Archer', 30, 2112.00),
+ (32, 'T''Pol', 31, 2088.00),
+ (33, 'Phlox', 32, 2102.00),
+ (34, 'Charles "Trip" Tucker III', 33, 2121.00),
+ (35, 'Malcolm Reed', 34, 2117.00),
+ (36, 'Hoshi Sato', 35, 2129.00),
+ (37, 'Travis Mayweather', 36, 2126.00),
+ (38, 'Michael Burnham', 37, 2226.00),
+ (39, 'Saru', 38, 2237.00),
+ (40, 'Paul Stamets', 39, 2246.00),
+ (41, 'Sylvia Tilly', 40, 2249.00),
+ (42, 'Hugh Culber', 41, 2246.00),
+ -- Strange New Worlds characters
+ (43, 'Christopher Pike', 42, 2219.00),
+ (44, 'Spock (SNW)', 43, 2230.06),
+ (45, 'Una Chin-Riley', 44, 2220.00),
+ (46, 'Nyota Uhura (SNW)', 45, 2233.00),
+ (47, 'Christine Chapel', 46, 2230.00),
+ -- Lower Decks characters
+ (48, 'Beckett Mariner', 47, 2357.00),
+ (49, 'Brad Boimler', 48, 2361.00),
+ (50, 'D''Vana Tendi', 49, 2368.00),
+ (51, 'Sam Rutherford', 50, 2359.00),
+ (52, 'Carol Freeman', 51, 2337.00),
+ -- Prodigy characters
+ (53, 'Dal R''El', 52, 2369.00),
+ (54, 'Gwyndala', 53, 2369.00),
+ (55, 'Jankom Pog', 54, 2368.00),
+ (56, 'Zero', 55, 2370.00),
+ (57, 'Murf', 56, 2371.00),
+ -- Starfleet Academy characters
+ (58, 'Admiral Grace', 57, 2328.00),
+ (59, 'Chancellor Vix', 58, 2335.00),
+ (60, 'Nyota Uhura (Academy)', 59, 2233.00),
+ (61, 'Cadet Sato', 60, 2377.00),
+ (62, 'Cadet Thira Sidhu', 61, 2378.00),
+ -- Section 31 characters
+ (63, 'Philippa Georgiou (Section 31)', 62, 2202.00),
+ (64, 'Alok Sahar', 63, 1970.00),
+ (65, 'Quasi', 64, 2335.00),
+ (66, 'Zeph', 65, 2330.00),
+ (67, 'Rachel Garrett (Young)', 66, 2305.00);
+
+INSERT INTO Series_Character (SeriesId, CharacterId, Role) VALUES
+ -- Star Trek (Original Series)
+ (1, 1, 'Captain'),
+ (1, 2, 'First Officer/Science Officer'),
+ (1, 3, 'Chief Medical Officer'),
+ (1, 4, 'Chief Engineer'),
+ (1, 5, 'Communications Officer'),
+ (1, 6, 'Helmsman'),
+ (1, 7, 'Navigator'),
+ -- Star Trek: The Next Generation
+ (2, 8, 'Captain'),
+ (2, 9, 'First Officer'),
+ (2, 10, 'Operations Officer'),
+ (2, 11, 'Chief of Security/Tactical Officer'),
+ (2, 12, 'Chief Medical Officer'),
+ (2, 13, 'Ship''s Counselor'),
+ (2, 14, 'Chief Engineer'),
+ -- Star Trek: Voyager
+ (3, 15, 'Captain'),
+ (3, 16, 'First Officer'),
+ (3, 17, 'Chief of Security/Tactical Officer'),
+ (3, 18, 'Chief Engineer'),
+ (3, 19, 'Helmsman'),
+ (3, 20, 'Operations Officer'),
+ (3, 21, 'Chief Medical Officer'),
+ (3, 22, 'Astrometrics Officer'),
+ -- Star Trek: Deep Space Nine
+ (4, 23, 'Commanding Officer'),
+ (4, 24, 'First Officer'),
+ (4, 25, 'Chief of Security'),
+ (4, 26, 'Science Officer'),
+ (4, 27, 'Science Officer'),
+ (4, 28, 'Chief Medical Officer'),
+ (4, 29, 'Bar Owner'),
+ (4, 30, 'Civilian'),
+ (4, 11, 'Strategic Operations Officer'), -- Worf also served on DS9
+ -- Star Trek: Enterprise
+ (5, 31, 'Captain'),
+ (5, 32, 'Science Officer/First Officer'),
+ (5, 33, 'Chief Medical Officer'),
+ (5, 34, 'Chief Engineer'),
+ (5, 35, 'Armory Officer'),
+ (5, 36, 'Communications Officer'),
+ (5, 37, 'Helmsman'),
+ -- Star Trek: Discovery
+ (6, 38, 'Science Specialist/First Officer'),
+ (6, 39, 'First Officer/Captain'),
+ (6, 40, 'Chief Engineer'),
+ (6, 41, 'Ensign/Cadet'),
+ (6, 42, 'Chief Medical Officer'),
+ -- Star Trek: Strange New Worlds
+ (7, 43, 'Captain'),
+ (7, 44, 'Science Officer'),
+ (7, 45, 'First Officer'),
+ (7, 46, 'Communications Officer'),
+ (7, 47, 'Nurse'),
+ -- Star Trek: Lower Decks
+ (8, 48, 'Ensign'),
+ (8, 49, 'Ensign'),
+ (8, 50, 'Ensign'),
+ (8, 51, 'Ensign'),
+ (8, 52, 'Captain'),
+ -- Star Trek: Prodigy
+ (9, 53, 'Crew Member'),
+ (9, 54, 'Crew Member'),
+ (9, 55, 'Crew Member'),
+ (9, 56, 'Crew Member'),
+ (9, 57, 'Crew Member'),
+ -- Star Trek: Starfleet Academy
+ (10, 58, 'Academy Commandant'),
+ (10, 59, 'Academy Chancellor'),
+ (10, 60, 'Cadet/Communications Specialist'),
+ (10, 61, 'First Year Cadet'),
+ (10, 62, 'First Year Cadet'),
+ -- Star Trek: Section 31
+ (11, 63, 'Emperor/Section 31 Operative'),
+ (11, 64, 'Section 31 Agent'),
+ (11, 65, 'Section 31 Agent/Shapeshifter'),
+ (11, 66, 'Section 31 Agent'),
+ (11, 67, 'Starfleet Officer/Future Captain');
+
+INSERT INTO Character_Species (CharacterId, SpeciesId) VALUES
+ -- Original Series
+ (1, 1), -- Kirk: Human
+ (2, 2), -- Spock: Vulcan
+ (2, 1), -- Spock: Human (half-human)
+ (3, 1), -- McCoy: Human
+ (4, 1), -- Scott: Human
+ (5, 1), -- Uhura: Human
+ (6, 1), -- Sulu: Human
+ (7, 1), -- Chekov: Human
+ -- Next Generation
+ (8, 1), -- Picard: Human
+ (9, 1), -- Riker: Human
+ (10, 3), -- Data: Android
+ (11, 4), -- Worf: Klingon
+ (12, 1), -- Crusher: Human
+ (13, 5), -- Troi: Betazoid
+ (13, 1), -- Troi: Human (half-human)
+ (14, 1), -- La Forge: Human
+ -- Voyager
+ (15, 1), -- Janeway: Human
+ (16, 1), -- Chakotay: Human
+ (17, 2), -- Tuvok: Vulcan
+ (18, 4), -- Torres: Klingon
+ (18, 1), -- Torres: Human (half-human)
+ (19, 1), -- Paris: Human
+ (20, 1), -- Kim: Human
+ (21, 6), -- The Doctor: Hologram
+ (22, 1), -- Seven of Nine: Human
+ (22, 12), -- Seven of Nine: Borg (ex-Borg)
+ -- Deep Space Nine
+ (23, 1), -- Sisko: Human
+ (24, 7), -- Kira: Bajoran
+ (25, 8), -- Odo: Changeling
+ (26, 1), -- Ezri Dax: Human
+ (26, 9), -- Ezri Dax: Trill (joined)
+ (27, 9), -- Jadzia Dax: Trill
+ (28, 1), -- Bashir: Human
+ (29, 10), -- Quark: Ferengi
+ (30, 1), -- Jake Sisko: Human
+ -- Enterprise
+ (31, 1), -- Archer: Human
+ (32, 2), -- T'Pol: Vulcan
+ (33, 11), -- Phlox: Denobulan
+ (34, 1), -- Tucker: Human
+ (35, 1), -- Reed: Human
+ (36, 1), -- Sato: Human
+ (37, 1), -- Mayweather: Human
+ -- Discovery
+ (38, 1), -- Burnham: Human
+ (39, 13), -- Saru: Kelpien
+ (40, 1), -- Stamets: Human
+ (41, 1), -- Tilly: Human
+ (42, 1), -- Culber: Human
+ -- Strange New Worlds
+ (43, 1), -- Pike: Human
+ (44, 2), -- Spock: Vulcan
+ (44, 1), -- Spock: Human (half-human)
+ (45, 1), -- Una: Human
+ (45, 14), -- Una: Illyrian (augmented)
+ (46, 1), -- Uhura: Human
+ (47, 1), -- Chapel: Human
+ -- Lower Decks
+ (48, 1), -- Mariner: Human
+ (49, 1), -- Boimler: Human
+ (50, 1), -- Tendi: Human (Orion)
+ (51, 1), -- Rutherford: Human
+ (52, 1), -- Freeman: Human
+ -- Prodigy
+ (53, 1), -- Dal: Human (augment)
+ (54, 17), -- Gwyndala: Vau N'Akat
+ (55, 1), -- Jankom Pog: Human (Tellarite)
+ (56, 16), -- Zero: Medusan
+ (57, 1), -- Murf: Unknown (listed as Human for database purposes)
+ -- Starfleet Academy
+ (58, 1), -- Admiral Grace: Human
+ (59, 1), -- Chancellor Vix: Human
+ (60, 1), -- Uhura (Academy): Human
+ (61, 1), -- Sato: Human
+ (62, 1), -- Thira Sidhu: Human
+ -- Section 31
+ (63, 1), -- Philippa Georgiou: Human
+ (64, 1), -- Alok Sahar: Human
+ (65, 19), -- Quasi: Chameloid
+ (66, 1), -- Zeph: Human
+ (67, 1); -- Rachel Garrett: Human
+
+GO
+CREATE VIEW [dbo].[SeriesActors]
+AS
+SELECT
+ a.Id AS Id,
+ a.FullName AS Actor,
+ a.BirthYear AS BirthYear,
+ s.Id AS SeriesId,
+ s.Name AS Series
+FROM Series s
+JOIN Series_Character AS sc ON s.Id = sc.SeriesId
+JOIN Character AS c ON sc.CharacterId = c.Id
+JOIN Actor AS a ON c.ActorId = a.Id;
+
+GO
+CREATE PROCEDURE [dbo].[GetSeriesActors]
+ @seriesId INT = 1,
+ @top INT = 5
+AS
+SET NOCOUNT ON;
+SELECT TOP (@top) *
+FROM SeriesActors
+WHERE SeriesId = @seriesId;
+
+GO
+-- demo indexes for performance
+CREATE INDEX IX_Character_ActorId ON Character(ActorId);
+
+GO
+CREATE INDEX IX_SeriesCharacter_SeriesId ON Series_Character(SeriesId);
+
+GO
+CREATE INDEX IX_CharacterSpecies_CharacterId ON Character_Species(CharacterId);
diff --git a/samples/aspire/global.json b/samples/aspire/global.json
new file mode 100644
index 0000000000..376af49c07
--- /dev/null
+++ b/samples/aspire/global.json
@@ -0,0 +1,5 @@
+{
+ "sdk": {
+ "version": "10.0.100"
+ }
+}
diff --git a/samples/aspire/readme.md b/samples/aspire/readme.md
new file mode 100644
index 0000000000..ff3157624f
--- /dev/null
+++ b/samples/aspire/readme.md
@@ -0,0 +1,66 @@
+# Sample: Data API builder in Aspire
+
+To run this sample, install the prerequisites, then execute the following command in this directory:
+
+```sh
+> aspire run
+```
+
+### Prerequisites
+
+ - Dotnet CLI v10+ [Install](https://dotnet.microsoft.com/en-us/download/dotnet) Verify with `dotnet --version`
+ - Aspire CLI v13+ [Install](https://aspire.dev/get-started/install-cli/) Verify with `aspire --version`
+ - Docker Desktop (running) [Install](https://www.docker.com/products/docker-desktop/)
+
+## Resources
+
+```mermaid
+flowchart TD
+ SQL["SQL Server"]
+ DAB["Data API Builder"]
+ CMD["SQL Commander"]
+ MCP["MCP Inspector"]
+
+ SQL --> DAB
+ SQL --> CMD
+ DAB --> MCP
+```
+
+## Files
+
+- [`apphost.cs`](apphost.cs) - Single-page [Aspire orchestration](https://learn.microsoft.com/en-us/dotnet/aspire/app-host/configuration)
+- [`database.sql`](database.sql) - Basic Star Trek schema & seed data
+- [`dab-config.json`](dab-config.json) - Standard DAB configuration file
+- [`dab-config.cmd`](dab-config.cmd) - [CLI commands](https://learn.microsoft.com/en-us/azure/data-api-builder/command-line/) to create `dab-config.json` (reference only)
+- [`global.json`](global.json) - .NET SDK version settings
+
+## Data structures
+
+This schema demonstrates all of the relationship cardinalities.
+
+```mermaid
+stateDiagram-v2
+direction LR
+
+ classDef empty fill:none,stroke:none
+ classDef table stroke:black;
+ classDef phantom stroke:gray,stroke-dasharray:5 5;
+
+
+ class Actor table
+ class Character table
+ class Series table
+ class Species table
+ class Series_Character table
+ class Character_Species table
+ class Series_Character phantom
+ class Character_Species phantom
+ state Tables {
+ Actor --> Character
+ Character --> Actor
+ Series_Character --> Series
+ Character_Species --> Species
+ Series_Character --> Character
+ Character_Species --> Character
+ }
+```
diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json
index 80cfd953ad..920c0a4da6 100644
--- a/schemas/dab.draft.schema.json
+++ b/schemas/dab.draft.schema.json
@@ -41,8 +41,8 @@
"additionalProperties": false,
"properties": {
"enabled": {
- "type": "boolean",
- "description": "Enable health check endpoint",
+ "$ref": "#/$defs/boolean-or-string",
+ "description": "Enable health check endpoint for something",
"default": true,
"additionalProperties": false
},
@@ -186,7 +186,7 @@
"type": "string"
},
"enabled": {
- "type": "boolean",
+ "$ref": "#/$defs/boolean-or-string",
"description": "Allow enabling/disabling REST requests for all entities."
},
"request-body-strict": {
@@ -210,7 +210,7 @@
"type": "string"
},
"enabled": {
- "type": "boolean",
+ "$ref": "#/$defs/boolean-or-string",
"description": "Allow enabling/disabling GraphQL requests for all entities."
},
"depth-limit": {
@@ -370,7 +370,7 @@
"description": "Custom authentication provider defined by the user. Use the JWT property to configure the custom provider."
}
],
- "default": "StaticWebApps"
+ "default": "AppService"
},
"jwt": {
"type": "object",
@@ -438,7 +438,7 @@
"description": "Application Insights connection string"
},
"enabled": {
- "type": "boolean",
+ "$ref": "#/$defs/boolean-or-string",
"description": "Allow enabling/disabling Application Insights telemetry.",
"default": true
}
@@ -481,7 +481,7 @@
"additionalProperties": false,
"properties": {
"enabled": {
- "type": "boolean",
+ "$ref": "#/$defs/boolean-or-string",
"description": "Allow enabling/disabling Azure Log Analytics.",
"default": false
},
@@ -618,7 +618,7 @@
"additionalProperties": false,
"properties": {
"enabled": {
- "type": "boolean",
+ "$ref": "#/$defs/boolean-or-string",
"description": "Enable health check endpoint globally",
"default": true,
"additionalProperties": false
@@ -693,9 +693,169 @@
}
}
},
+ "autoentities": {
+ "type": "object",
+ "description": "Defines automatic entity generation rules for MSSQL tables based on include/exclude patterns and defaults.",
+ "patternProperties": {
+ "^.*$": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "patterns": {
+ "type": "object",
+ "description": "Pattern matching rules for including/excluding database objects",
+ "additionalProperties": false,
+ "properties": {
+ "include": {
+ "type": "array",
+ "description": "MSSQL LIKE pattern for objects to include (e.g., '%.%'). Null includes all.",
+ "items": {
+ "type": "string"
+ },
+ "default": [ "%.%" ]
+ },
+ "exclude": {
+ "type": "array",
+ "description": "MSSQL LIKE pattern for objects to exclude (e.g., 'sales.%'). Null excludes none.",
+ "items": {
+ "type": "string"
+ },
+ "default": null
+ },
+ "name": {
+ "type": "string",
+ "description": "Entity name interpolation pattern using {schema} and {object}. Null defaults to {object}. Must be unique for every entity inside the pattern",
+ "default": "{object}"
+ }
+ }
+ },
+ "template": {
+ "type": "object",
+ "description": "Template configuration for generated entities",
+ "additionalProperties": false,
+ "properties": {
+ "mcp": {
+ "type": "object",
+ "description": "MCP endpoint configuration",
+ "additionalProperties": false,
+ "properties": {
+ "dml-tools": {
+ "type": "boolean",
+ "description": "Enable/disable all DML tools with default settings."
+ }
+ }
+ },
+ "rest": {
+ "type": "object",
+ "description": "REST endpoint configuration",
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean",
+ "description": "Enable/disable REST endpoint",
+ "default": true
+ }
+ }
+ },
+ "graphql": {
+ "type": "object",
+ "description": "GraphQL endpoint configuration",
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean",
+ "description": "Enable/disable GraphQL endpoint",
+ "default": true
+ }
+ }
+ },
+ "health": {
+ "type": "object",
+ "description": "Health check configuration",
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean",
+ "description": "Enable/disable health check endpoint",
+ "default": true
+ }
+ }
+ },
+ "cache": {
+ "type": "object",
+ "description": "Cache configuration",
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean",
+ "description": "Enable/disable caching",
+ "default": false
+ },
+ "ttl-seconds": {
+ "type": [ "integer", "null" ],
+ "description": "Time-to-live for cached responses in seconds",
+ "default": null,
+ "minimum": 1
+ },
+ "level": {
+ "type": "string",
+ "description": "Cache level (L1 or L1L2)",
+ "enum": [ "L1", "L1L2", null ],
+ "default": "L1L2"
+ }
+ }
+ }
+ }
+ },
+ "permissions": {
+ "type": "array",
+ "description": "Permissions assigned to this object",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "role": {
+ "type": "string"
+ },
+ "actions": {
+ "oneOf": [
+ {
+ "type": "string",
+ "pattern": "[*]"
+ },
+ {
+ "type": "array",
+ "items": {
+ "oneOf": [
+ {
+ "$ref": "#/$defs/action"
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "action": {
+ "$ref": "#/$defs/action"
+ }
+ }
+ }
+ ]
+ },
+ "uniqueItems": true
+ }
+ ]
+ }
+ }
+ },
+ "required": [ "role", "actions" ]
+ }
+ }
+ }
+ }
+ },
"entities": {
"type": "object",
- "description": "Entities that will be exposed via REST and/or GraphQL",
+ "description": "Entities that will be exposed via REST, GraphQL and/or MCP",
"patternProperties": {
"^.*$": {
"type": "object",
@@ -961,6 +1121,31 @@
"default": 5
}
}
+ },
+ "mcp": {
+ "oneOf": [
+ {
+ "type": "boolean",
+ "description": "Boolean shorthand: true enables dml-tools only (custom-tool remains false), false disables all MCP functionality."
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "dml-tools": {
+ "type": "boolean",
+ "description": "Enable MCP DML (Data Manipulation Language) tools for this entity. Allows CRUD operations via MCP.",
+ "default": true
+ },
+ "custom-tool": {
+ "type": "boolean",
+ "description": "Enable MCP custom tool for this entity. Only valid for stored procedures.",
+ "default": false
+ }
+ }
+ }
+ ],
+ "description": "Model Context Protocol (MCP) configuration for this entity. Controls whether the entity is exposed via MCP tools."
}
},
"if": {
@@ -1145,6 +1330,33 @@
]
}
}
+ },
+ {
+ "if": {
+ "properties": {
+ "mcp": {
+ "properties": {
+ "custom-tool": {
+ "const": true
+ }
+ }
+ }
+ },
+ "required": ["mcp"]
+ },
+ "then": {
+ "properties": {
+ "source": {
+ "properties": {
+ "type": {
+ "const": "stored-procedure"
+ }
+ },
+ "required": ["type"]
+ }
+ },
+ "errorMessage": "custom-tool can only be enabled for entities with source type 'stored-procedure'."
+ }
}
]
}
@@ -1179,7 +1391,15 @@
"type": "string"
}
},
- "required": ["singular"]
+ "required": [ "singular" ]
+ }
+ ]
+ },
+ "boolean-or-string": {
+ "oneOf":[
+ {
+ "type": [ "boolean", "string" ],
+ "pattern": "^(?:true|false|1|0|@env\\('.*'\\)|@akv\\('.*'\\))$"
}
]
},
diff --git a/src/.editorconfig b/src/.editorconfig
index 5639a063ae..6367bdcb3c 100644
--- a/src/.editorconfig
+++ b/src/.editorconfig
@@ -75,9 +75,9 @@ dotnet_style_allow_statement_immediately_after_block_experimental = false:error
#### C# Coding Conventions ####
# var preferences
-csharp_style_var_elsewhere = false:error
-csharp_style_var_for_built_in_types = false:error
-csharp_style_var_when_type_is_apparent = false:error
+# csharp_style_var_elsewhere = false:error
+# csharp_style_var_for_built_in_types = false:error
+# csharp_style_var_when_type_is_apparent = false:error
# Modifier preferences
csharp_prefer_static_local_function = true:suggestion
diff --git a/src/Aspire.AppHost/AppHost.cs b/src/Aspire.AppHost/AppHost.cs
new file mode 100644
index 0000000000..5477c5c81b
--- /dev/null
+++ b/src/Aspire.AppHost/AppHost.cs
@@ -0,0 +1,95 @@
+using Microsoft.Extensions.DependencyInjection;
+
+var builder = DistributedApplication.CreateBuilder(args);
+
+var aspireDB = Environment.GetEnvironmentVariable("ASPIRE_DATABASE_TYPE");
+
+var databaseConnectionString = Environment.GetEnvironmentVariable("ASPIRE_DATABASE_CONNECTION_STRING") ?? "";
+
+switch (aspireDB)
+{
+ case "mssql":
+ var sqlScript = File.ReadAllText("./init-scripts/sql/create-database.sql");
+
+ IResourceBuilder? sqlDbContainer = null;
+
+ if (string.IsNullOrEmpty(databaseConnectionString))
+ {
+ Console.WriteLine("No connection string provided, starting a local SQL Server container.");
+
+ sqlDbContainer = builder.AddSqlServer("sqlserver")
+ .WithDataVolume()
+ .WithLifetime(ContainerLifetime.Persistent)
+ .AddDatabase("msSqlDb", "Trek")
+ .WithCreationScript(sqlScript);
+ }
+
+ var mssqlService = builder.AddProject("mssql-service", "Development")
+ .WithArgs("-f", "net8.0")
+ .WithEndpoint(endpointName: "https", (e) => e.Port = 1234)
+ .WithEndpoint(endpointName: "http", (e) => e.Port = 2345)
+ .WithEnvironment("db-type", "mssql")
+ .WithUrls((e) =>
+ {
+ e.Urls.Clear();
+ e.Urls.Add(new() { Url = "/swagger", DisplayText = "🔒Swagger", Endpoint = e.GetEndpoint("https") });
+ e.Urls.Add(new() { Url = "/graphql", DisplayText = "🔒GraphQL", Endpoint = e.GetEndpoint("https") });
+ })
+ .WithHttpHealthCheck("/health");
+
+ if (sqlDbContainer is null)
+ {
+ mssqlService.WithEnvironment("ConnectionStrings__Database", databaseConnectionString);
+ }
+ else
+ {
+ mssqlService.WithEnvironment("ConnectionStrings__Database", sqlDbContainer)
+ .WaitFor(sqlDbContainer);
+ }
+
+ break;
+ case "postgresql":
+ var pgScript = File.ReadAllText("./init-scripts/pg/create-database-pg.sql");
+
+ IResourceBuilder? postgresDB = null;
+
+ if (!string.IsNullOrEmpty(databaseConnectionString))
+ {
+ Console.WriteLine("No connection string provided, starting a local PostgreSQL container.");
+
+ postgresDB = builder.AddPostgres("postgres")
+ .WithPgAdmin()
+ .WithLifetime(ContainerLifetime.Persistent)
+ .AddDatabase("pgDb", "postgres")
+ .WithCreationScript(pgScript);
+ }
+
+ var pgService = builder.AddProject("pg-service", "Development")
+ .WithArgs("-f", "net8.0")
+ .WithEndpoint(endpointName: "https", (e) => e.Port = 1234)
+ .WithEndpoint(endpointName: "http", (e) => e.Port = 2345)
+ .WithEnvironment("db-type", "postgresql")
+ .WithUrls((e) =>
+ {
+ e.Urls.Clear();
+ e.Urls.Add(new() { Url = "/swagger", DisplayText = "🔒Swagger", Endpoint = e.GetEndpoint("https") });
+ e.Urls.Add(new() { Url = "/graphql", DisplayText = "🔒GraphQL", Endpoint = e.GetEndpoint("https") });
+ })
+ .WithHttpHealthCheck("/health");
+
+ if (postgresDB is null)
+ {
+ pgService.WithEnvironment("ConnectionStrings__Database", databaseConnectionString);
+ }
+ else
+ {
+ pgService.WithEnvironment("ConnectionStrings__Database", postgresDB)
+ .WaitFor(postgresDB);
+ }
+
+ break;
+ default:
+ throw new Exception("Please set the ASPIRE_DATABASE environment variable to either 'mssql' or 'postgresql'.");
+}
+
+builder.Build().Run();
diff --git a/src/Aspire.AppHost/Aspire.AppHost.csproj b/src/Aspire.AppHost/Aspire.AppHost.csproj
new file mode 100644
index 0000000000..79d44e51a1
--- /dev/null
+++ b/src/Aspire.AppHost/Aspire.AppHost.csproj
@@ -0,0 +1,24 @@
+
+
+
+
+
+ Exe
+ net8.0
+ enable
+ NU1603;NU1605
+ enable
+ f08719fd-267f-459e-9980-77b1c52c8755
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Aspire.AppHost/DockerStatus.cs b/src/Aspire.AppHost/DockerStatus.cs
new file mode 100644
index 0000000000..ad4237357e
--- /dev/null
+++ b/src/Aspire.AppHost/DockerStatus.cs
@@ -0,0 +1,26 @@
+using System.Diagnostics;
+
+public static class DockerStatus
+{
+ public static async Task IsDockerRunningAsync()
+ {
+ var psi = new ProcessStartInfo
+ {
+ FileName = "docker",
+ Arguments = "info",
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ };
+ try
+ {
+ using var process = Process.Start(psi)!;
+ await process.WaitForExitAsync();
+ return process.ExitCode == 0;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+}
diff --git a/src/Aspire.AppHost/Properties/launchSettings.json b/src/Aspire.AppHost/Properties/launchSettings.json
new file mode 100644
index 0000000000..43a2673a34
--- /dev/null
+++ b/src/Aspire.AppHost/Properties/launchSettings.json
@@ -0,0 +1,57 @@
+{
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21213",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22145"
+ },
+ "dotnetRunMessages": true,
+ "applicationUrl": "https://localhost:17047;http://localhost:15161"
+ },
+ "http": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19015",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20166"
+ },
+ "dotnetRunMessages": true,
+ "applicationUrl": "http://localhost:15161"
+ },
+ "aspire-mssql": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21213",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22145",
+ "ASPIRE_DATABASE_TYPE": "mssql",
+ "ASPIRE_DATABASE_CONNECTION_STRING": ""
+ },
+ "dotnetRunMessages": true,
+ "applicationUrl": "https://localhost:17047;http://localhost:15161"
+ },
+ "aspire-postgresql": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21213",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22145",
+ "ASPIRE_DATABASE_TYPE": "postgresql",
+ "ASPIRE_DATABASE_CONNECTION_STRING": ""
+ },
+ "dotnetRunMessages": true,
+ "applicationUrl": "https://localhost:17047;http://localhost:15161"
+ }
+ },
+ "$schema": "https://json.schemastore.org/launchsettings.json"
+}
diff --git a/src/Aspire.AppHost/README.md b/src/Aspire.AppHost/README.md
new file mode 100644
index 0000000000..3437c367bb
--- /dev/null
+++ b/src/Aspire.AppHost/README.md
@@ -0,0 +1,17 @@
+# Aspire Instructions
+
+This project allows you to run DAB in debug mode using [Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview).
+
+## Prerequisites
+- [.NET SDK](https://dotnet.microsoft.com/download) (8.0 or later)
+- [Docker](https://www.docker.com/products/docker-desktop) (optional, for containerized development)
+
+## Database Configuration
+
+In the `launchProfile.json` file, you can configure the database connection string. If you don't, Aspire will start for you a local instance in a Docker container.
+
+Simply provide a value for the `ASPIRE_DATABASE_CONNECTION_STRING` environment variable.
+
+You can select to run Aspire with different databases selecting the appropriate launch profile:
+- `aspire-sql`
+- `aspire-postgres`
diff --git a/src/Aspire.AppHost/appsettings.json b/src/Aspire.AppHost/appsettings.json
new file mode 100644
index 0000000000..31c092aa45
--- /dev/null
+++ b/src/Aspire.AppHost/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "Warning"
+ }
+ }
+}
diff --git a/src/Aspire.AppHost/init-scripts/pg/create-database-pg.sql b/src/Aspire.AppHost/init-scripts/pg/create-database-pg.sql
new file mode 100644
index 0000000000..73adccc008
--- /dev/null
+++ b/src/Aspire.AppHost/init-scripts/pg/create-database-pg.sql
@@ -0,0 +1,238 @@
+-- create-database-pg.sql
+-- PostgreSQL version of create-database.sql
+
+-- Create database (run this as a superuser, outside the target database)
+-- Uncomment and edit the database name as needed
+--CREATE DATABASE "Trek";
+
+-- Connect to the target database before running the rest of the script
+--\connect Trek;
+
+-- Drop tables in reverse order of creation due to foreign key dependencies
+DROP TABLE IF EXISTS "Character_Species";
+DROP TABLE IF EXISTS "Series_Character";
+DROP TABLE IF EXISTS "Character";
+DROP TABLE IF EXISTS "Species";
+DROP TABLE IF EXISTS "Actor";
+DROP TABLE IF EXISTS "Series";
+
+-- create tables
+CREATE TABLE "Series" (
+ "Id" INTEGER PRIMARY KEY,
+ "Name" VARCHAR(255) NOT NULL
+);
+
+CREATE TABLE "Actor" (
+ "Id" INTEGER PRIMARY KEY,
+ "Name" VARCHAR(255) NOT NULL,
+ "BirthYear" INTEGER NOT NULL
+);
+
+CREATE TABLE "Species" (
+ "Id" INTEGER PRIMARY KEY,
+ "Name" VARCHAR(255) NOT NULL
+);
+
+CREATE TABLE "Character" (
+ "Id" INTEGER PRIMARY KEY,
+ "Name" VARCHAR(255) NOT NULL,
+ "ActorId" INTEGER NOT NULL,
+ "Stardate" DECIMAL(10, 2),
+ FOREIGN KEY ("ActorId") REFERENCES "Actor"("Id")
+);
+
+CREATE TABLE "Series_Character" (
+ "SeriesId" INTEGER,
+ "CharacterId" INTEGER,
+ "Role" VARCHAR(500),
+ FOREIGN KEY ("SeriesId") REFERENCES "Series"("Id"),
+ FOREIGN KEY ("CharacterId") REFERENCES "Character"("Id"),
+ PRIMARY KEY ("SeriesId", "CharacterId")
+);
+
+CREATE TABLE "Character_Species" (
+ "CharacterId" INTEGER,
+ "SpeciesId" INTEGER,
+ FOREIGN KEY ("CharacterId") REFERENCES "Character"("Id"),
+ FOREIGN KEY ("SpeciesId") REFERENCES "Species"("Id"),
+ PRIMARY KEY ("CharacterId", "SpeciesId")
+);
+
+-- create data
+INSERT INTO "Series" ("Id", "Name") VALUES
+ (1, 'Star Trek'),
+ (2, 'Star Trek: The Next Generation'),
+ (3, 'Star Trek: Voyager'),
+ (4, 'Star Trek: Deep Space Nine'),
+ (5, 'Star Trek: Enterprise');
+
+INSERT INTO "Species" ("Id", "Name") VALUES
+ (1, 'Human'),
+ (2, 'Vulcan'),
+ (3, 'Android'),
+ (4, 'Klingon'),
+ (5, 'Betazoid'),
+ (6, 'Hologram'),
+ (7, 'Bajoran'),
+ (8, 'Changeling'),
+ (9, 'Trill'),
+ (10, 'Ferengi'),
+ (11, 'Denobulan'),
+ (12, 'Borg');
+
+INSERT INTO "Actor" ("Id", "Name", "BirthYear") VALUES
+ (1, 'William Shatner', 1931),
+ (2, 'Leonard Nimoy', 1931),
+ (3, 'DeForest Kelley', 1920),
+ (4, 'James Doohan', 1920),
+ (5, 'Nichelle Nichols', 1932),
+ (6, 'George Takei', 1937),
+ (7, 'Walter Koenig', 1936),
+ (8, 'Patrick Stewart', 1940),
+ (9, 'Jonathan Frakes', 1952),
+ (10, 'Brent Spiner', 1949),
+ (11, 'Michael Dorn', 1952),
+ (12, 'Gates McFadden', 1949),
+ (13, 'Marina Sirtis', 1955),
+ (14, 'LeVar Burton', 1957),
+ (15, 'Kate Mulgrew', 1955),
+ (16, 'Robert Beltran', 1953),
+ (17, 'Tim Russ', 1956),
+ (18, 'Roxann Dawson', 1958),
+ (19, 'Robert Duncan McNeill', 1964),
+ (20, 'Garrett Wang', 1968),
+ (21, 'Robert Picardo', 1953),
+ (22, 'Jeri Ryan', 1968),
+ (23, 'Avery Brooks', 1948),
+ (24, 'Nana Visitor', 1957),
+ (25, 'Rene Auberjonois', 1940),
+ (26, 'Terry Farrell', 1963),
+ (27, 'Alexander Siddig', 1965),
+ (28, 'Armin Shimerman', 1949),
+ (29, 'Cirroc Lofton', 1978),
+ (30, 'Scott Bakula', 1954),
+ (31, 'Jolene Blalock', 1975),
+ (32, 'John Billingsley', 1960),
+ (33, 'Connor Trinneer', 1969),
+ (34, 'Dominic Keating', 1962),
+ (35, 'Linda Park', 1978),
+ (36, 'Anthony Montgomery', 1971);
+
+INSERT INTO "Character" ("Id", "Name", "ActorId", "Stardate") VALUES
+ (1, 'James T. Kirk', 1, 2233.04),
+ (2, 'Spock', 2, 2230.06),
+ (3, 'Leonard McCoy', 3, 2227.00),
+ (4, 'Montgomery Scott', 4, 2222.00),
+ (5, 'Uhura', 5, 2233.00),
+ (6, 'Hikaru Sulu', 6, 2237.00),
+ (7, 'Pavel Chekov', 7, 2245.00),
+ (8, 'Jean-Luc Picard', 8, 2305.07),
+ (9, 'William Riker', 9, 2335.08),
+ (10, 'Data', 10, 2336.00),
+ (11, 'Worf', 11, 2340.00),
+ (12, 'Beverly Crusher', 12, 2324.00),
+ (13, 'Deanna Troi', 13, 2336.00),
+ (14, 'Geordi La Forge', 14, 2335.02),
+ (15, 'Kathryn Janeway', 15, 2336.05),
+ (16, 'Chakotay', 16, 2329.00),
+ (17, 'Tuvok', 17, 2264.00),
+ (18, 'B''Elanna Torres', 18, 2349.00),
+ (19, 'Tom Paris', 19, 2346.00),
+ (20, 'Harry Kim', 20, 2349.00),
+ (21, 'The Doctor', 21, 2371.00), -- Stardate of activation
+ (22, 'Seven of Nine', 22, 2348.00),
+ (23, 'Benjamin Sisko', 23, 2332.00),
+ (24, 'Kira Nerys', 24, 2343.00),
+ (25, 'Odo', 25, 2337.00), -- Approximate stardate of discovery
+ (27, 'Jadzia Dax', 26, 2341.00),
+ (28, 'Julian Bashir', 27, 2341.00),
+ (29, 'Quark', 28, 2333.00),
+ (30, 'Jake Sisko', 29, 2355.00),
+ (31, 'Jonathan Archer', 30, 2112.00),
+ (32, 'T''Pol', 31, 2088.00),
+ (33, 'Phlox', 32, 2102.00),
+ (34, 'Charles "Trip" Tucker III', 33, 2121.00),
+ (35, 'Malcolm Reed', 34, 2117.00),
+ (36, 'Hoshi Sato', 35, 2129.00),
+ (37, 'Travis Mayweather', 36, 2126.00);
+
+INSERT INTO "Series_Character" ("SeriesId", "CharacterId", "Role") VALUES
+ (1, 1, 'Captain'), -- James T. Kirk in Star Trek
+ (1, 2, 'Science Officer'), -- Spock in Star Trek
+ (1, 3, 'Doctor'), -- Leonard McCoy in Star Trek
+ (1, 4, 'Engineer'), -- Montgomery Scott in Star Trek
+ (1, 5, 'Communications Officer'), -- Uhura in Star Trek
+ (1, 6, 'Helmsman'), -- Hikaru Sulu in Star Trek
+ (1, 7, 'Navigator'), -- Pavel Chekov in Star Trek
+ (2, 8, 'Captain'), -- Jean-Luc Picard in Star Trek: The Next Generation
+ (2, 9, 'First Officer'), -- William Riker in Star Trek: The Next Generation
+ (2, 10, 'Operations Officer'),-- Data in Star Trek: The Next Generation
+ (2, 11, 'Security Officer'),-- Worf in Star Trek: The Next Generation
+ (2, 12, 'Doctor'),-- Beverly Crusher in Star Trek: The Next Generation
+ (2, 13, 'Counselor'),-- Deanna Troi in Star Trek: The Next Generation
+ (2, 14, 'Engineer'),-- Geordi La Forge in Star Trek: The Next Generation
+ (3, 15, 'Captain'),-- Kathryn Janeway in Star Trek: Voyager
+ (3, 16, 'First Officer'),-- Chakotay in Star Trek: Voyager
+ (3, 17, 'Tactical Officer'),-- Tuvok in Star Trek: Voyager
+ (3, 18, 'Engineer'),-- B'Elanna Torres in Star Trek: Voyager
+ (3, 19, 'Helmsman'),-- Tom Paris in Star Trek: Voyager
+ (3, 20, 'Operations Officer'),-- Harry Kim in Star Trek: Voyager
+ (3, 21, 'Doctor'),-- The Doctor in Star Trek: Voyager
+ (3, 22, 'Astrometrics Officer'),-- Seven of Nine in Star Trek: Voyager
+ (4, 23, 'Commanding Officer'),-- Benjamin Sisko in Star Trek: Deep Space Nine
+ (4, 24, 'First Officer'),-- Kira Nerys in Star Trek: Deep Space Nine
+ (4, 25, 'Security Officer'),-- Odo in Star Trek: Deep Space Nine
+ (4, 11, 'Strategic Operations Officer'),-- Worf in Star Trek: Deep Space Nine
+ (4, 27, 'Science Officer'),-- Jadzia Dax in Star Trek: Deep Space Nine
+ (4, 28, 'Doctor'),-- Julian Bashir in Star Trek: Deep Space Nine
+ (4, 29, 'Bar Owner'),-- Quark in Star Trek: Deep Space Nine
+ (4, 30, 'Civilian'),-- Jake Sisko in Star Trek: Deep Space Nine
+ (5, 31, 'Captain'),-- Jonathan Archer in Star Trek: Enterprise
+ (5, 32, 'Science Officer'),-- T'Pol in Star Trek: Enterprise
+ (5, 33, 'Doctor'),-- Phlox in Star Trek: Enterprise
+ (5, 34, 'Chief Engineer'),-- Charles "Trip" Tucker III in Star Trek: Enterprise
+ (5, 35, 'Armory Officer'),-- Malcolm Reed in Star Trek: Enterprise
+ (5, 36, 'Communications Officer'),-- Hoshi Sato in Star Trek: Enterprise
+ (5, 37, 'Helmsman');-- Travis Mayweather in Star Trek: Enterprise
+
+INSERT INTO "Character_Species" ("CharacterId", "SpeciesId") VALUES
+ (1, 1), -- James T. Kirk is Human
+ (2, 2), -- Spock is Vulcan
+ (2, 1), -- Spock is also Human
+ (3, 1), -- Leonard McCoy is Human
+ (4, 1), -- Montgomery Scott is Human
+ (5, 1), -- Uhura is Human
+ (6, 1), -- Hikaru Sulu is Human
+ (7, 1), -- Pavel Chekov is Human
+ (8, 1), -- Jean-Luc Picard is Human
+ (9, 1), -- William Riker is Human
+ (10, 3), -- Data is Android
+ (11, 4), -- Worf is Klingon
+ (12, 1), -- Beverly Crusher is Human
+ (13, 1), -- Deanna Troi is Human
+ (13, 5), -- Deanna Troi is also Betazoid
+ (14, 1), -- Geordi La Forge is Human
+ (15, 1), -- Kathryn Janeway is Human
+ (16, 1), -- Chakotay is Human
+ (17, 2), -- Tuvok is Vulcan
+ (18, 1), -- B'Elanna Torres is Human
+ (18, 4), -- B'Elanna Torres is also Klingon
+ (19, 1), -- Tom Paris is Human
+ (20, 1), -- Harry Kim is Human
+ (21, 6), -- The Doctor is a Hologram
+ (22, 1), -- Seven of Nine is Human
+ (22, 12),-- Seven of Nine is also Borg
+ (23, 1), -- Benjamin Sisko is Human
+ (24, 7), -- Kira Nerys is Bajoran
+ (25, 8), -- Odo is Changeling
+ (27, 9), -- Jadzia Dax is Trill
+ (28, 1), -- Julian Bashir is Human
+ (29, 10),-- Quark is Ferengi
+ (30, 1), -- Jake Sisko is Human
+ (31, 1), -- Jonathan Archer is Human
+ (32, 2), -- T'Pol is Vulcan
+ (33, 11),-- Phlox is Denobulan
+ (34, 1), -- Charles "Trip" Tucker III is Human
+ (35, 1), -- Malcolm Reed is Human
+ (36, 1), -- Hoshi Sato is Human
+ (37, 1); -- Travis Mayweather is Human
diff --git a/src/Aspire.AppHost/init-scripts/sql/create-database.sql b/src/Aspire.AppHost/init-scripts/sql/create-database.sql
new file mode 100644
index 0000000000..a12c71138e
--- /dev/null
+++ b/src/Aspire.AppHost/init-scripts/sql/create-database.sql
@@ -0,0 +1,239 @@
+
+USE [master]
+GO
+
+CREATE DATABASE [Trek]
+GO
+
+USE [Trek]
+GO
+
+-- Drop tables in reverse order of creation due to foreign key dependencies
+DROP TABLE IF EXISTS Character_Species;
+DROP TABLE IF EXISTS Series_Character;
+DROP TABLE IF EXISTS Character;
+DROP TABLE IF EXISTS Species;
+DROP TABLE IF EXISTS Actor;
+DROP TABLE IF EXISTS Series;
+
+-- create tables
+CREATE TABLE Series (
+ Id INT PRIMARY KEY,
+ Name NVARCHAR(255) NOT NULL
+);
+
+CREATE TABLE Actor (
+ Id INT PRIMARY KEY,
+ Name NVARCHAR(255) NOT NULL,
+ [BirthYear] INT NOT NULL
+);
+
+CREATE TABLE Species (
+ Id INT PRIMARY KEY,
+ Name NVARCHAR(255) NOT NULL
+);
+
+CREATE TABLE Character (
+ Id INT PRIMARY KEY,
+ Name NVARCHAR(255) NOT NULL,
+ ActorId INT NOT NULL,
+ Stardate DECIMAL(10, 2),
+ FOREIGN KEY (ActorId) REFERENCES Actor(Id)
+);
+
+CREATE TABLE Series_Character (
+ SeriesId INT,
+ CharacterId INT,
+ Role VARCHAR(500),
+ FOREIGN KEY (SeriesId) REFERENCES Series(Id),
+ FOREIGN KEY (CharacterId) REFERENCES Character(Id),
+ PRIMARY KEY (SeriesId, CharacterId)
+);
+
+CREATE TABLE Character_Species (
+ CharacterId INT,
+ SpeciesId INT,
+ FOREIGN KEY (CharacterId) REFERENCES Character(Id),
+ FOREIGN KEY (SpeciesId) REFERENCES Species(Id),
+ PRIMARY KEY (CharacterId, SpeciesId)
+);
+
+-- create data
+INSERT INTO Series (Id, Name) VALUES
+ (1, 'Star Trek'),
+ (2, 'Star Trek: The Next Generation'),
+ (3, 'Star Trek: Voyager'),
+ (4, 'Star Trek: Deep Space Nine'),
+ (5, 'Star Trek: Enterprise');
+
+INSERT INTO Species (Id, Name) VALUES
+ (1, 'Human'),
+ (2, 'Vulcan'),
+ (3, 'Android'),
+ (4, 'Klingon'),
+ (5, 'Betazoid'),
+ (6, 'Hologram'),
+ (7, 'Bajoran'),
+ (8, 'Changeling'),
+ (9, 'Trill'),
+ (10, 'Ferengi'),
+ (11, 'Denobulan'),
+ (12, 'Borg');
+
+INSERT INTO Actor (Id, Name, [BirthYear]) VALUES
+ (1, 'William Shatner', 1931),
+ (2, 'Leonard Nimoy', 1931),
+ (3, 'DeForest Kelley', 1920),
+ (4, 'James Doohan', 1920),
+ (5, 'Nichelle Nichols', 1932),
+ (6, 'George Takei', 1937),
+ (7, 'Walter Koenig', 1936),
+ (8, 'Patrick Stewart', 1940),
+ (9, 'Jonathan Frakes', 1952),
+ (10, 'Brent Spiner', 1949),
+ (11, 'Michael Dorn', 1952),
+ (12, 'Gates McFadden', 1949),
+ (13, 'Marina Sirtis', 1955),
+ (14, 'LeVar Burton', 1957),
+ (15, 'Kate Mulgrew', 1955),
+ (16, 'Robert Beltran', 1953),
+ (17, 'Tim Russ', 1956),
+ (18, 'Roxann Dawson', 1958),
+ (19, 'Robert Duncan McNeill', 1964),
+ (20, 'Garrett Wang', 1968),
+ (21, 'Robert Picardo', 1953),
+ (22, 'Jeri Ryan', 1968),
+ (23, 'Avery Brooks', 1948),
+ (24, 'Nana Visitor', 1957),
+ (25, 'Rene Auberjonois', 1940),
+ (26, 'Terry Farrell', 1963),
+ (27, 'Alexander Siddig', 1965),
+ (28, 'Armin Shimerman', 1949),
+ (29, 'Cirroc Lofton', 1978),
+ (30, 'Scott Bakula', 1954),
+ (31, 'Jolene Blalock', 1975),
+ (32, 'John Billingsley', 1960),
+ (33, 'Connor Trinneer', 1969),
+ (34, 'Dominic Keating', 1962),
+ (35, 'Linda Park', 1978),
+ (36, 'Anthony Montgomery', 1971);
+
+INSERT INTO Character (Id, Name, ActorId, Stardate) VALUES
+ (1, 'James T. Kirk', 1, 2233.04),
+ (2, 'Spock', 2, 2230.06),
+ (3, 'Leonard McCoy', 3, 2227.00),
+ (4, 'Montgomery Scott', 4, 2222.00),
+ (5, 'Uhura', 5, 2233.00),
+ (6, 'Hikaru Sulu', 6, 2237.00),
+ (7, 'Pavel Chekov', 7, 2245.00),
+ (8, 'Jean-Luc Picard', 8, 2305.07),
+ (9, 'William Riker', 9, 2335.08),
+ (10, 'Data', 10, 2336.00),
+ (11, 'Worf', 11, 2340.00),
+ (12, 'Beverly Crusher', 12, 2324.00),
+ (13, 'Deanna Troi', 13, 2336.00),
+ (14, 'Geordi La Forge', 14, 2335.02),
+ (15, 'Kathryn Janeway', 15, 2336.05),
+ (16, 'Chakotay', 16, 2329.00),
+ (17, 'Tuvok', 17, 2264.00),
+ (18, 'B''Elanna Torres', 18, 2349.00),
+ (19, 'Tom Paris', 19, 2346.00),
+ (20, 'Harry Kim', 20, 2349.00),
+ (21, 'The Doctor', 21, 2371.00), -- Stardate of activation
+ (22, 'Seven of Nine', 22, 2348.00),
+ (23, 'Benjamin Sisko', 23, 2332.00),
+ (24, 'Kira Nerys', 24, 2343.00),
+ (25, 'Odo', 25, 2337.00), -- Approximate stardate of discovery
+ (27, 'Jadzia Dax', 26, 2341.00),
+ (28, 'Julian Bashir', 27, 2341.00),
+ (29, 'Quark', 28, 2333.00),
+ (30, 'Jake Sisko', 29, 2355.00),
+ (31, 'Jonathan Archer', 30, 2112.00),
+ (32, 'T''Pol', 31, 2088.00),
+ (33, 'Phlox', 32, 2102.00),
+ (34, 'Charles "Trip" Tucker III', 33, 2121.00),
+ (35, 'Malcolm Reed', 34, 2117.00),
+ (36, 'Hoshi Sato', 35, 2129.00),
+ (37, 'Travis Mayweather', 36, 2126.00);
+
+INSERT INTO Series_Character (SeriesId, CharacterId, Role) VALUES
+ (1, 1, 'Captain'), -- James T. Kirk in Star Trek
+ (1, 2, 'Science Officer'), -- Spock in Star Trek
+ (1, 3, 'Doctor'), -- Leonard McCoy in Star Trek
+ (1, 4, 'Engineer'), -- Montgomery Scott in Star Trek
+ (1, 5, 'Communications Officer'), -- Uhura in Star Trek
+ (1, 6, 'Helmsman'), -- Hikaru Sulu in Star Trek
+ (1, 7, 'Navigator'), -- Pavel Chekov in Star Trek
+ (2, 8, 'Captain'), -- Jean-Luc Picard in Star Trek: The Next Generation
+ (2, 9, 'First Officer'), -- William Riker in Star Trek: The Next Generation
+ (2, 10, 'Operations Officer'),-- Data in Star Trek: The Next Generation
+ (2, 11, 'Security Officer'),-- Worf in Star Trek: The Next Generation
+ (2, 12, 'Doctor'),-- Beverly Crusher in Star Trek: The Next Generation
+ (2, 13, 'Counselor'),-- Deanna Troi in Star Trek: The Next Generation
+ (2, 14, 'Engineer'),-- Geordi La Forge in Star Trek: The Next Generation
+ (3, 15, 'Captain'),-- Kathryn Janeway in Star Trek: Voyager
+ (3, 16, 'First Officer'),-- Chakotay in Star Trek: Voyager
+ (3, 17, 'Tactical Officer'),-- Tuvok in Star Trek: Voyager
+ (3, 18, 'Engineer'),-- B'Elanna Torres in Star Trek: Voyager
+ (3, 19, 'Helmsman'),-- Tom Paris in Star Trek: Voyager
+ (3, 20, 'Operations Officer'),-- Harry Kim in Star Trek: Voyager
+ (3, 21, 'Doctor'),-- The Doctor in Star Trek: Voyager
+ (3, 22, 'Astrometrics Officer'),-- Seven of Nine in Star Trek: Voyager
+ (4, 23, 'Commanding Officer'),-- Benjamin Sisko in Star Trek: Deep Space Nine
+ (4, 24, 'First Officer'),-- Kira Nerys in Star Trek: Deep Space Nine
+ (4, 25, 'Security Officer'),-- Odo in Star Trek: Deep Space Nine
+ (4, 11, 'Strategic Operations Officer'),-- Worf in Star Trek: Deep Space Nine
+ (4, 27, 'Science Officer'),-- Jadzia Dax in Star Trek: Deep Space Nine
+ (4, 28, 'Doctor'),-- Julian Bashir in Star Trek: Deep Space Nine
+ (4, 29, 'Bar Owner'),-- Quark in Star Trek: Deep Space Nine
+ (4, 30, 'Civilian'),-- Jake Sisko in Star Trek: Deep Space Nine
+ (5, 31, 'Captain'),-- Jonathan Archer in Star Trek: Enterprise
+ (5, 32, 'Science Officer'),-- T'Pol in Star Trek: Enterprise
+ (5, 33, 'Doctor'),-- Phlox in Star Trek: Enterprise
+ (5, 34, 'Chief Engineer'),-- Charles "Trip" Tucker III in Star Trek: Enterprise
+ (5, 35, 'Armory Officer'),-- Malcolm Reed in Star Trek: Enterprise
+ (5, 36, 'Communications Officer'),-- Hoshi Sato in Star Trek: Enterprise
+ (5, 37, 'Helmsman');-- Travis Mayweather in Star Trek: Enterprise
+
+INSERT INTO Character_Species (CharacterId, SpeciesId) VALUES
+ (1, 1), -- James T. Kirk is Human
+ (2, 2), -- Spock is Vulcan
+ (2, 1), -- Spock is also Human
+ (3, 1), -- Leonard McCoy is Human
+ (4, 1), -- Montgomery Scott is Human
+ (5, 1), -- Uhura is Human
+ (6, 1), -- Hikaru Sulu is Human
+ (7, 1), -- Pavel Chekov is Human
+ (8, 1), -- Jean-Luc Picard is Human
+ (9, 1), -- William Riker is Human
+ (10, 3), -- Data is Android
+ (11, 4), -- Worf is Klingon
+ (12, 1), -- Beverly Crusher is Human
+ (13, 1), -- Deanna Troi is Human
+ (13, 5), -- Deanna Troi is also Betazoid
+ (14, 1), -- Geordi La Forge is Human
+ (15, 1), -- Kathryn Janeway is Human
+ (16, 1), -- Chakotay is Human
+ (17, 2), -- Tuvok is Vulcan
+ (18, 1), -- B'Elanna Torres is Human
+ (18, 4), -- B'Elanna Torres is also Klingon
+ (19, 1), -- Tom Paris is Human
+ (20, 1), -- Harry Kim is Human
+ (21, 6), -- The Doctor is a Hologram
+ (22, 1), -- Seven of Nine is Human
+ (22, 12),-- Seven of Nine is also Borg
+ (23, 1), -- Benjamin Sisko is Human
+ (24, 7), -- Kira Nerys is Bajoran
+ (25, 8), -- Odo is Changeling
+ (27, 9), -- Jadzia Dax is Trill
+ (28, 1), -- Julian Bashir is Human
+ (29, 10),-- Quark is Ferengi
+ (30, 1), -- Jake Sisko is Human
+ (31, 1), -- Jonathan Archer is Human
+ (32, 2), -- T'Pol is Vulcan
+ (33, 11),-- Phlox is Denobulan
+ (34, 1), -- Charles "Trip" Tucker III is Human
+ (35, 1), -- Malcolm Reed is Human
+ (36, 1), -- Hoshi Sato is Human
+ (37, 1); -- Travis Mayweather is Human
+
\ No newline at end of file
diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs
index 6fbe08879b..1a944d115b 100644
--- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs
+++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs
@@ -5,14 +5,13 @@
using Azure.DataApiBuilder.Auth;
using Azure.DataApiBuilder.Config.DatabasePrimitives;
using Azure.DataApiBuilder.Config.ObjectModel;
-using Azure.DataApiBuilder.Core.Authorization;
using Azure.DataApiBuilder.Core.Configurations;
using Azure.DataApiBuilder.Core.Models;
using Azure.DataApiBuilder.Core.Resolvers;
using Azure.DataApiBuilder.Core.Resolvers.Factories;
using Azure.DataApiBuilder.Core.Services;
-using Azure.DataApiBuilder.Core.Services.MetadataProviders;
using Azure.DataApiBuilder.Mcp.Model;
+using Azure.DataApiBuilder.Mcp.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
@@ -31,18 +30,18 @@ public Tool GetToolMetadata()
return new Tool
{
Name = "create_record",
- Description = "Creates a new record in the specified entity.",
+ Description = "STEP 1: describe_entities -> find entities with CREATE permission and their fields. STEP 2: call this tool with matching field names and values.",
InputSchema = JsonSerializer.Deserialize(
@"{
""type"": ""object"",
""properties"": {
""entity"": {
""type"": ""string"",
- ""description"": ""The name of the entity""
+ ""description"": ""Entity name with CREATE permission.""
},
""data"": {
""type"": ""object"",
- ""description"": ""The data for the new record""
+ ""description"": ""Required fields and values for the new record.""
}
},
""required"": [""entity"", ""data""]
@@ -57,23 +56,21 @@ public async Task ExecuteAsync(
CancellationToken cancellationToken = default)
{
ILogger? logger = serviceProvider.GetService>();
+ string toolName = GetToolMetadata().Name;
if (arguments == null)
{
- return Utils.McpResponseBuilder.BuildErrorResult("Invalid Arguments", "No arguments provided", logger);
+ return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "No arguments provided.", logger);
}
RuntimeConfigProvider runtimeConfigProvider = serviceProvider.GetRequiredService();
if (!runtimeConfigProvider.TryGetConfig(out RuntimeConfig? runtimeConfig))
{
- return Utils.McpResponseBuilder.BuildErrorResult("Invalid Configuration", "Runtime configuration not available", logger);
+ return McpResponseBuilder.BuildErrorResult(toolName, "InvalidConfiguration", "Runtime configuration not available.", logger);
}
if (runtimeConfig.McpDmlTools?.CreateRecord != true)
{
- return Utils.McpResponseBuilder.BuildErrorResult(
- "ToolDisabled",
- "The create_record tool is disabled in the configuration.",
- logger);
+ return McpErrorHelpers.ToolDisabled(toolName, logger);
}
try
@@ -81,39 +78,21 @@ public async Task ExecuteAsync(
cancellationToken.ThrowIfCancellationRequested();
JsonElement root = arguments.RootElement;
- if (!root.TryGetProperty("entity", out JsonElement entityElement) ||
- !root.TryGetProperty("data", out JsonElement dataElement))
+ if (!McpArgumentParser.TryParseEntityAndData(root, out string entityName, out JsonElement dataElement, out string parseError))
{
- return Utils.McpResponseBuilder.BuildErrorResult("InvalidArguments", "Missing required arguments 'entity' or 'data'", logger);
+ return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", parseError, logger);
}
- string entityName = entityElement.GetString() ?? string.Empty;
- if (string.IsNullOrWhiteSpace(entityName))
+ if (!McpMetadataHelper.TryResolveMetadata(
+ entityName,
+ runtimeConfig,
+ serviceProvider,
+ out ISqlMetadataProvider sqlMetadataProvider,
+ out DatabaseObject dbObject,
+ out string dataSourceName,
+ out string metadataError))
{
- return Utils.McpResponseBuilder.BuildErrorResult("InvalidArguments", "Entity name cannot be empty", logger);
- }
-
- string dataSourceName;
- try
- {
- dataSourceName = runtimeConfig.GetDataSourceNameFromEntityName(entityName);
- }
- catch (Exception)
- {
- return Utils.McpResponseBuilder.BuildErrorResult("InvalidConfiguration", $"Entity '{entityName}' not found in configuration", logger);
- }
-
- IMetadataProviderFactory metadataProviderFactory = serviceProvider.GetRequiredService();
- ISqlMetadataProvider sqlMetadataProvider = metadataProviderFactory.GetMetadataProvider(dataSourceName);
-
- DatabaseObject dbObject;
- try
- {
- dbObject = sqlMetadataProvider.GetDatabaseObjectByKey(entityName);
- }
- catch (Exception)
- {
- return Utils.McpResponseBuilder.BuildErrorResult("InvalidConfiguration", $"Database object for entity '{entityName}' not found", logger);
+ return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", metadataError, logger);
}
// Create an HTTP context for authorization
@@ -121,15 +100,20 @@ public async Task ExecuteAsync(
HttpContext httpContext = httpContextAccessor.HttpContext ?? new DefaultHttpContext();
IAuthorizationResolver authorizationResolver = serviceProvider.GetRequiredService();
- if (httpContext is null || !authorizationResolver.IsValidRoleContext(httpContext))
+ if (!McpAuthorizationHelper.ValidateRoleContext(httpContext, authorizationResolver, out string roleCtxError))
{
- return Utils.McpResponseBuilder.BuildErrorResult("PermissionDenied", "Permission denied: Unable to resolve a valid role context for update operation.", logger);
+ return McpErrorHelpers.PermissionDenied(toolName, entityName, "create", roleCtxError, logger);
}
- // Validate that we have at least one role authorized for create
- if (!TryResolveAuthorizedRole(httpContext, authorizationResolver, entityName, out string authError))
+ if (!McpAuthorizationHelper.TryResolveAuthorizedRole(
+ httpContext,
+ authorizationResolver,
+ entityName,
+ EntityActionOperation.Create,
+ out string? effectiveRole,
+ out string authError))
{
- return Utils.McpResponseBuilder.BuildErrorResult("PermissionDenied", authError, logger);
+ return McpErrorHelpers.PermissionDenied(toolName, entityName, "create", authError, logger);
}
JsonElement insertPayloadRoot = dataElement.Clone();
@@ -150,12 +134,13 @@ public async Task ExecuteAsync(
}
catch (Exception ex)
{
- return Utils.McpResponseBuilder.BuildErrorResult("ValidationFailed", $"Request validation failed: {ex.Message}", logger);
+ return McpResponseBuilder.BuildErrorResult(toolName, "ValidationFailed", $"Request validation failed: {ex.Message}", logger);
}
}
else
{
- return Utils.McpResponseBuilder.BuildErrorResult(
+ return McpResponseBuilder.BuildErrorResult(
+ toolName,
"InvalidCreateTarget",
"The create_record tool is only available for tables.",
logger);
@@ -169,7 +154,7 @@ public async Task ExecuteAsync(
if (result is CreatedResult createdResult)
{
- return Utils.McpResponseBuilder.BuildSuccessResult(
+ return McpResponseBuilder.BuildSuccessResult(
new Dictionary
{
["entity"] = entityName,
@@ -184,14 +169,15 @@ public async Task ExecuteAsync(
bool isError = objectResult.StatusCode.HasValue && objectResult.StatusCode.Value >= 400 && objectResult.StatusCode.Value != 403;
if (isError)
{
- return Utils.McpResponseBuilder.BuildErrorResult(
+ return McpResponseBuilder.BuildErrorResult(
+ toolName,
"CreateFailed",
$"Failed to create record in entity '{entityName}'. Error: {JsonSerializer.Serialize(objectResult.Value)}",
logger);
}
else
{
- return Utils.McpResponseBuilder.BuildSuccessResult(
+ return McpResponseBuilder.BuildSuccessResult(
new Dictionary
{
["entity"] = entityName,
@@ -206,14 +192,15 @@ public async Task ExecuteAsync(
{
if (result is null)
{
- return Utils.McpResponseBuilder.BuildErrorResult(
+ return McpResponseBuilder.BuildErrorResult(
+ toolName,
"UnexpectedError",
$"Mutation engine returned null result for entity '{entityName}'",
logger);
}
else
{
- return Utils.McpResponseBuilder.BuildSuccessResult(
+ return McpResponseBuilder.BuildSuccessResult(
new Dictionary
{
["entity"] = entityName,
@@ -226,50 +213,8 @@ public async Task ExecuteAsync(
}
catch (Exception ex)
{
- return Utils.McpResponseBuilder.BuildErrorResult("Error", $"Error: {ex.Message}", logger);
+ return McpResponseBuilder.BuildErrorResult(toolName, "Error", $"Error: {ex.Message}", logger);
}
}
-
- private static bool TryResolveAuthorizedRole(
- HttpContext httpContext,
- IAuthorizationResolver authorizationResolver,
- string entityName,
- out string error)
- {
- error = string.Empty;
-
- string roleHeader = httpContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER].ToString();
-
- if (string.IsNullOrWhiteSpace(roleHeader))
- {
- error = "Client role header is missing or empty.";
- return false;
- }
-
- string[] roles = roleHeader
- .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .ToArray();
-
- if (roles.Length == 0)
- {
- error = "Client role header is missing or empty.";
- return false;
- }
-
- foreach (string role in roles)
- {
- bool allowed = authorizationResolver.AreRoleAndOperationDefinedForEntity(
- entityName, role, EntityActionOperation.Create);
-
- if (allowed)
- {
- return true;
- }
- }
-
- error = "You do not have permission to create records for this entity.";
- return false;
- }
}
}
diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DeleteRecordTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DeleteRecordTool.cs
index 86a5ce15ec..d7837c0103 100644
--- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DeleteRecordTool.cs
+++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DeleteRecordTool.cs
@@ -44,18 +44,18 @@ public Tool GetToolMetadata()
return new Tool
{
Name = "delete_record",
- Description = "Deletes a record from a table based on primary key or composite key",
+ Description = "STEP 1: describe_entities -> find entities with DELETE permission and their key fields. STEP 2: call this tool with full key values.",
InputSchema = JsonSerializer.Deserialize(
@"{
""type"": ""object"",
""properties"": {
""entity"": {
""type"": ""string"",
- ""description"": ""The name of the entity (table) as configured in dab-config. Required.""
+ ""description"": ""Entity name with DELETE permission.""
},
""keys"": {
""type"": ""object"",
- ""description"": ""Primary key values to identify the record to delete. For composite keys, provide all key columns as properties. Required.""
+ ""description"": ""All key fields identifying the record.""
}
},
""required"": [""entity"", ""keys""]
@@ -73,6 +73,7 @@ public async Task ExecuteAsync(
CancellationToken cancellationToken = default)
{
ILogger? logger = serviceProvider.GetService>();
+ string toolName = GetToolMetadata().Name;
try
{
@@ -86,49 +87,37 @@ public async Task ExecuteAsync(
// 2) Check if the tool is enabled in configuration before proceeding
if (config.McpDmlTools?.DeleteRecord != true)
{
- return McpResponseBuilder.BuildErrorResult(
- "ToolDisabled",
- $"The {this.GetToolMetadata().Name} tool is disabled in the configuration.",
- logger);
+ return McpErrorHelpers.ToolDisabled(GetToolMetadata().Name, logger);
}
// 3) Parsing & basic argument validation
if (arguments is null)
{
- return McpResponseBuilder.BuildErrorResult("InvalidArguments", "No arguments provided.", logger);
+ return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "No arguments provided.", logger);
}
if (!McpArgumentParser.TryParseEntityAndKeys(arguments.RootElement, out string entityName, out Dictionary keys, out string parseError))
{
- return McpResponseBuilder.BuildErrorResult("InvalidArguments", parseError, logger);
- }
-
- IMetadataProviderFactory metadataProviderFactory = serviceProvider.GetRequiredService();
- IMutationEngineFactory mutationEngineFactory = serviceProvider.GetRequiredService();
-
- // 4) Resolve metadata for entity existence check
- string dataSourceName;
- ISqlMetadataProvider sqlMetadataProvider;
-
- try
- {
- dataSourceName = config.GetDataSourceNameFromEntityName(entityName);
- sqlMetadataProvider = metadataProviderFactory.GetMetadataProvider(dataSourceName);
- }
- catch (Exception)
- {
- return McpResponseBuilder.BuildErrorResult("EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger);
+ return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", parseError, logger);
}
- if (!sqlMetadataProvider.EntityToDatabaseObject.TryGetValue(entityName, out DatabaseObject? dbObject) || dbObject is null)
+ // 4) Resolve metadata for entity existence
+ if (!McpMetadataHelper.TryResolveMetadata(
+ entityName,
+ config,
+ serviceProvider,
+ out ISqlMetadataProvider sqlMetadataProvider,
+ out DatabaseObject dbObject,
+ out string dataSourceName,
+ out string metadataError))
{
- return McpResponseBuilder.BuildErrorResult("EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger);
+ return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", metadataError, logger);
}
// Validate it's a table or view
if (dbObject.SourceType != EntitySourceType.Table && dbObject.SourceType != EntitySourceType.View)
{
- return McpResponseBuilder.BuildErrorResult("InvalidEntity", $"Entity '{entityName}' is not a table or view. Use 'execute-entity' for stored procedures.", logger);
+ return McpResponseBuilder.BuildErrorResult(toolName, "InvalidEntity", $"Entity '{entityName}' is not a table or view. Use 'execute-entity' for stored procedures.", logger);
}
// 5) Authorization
@@ -138,7 +127,7 @@ public async Task ExecuteAsync(
if (!McpAuthorizationHelper.ValidateRoleContext(httpContext, authResolver, out string roleError))
{
- return McpResponseBuilder.BuildErrorResult("PermissionDenied", $"Permission denied: {roleError}", logger);
+ return McpErrorHelpers.PermissionDenied(toolName, entityName, "delete", roleError, logger);
}
if (!McpAuthorizationHelper.TryResolveAuthorizedRole(
@@ -149,10 +138,11 @@ public async Task ExecuteAsync(
out string? effectiveRole,
out string authError))
{
- return McpResponseBuilder.BuildErrorResult("PermissionDenied", $"Permission denied: {authError}", logger);
+ return McpErrorHelpers.PermissionDenied(toolName, entityName, "delete", authError, logger);
}
- // 6) Build and validate Delete context
+ // Need MetadataProviderFactory for RequestValidator; resolve here.
+ IMetadataProviderFactory metadataProviderFactory = serviceProvider.GetRequiredService();
RequestValidator requestValidator = new(metadataProviderFactory, runtimeConfigProvider);
DeleteRequestContext context = new(
@@ -164,7 +154,7 @@ public async Task ExecuteAsync(
{
if (kvp.Value is null)
{
- return McpResponseBuilder.BuildErrorResult("InvalidArguments", $"Primary key value for '{kvp.Key}' cannot be null.", logger);
+ return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", $"Primary key value for '{kvp.Key}' cannot be null.", logger);
}
context.PrimaryKeyValuePairs[kvp.Key] = kvp.Value;
@@ -172,7 +162,7 @@ public async Task ExecuteAsync(
requestValidator.ValidatePrimaryKey(context);
- // 7) Execute
+ IMutationEngineFactory mutationEngineFactory = serviceProvider.GetRequiredService();
DatabaseType dbType = config.GetDataSourceFromDataSourceName(dataSourceName).DatabaseType;
IMutationEngine mutationEngine = mutationEngineFactory.GetMutationEngine(dbType);
@@ -195,6 +185,7 @@ public async Task ExecuteAsync(
{
string keyDetails = McpJsonHelper.FormatKeyDetails(keys);
return McpResponseBuilder.BuildErrorResult(
+ toolName,
"RecordNotFound",
$"No record found with the specified primary key: {keyDetails}",
logger);
@@ -203,6 +194,7 @@ public async Task ExecuteAsync(
message.Contains("REFERENCE constraint", StringComparison.OrdinalIgnoreCase))
{
return McpResponseBuilder.BuildErrorResult(
+ toolName,
"ConstraintViolation",
"Cannot delete record due to foreign key constraint. Other records depend on this record.",
logger);
@@ -211,6 +203,7 @@ public async Task ExecuteAsync(
message.Contains("authorization", StringComparison.OrdinalIgnoreCase))
{
return McpResponseBuilder.BuildErrorResult(
+ toolName,
"PermissionDenied",
"You do not have permission to delete this record.",
logger);
@@ -219,6 +212,7 @@ public async Task ExecuteAsync(
message.Contains("type", StringComparison.OrdinalIgnoreCase))
{
return McpResponseBuilder.BuildErrorResult(
+ toolName,
"InvalidArguments",
"Invalid data type for one or more key values.",
logger);
@@ -226,6 +220,7 @@ public async Task ExecuteAsync(
// For any other DAB exceptions, return the message as-is
return McpResponseBuilder.BuildErrorResult(
+ toolName,
"DataApiBuilderError",
dabEx.Message,
logger);
@@ -242,7 +237,7 @@ public async Task ExecuteAsync(
208 => $"Table '{dbObject.FullName}' not found in the database.",
_ => $"Database error: {sqlEx.Message}"
};
- return McpResponseBuilder.BuildErrorResult("DatabaseError", errorMessage, logger);
+ return McpResponseBuilder.BuildErrorResult(toolName, "DatabaseError", errorMessage, logger);
}
catch (DbException dbEx)
{
@@ -254,6 +249,7 @@ public async Task ExecuteAsync(
if (errorMsg.Contains("foreign key") || errorMsg.Contains("constraint"))
{
return McpResponseBuilder.BuildErrorResult(
+ toolName,
"ConstraintViolation",
"Cannot delete record due to foreign key constraint. Other records depend on this record.",
logger);
@@ -261,24 +257,25 @@ public async Task ExecuteAsync(
else if (errorMsg.Contains("not found") || errorMsg.Contains("does not exist"))
{
return McpResponseBuilder.BuildErrorResult(
+ toolName,
"RecordNotFound",
"No record found with the specified primary key.",
logger);
}
- return McpResponseBuilder.BuildErrorResult("DatabaseError", $"Database error: {dbEx.Message}", logger);
+ return McpResponseBuilder.BuildErrorResult(toolName, "DatabaseError", $"Database error: {dbEx.Message}", logger);
}
catch (InvalidOperationException ioEx) when (ioEx.Message.Contains("connection", StringComparison.OrdinalIgnoreCase))
{
// Handle connection-related issues
logger?.LogError(ioEx, "Database connection error");
- return McpResponseBuilder.BuildErrorResult("ConnectionError", "Failed to connect to the database.", logger);
+ return McpResponseBuilder.BuildErrorResult(toolName, "ConnectionError", "Failed to connect to the database.", logger);
}
catch (TimeoutException timeoutEx)
{
// Handle query timeout
logger?.LogError(timeoutEx, "Delete operation timeout for {Entity}", entityName);
- return McpResponseBuilder.BuildErrorResult("TimeoutError", "The delete operation timed out.", logger);
+ return McpResponseBuilder.BuildErrorResult(toolName, "TimeoutError", "The delete operation timed out.", logger);
}
catch (Exception ex)
{
@@ -289,6 +286,7 @@ public async Task ExecuteAsync(
{
string keyDetails = McpJsonHelper.FormatKeyDetails(keys);
return McpResponseBuilder.BuildErrorResult(
+ toolName,
"RecordNotFound",
$"No entity found with the given key {keyDetails}.",
logger);
@@ -325,18 +323,18 @@ public async Task ExecuteAsync(
}
catch (OperationCanceledException)
{
- return McpResponseBuilder.BuildErrorResult("OperationCanceled", "The delete operation was canceled.", logger);
+ return McpResponseBuilder.BuildErrorResult(toolName, "OperationCanceled", "The delete operation was canceled.", logger);
}
catch (ArgumentException argEx)
{
- return McpResponseBuilder.BuildErrorResult("InvalidArguments", argEx.Message, logger);
+ return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", argEx.Message, logger);
}
catch (Exception ex)
{
- ILogger? innerLogger = serviceProvider.GetService>();
- innerLogger?.LogError(ex, "Unexpected error in DeleteRecordTool.");
+ logger?.LogError(ex, "Unexpected error in DeleteRecordTool.");
return McpResponseBuilder.BuildErrorResult(
+ toolName,
"UnexpectedError",
"An unexpected error occurred during the delete operation.",
logger);
diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs
index 95c53d1d28..c5b283214f 100644
--- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs
+++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs
@@ -2,11 +2,14 @@
// Licensed under the MIT License.
using System.Text.Json;
+using Azure.DataApiBuilder.Auth;
using Azure.DataApiBuilder.Config.ObjectModel;
+using Azure.DataApiBuilder.Core.Authorization;
using Azure.DataApiBuilder.Core.Configurations;
using Azure.DataApiBuilder.Mcp.Model;
using Azure.DataApiBuilder.Mcp.Utils;
using Azure.DataApiBuilder.Service.Exceptions;
+using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Protocol;
@@ -25,7 +28,7 @@ public class DescribeEntitiesTool : IMcpTool
public ToolType ToolType { get; } = ToolType.BuiltIn;
///
- /// Gets the metadata for the delete-record tool, including its name, description, and input schema.
+ /// Gets the metadata for the describe-entities tool, including its name, description, and input schema.
///
///
public Tool GetToolMetadata()
@@ -33,21 +36,21 @@ public Tool GetToolMetadata()
return new Tool
{
Name = "describe_entities",
- Description = "Lists and describes all entities in the database, including their types and available operations.",
+ Description = "Lists all entities and metadata. ALWAYS CALL FIRST. Each entity includes: name, type, fields, parameters, and permissions. The permissions array defines which tools are allowed. 'ALL' expands by type: data->CREATE, READ, UPDATE, DELETE.",
InputSchema = JsonSerializer.Deserialize(
@"{
""type"": ""object"",
""properties"": {
""nameOnly"": {
""type"": ""boolean"",
- ""description"": ""If true, only entity names and descriptions will be returned. If false, full metadata including fields, parameters etc. will be included. Default is false.""
+ ""description"": ""If true, the response includes only entity names and short summaries, omitting detailed metadata such as fields, parameters, and permissions. Use this when the database contains many entities and the full payload would be too large. The usual strategy is: first call describe_entities with nameOnly=true to get a lightweight list, then call describe_entities again with nameOnly=false for specific entities that require full metadata. This flag is meant for discovery, not execution planning. The model must not assume that nameOnly=true provides enough detail for CRUD or EXECUTE operations.""
},
""entities"": {
""type"": ""array"",
""items"": {
""type"": ""string""
},
- ""description"": ""Optional list of specific entity names to filter by. If empty, all entities will be described.""
+ ""description"": ""Optional list of entity names to describe in full detail. Use this to reduce payload size when only certain entities are relevant. Do NOT pass both entities[] and nameOnly=true together, as that combination is nonsensical: nameOnly=true ignores detailed metadata, while entities[] explicitly requests it. Choose one approach—broad discovery with nameOnly=true OR targeted metadata with entities[].""
}
}
}"
@@ -64,6 +67,7 @@ public Task ExecuteAsync(
CancellationToken cancellationToken = default)
{
ILogger? logger = serviceProvider.GetService>();
+ string toolName = GetToolMetadata().Name;
try
{
@@ -74,14 +78,72 @@ public Task ExecuteAsync(
if (!IsToolEnabled(runtimeConfig))
{
- return Task.FromResult(McpResponseBuilder.BuildErrorResult(
- "ToolDisabled",
- $"The {GetToolMetadata().Name} tool is disabled in the configuration.",
- logger));
+ return Task.FromResult(McpErrorHelpers.ToolDisabled(GetToolMetadata().Name, logger));
+ }
+
+ // Get authorization services to determine current user's role
+ IAuthorizationResolver authResolver = serviceProvider.GetRequiredService();
+ IHttpContextAccessor httpContextAccessor = serviceProvider.GetRequiredService();
+ HttpContext? httpContext = httpContextAccessor.HttpContext;
+
+ // Get current user's role for permission filtering
+ // For discovery tools like describe_entities, we use the first valid role from the header
+ // This differs from operation-specific tools that check permissions per entity per operation
+ string? currentUserRole = null;
+ if (httpContext != null && authResolver.IsValidRoleContext(httpContext))
+ {
+ string roleHeader = httpContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER].ToString();
+ if (!string.IsNullOrWhiteSpace(roleHeader))
+ {
+ string[] roles = roleHeader
+ .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+
+ if (roles.Length > 1)
+ {
+ logger?.LogWarning("Multiple roles detected in request header: [{Roles}]. Using first role '{FirstRole}' for entity discovery. " +
+ "Consider using a single role for consistent permission reporting.",
+ string.Join(", ", roles), roles[0]);
+ }
+
+ // For discovery operations, take the first role from comma-separated list
+ // This provides a consistent view of available entities for the primary role
+ currentUserRole = roles.FirstOrDefault();
+ }
+ }
+
+ // Get current user's role for permission filtering
+ // For discovery tools like describe_entities, we use the first valid role from the header
+ // This differs from operation-specific tools that check permissions per entity per operation
+ if (httpContext != null && authResolver.IsValidRoleContext(httpContext))
+ {
+ string roleHeader = httpContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER].ToString();
+ if (!string.IsNullOrWhiteSpace(roleHeader))
+ {
+ string[] roles = roleHeader
+ .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+
+ if (roles.Length > 1)
+ {
+ logger?.LogWarning("Multiple roles detected in request header: [{Roles}]. Using first role '{FirstRole}' for entity discovery. " +
+ "Consider using a single role for consistent permission reporting.",
+ string.Join(", ", roles), roles[0]);
+ }
+
+ // For discovery operations, take the first role from comma-separated list
+ // This provides a consistent view of available entities for the primary role
+ currentUserRole = roles.FirstOrDefault();
+ }
}
(bool nameOnly, HashSet? entityFilter) = ParseArguments(arguments, logger);
+ if (currentUserRole == null)
+ {
+ logger?.LogWarning("Current user role could not be determined from HTTP context or role header. " +
+ "Entity permissions will be empty (no permissions shown) rather than using anonymous permissions. " +
+ "Ensure the '{RoleHeader}' header is properly set.", AuthorizationResolver.CLIENT_ROLE_HEADER);
+ }
+
List> entityList = new();
if (runtimeConfig.Entities != null)
@@ -102,7 +164,7 @@ public Task ExecuteAsync(
{
Dictionary entityInfo = nameOnly
? BuildBasicEntityInfo(entityName, entity)
- : BuildFullEntityInfo(entityName, entity);
+ : BuildFullEntityInfo(entityName, entity, currentUserRole);
entityList.Add(entityInfo);
}
@@ -118,6 +180,7 @@ public Task ExecuteAsync(
if (entityFilter != null && entityFilter.Count > 0)
{
return Task.FromResult(McpResponseBuilder.BuildErrorResult(
+ toolName,
"EntitiesNotFound",
$"No entities found matching the filter: {string.Join(", ", entityFilter)}",
logger));
@@ -125,6 +188,7 @@ public Task ExecuteAsync(
else
{
return Task.FromResult(McpResponseBuilder.BuildErrorResult(
+ toolName,
"NoEntitiesConfigured",
"No entities are configured in the runtime configuration.",
logger));
@@ -140,19 +204,14 @@ public Task ExecuteAsync(
Dictionary responseData = new()
{
["entities"] = finalEntityList,
- ["count"] = finalEntityList.Count,
- ["mode"] = nameOnly ? "basic" : "full"
+ ["count"] = finalEntityList.Count
};
- if (entityFilter != null && entityFilter.Count > 0)
- {
- responseData["filter"] = entityFilter.ToArray();
- }
-
logger?.LogInformation(
- "DescribeEntitiesTool returned {EntityCount} entities in {Mode} mode.",
+ "DescribeEntitiesTool returned {EntityCount} entities. Response type: {ResponseType} (nameOnly={NameOnly}).",
finalEntityList.Count,
- nameOnly ? "basic" : "full");
+ nameOnly ? "lightweight summary (names and descriptions only)" : "full metadata with fields, parameters, and permissions",
+ nameOnly);
return Task.FromResult(McpResponseBuilder.BuildSuccessResult(
responseData,
@@ -162,6 +221,7 @@ public Task ExecuteAsync(
catch (OperationCanceledException)
{
return Task.FromResult(McpResponseBuilder.BuildErrorResult(
+ toolName,
"OperationCanceled",
"The describe operation was canceled.",
logger));
@@ -170,6 +230,7 @@ public Task ExecuteAsync(
{
logger?.LogError(dabEx, "Data API Builder error in DescribeEntitiesTool");
return Task.FromResult(McpResponseBuilder.BuildErrorResult(
+ toolName,
"DataApiBuilderError",
dabEx.Message,
logger));
@@ -177,6 +238,7 @@ public Task ExecuteAsync(
catch (ArgumentException argEx)
{
return Task.FromResult(McpResponseBuilder.BuildErrorResult(
+ toolName,
"InvalidArguments",
argEx.Message,
logger));
@@ -185,6 +247,7 @@ public Task ExecuteAsync(
{
logger?.LogError(ioEx, "Invalid operation in DescribeEntitiesTool");
return Task.FromResult(McpResponseBuilder.BuildErrorResult(
+ toolName,
"InvalidOperation",
"Failed to retrieve entity metadata: " + ioEx.Message,
logger));
@@ -193,6 +256,7 @@ public Task ExecuteAsync(
{
logger?.LogError(ex, "Unexpected error in DescribeEntitiesTool");
return Task.FromResult(McpResponseBuilder.BuildErrorResult(
+ toolName,
"UnexpectedError",
"An unexpected error occurred while describing entities.",
logger));
@@ -276,13 +340,18 @@ private static bool ShouldIncludeEntity(string entityName, HashSet? enti
///
/// The name of the entity to include in the dictionary.
/// The entity object from which to extract additional information.
- /// A dictionary with two keys: "name", containing the entity name, and "description", containing the entity's
+ /// A dictionary with two keys: "name", containing the entity alias (or name if no alias), and "description", containing the entity's
/// description or an empty string if the description is null.
private static Dictionary BuildBasicEntityInfo(string entityName, Entity entity)
{
+ // Use GraphQL singular name as alias if available, otherwise use entity name
+ string displayName = !string.IsNullOrWhiteSpace(entity.GraphQL?.Singular)
+ ? entity.GraphQL.Singular
+ : entityName;
+
return new Dictionary
{
- ["name"] = entityName,
+ ["name"] = displayName,
["description"] = entity.Description ?? string.Empty
};
}
@@ -290,11 +359,22 @@ private static bool ShouldIncludeEntity(string entityName, HashSet? enti
///
/// Builds full entity info: name, description, fields, parameters (for stored procs), permissions.
///
- private static Dictionary BuildFullEntityInfo(string entityName, Entity entity)
+ /// The name of the entity to include in the dictionary.
+ /// The entity object from which to extract additional information.
+ /// The role of the current user, used to determine permissions.
+ ///
+ /// A dictionary containing the entity's name, description, fields, parameters (if applicable), and permissions.
+ ///
+ private static Dictionary BuildFullEntityInfo(string entityName, Entity entity, string? currentUserRole)
{
+ // Use GraphQL singular name as alias if available, otherwise use entity name
+ string displayName = !string.IsNullOrWhiteSpace(entity.GraphQL?.Singular)
+ ? entity.GraphQL.Singular
+ : entityName;
+
Dictionary info = new()
{
- ["name"] = entityName,
+ ["name"] = displayName,
["description"] = entity.Description ?? string.Empty,
["fields"] = BuildFieldMetadataInfo(entity.Fields),
};
@@ -304,7 +384,7 @@ private static bool ShouldIncludeEntity(string entityName, HashSet? enti
info["parameters"] = BuildParameterMetadataInfo(entity.Source.Parameters);
}
- info["permissions"] = BuildPermissionsInfo(entity);
+ info["permissions"] = BuildPermissionsInfo(entity, currentUserRole);
return info;
}
@@ -325,7 +405,7 @@ private static List