Skip to content

Commit a1ecf21

Browse files
CopilotJerryNixon
andcommitted
Add QueryExecutor extensibility docs for custom database types (discussion #2993)
Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com>
1 parent 35a4630 commit a1ecf21

2 files changed

Lines changed: 224 additions & 1 deletion

File tree

docs/custom-database-executor.md

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Extending DAB with a Custom Database Executor
2+
3+
Data API builder (DAB) natively supports Azure SQL / SQL Server, Azure SQL DW, PostgreSQL, MySQL, and Cosmos DB.
4+
This guide shows how to create a custom `QueryExecutor` for a database engine that DAB does not support out of the box (for example, Oracle).
5+
6+
## Prerequisites
7+
8+
- Familiarity with C# generics and nullable reference types.
9+
- The [Oracle managed driver NuGet package](https://www.nuget.org/packages/Oracle.ManagedDataAccess.Core)
10+
(or the equivalent driver for your database).
11+
12+
---
13+
14+
## 1. Project setup
15+
16+
Create a new class-library project and reference the `Azure.DataApiBuilder.Core` package.
17+
18+
In your `.csproj`, enable **nullable reference types** — this is required because the
19+
`QueryExecutor<TConnection>` base class uses `TResult?` on unconstrained generic type
20+
parameters:
21+
22+
```xml
23+
<PropertyGroup>
24+
<Nullable>enable</Nullable>
25+
</PropertyGroup>
26+
```
27+
28+
---
29+
30+
## 2. Implement `OracleQueryExecutor`
31+
32+
Inherit from `QueryExecutor<TConnection>`, supplying your concrete connection type as
33+
the generic argument:
34+
35+
```csharp
36+
using System.Data.Common;
37+
using Azure.DataApiBuilder.Core.Configurations;
38+
using Azure.DataApiBuilder.Core.Models;
39+
using Azure.DataApiBuilder.Core.Resolvers;
40+
using Microsoft.AspNetCore.Http;
41+
using Microsoft.Extensions.Logging;
42+
using Oracle.ManagedDataAccess.Client;
43+
44+
public class OracleQueryExecutor : QueryExecutor<OracleConnection>
45+
{
46+
public OracleQueryExecutor(
47+
DbExceptionParser dbExceptionParser,
48+
ILogger<IQueryExecutor> logger,
49+
RuntimeConfigProvider configProvider,
50+
IHttpContextAccessor httpContextAccessor,
51+
HotReloadEventHandler<HotReloadEventArgs>? handler)
52+
: base(dbExceptionParser, logger, configProvider, httpContextAccessor, handler)
53+
{
54+
}
55+
56+
// Override only the methods that need Oracle-specific behavior.
57+
// All other virtual methods (CreateConnection, PrepareDbCommand, etc.)
58+
// fall back to the base-class implementations.
59+
60+
/// <inheritdoc/>
61+
public override async Task<TResult?> ExecuteQueryAsync<TResult>(
62+
string sqltext,
63+
IDictionary<string, DbConnectionParam> parameters,
64+
Func<DbDataReader, List<string>?, Task<TResult>>? dataReaderHandler,
65+
string dataSourceName,
66+
HttpContext? httpContext = null,
67+
List<string>? args = null)
68+
{
69+
// Add Oracle-specific pre/post processing here if needed.
70+
return await base.ExecuteQueryAsync(
71+
sqltext,
72+
parameters,
73+
dataReaderHandler,
74+
dataSourceName,
75+
httpContext,
76+
args);
77+
}
78+
}
79+
```
80+
81+
---
82+
83+
## 3. Common pitfalls
84+
85+
### Missing generic type argument
86+
87+
```csharp
88+
// ❌ WRONG — "QueryExecutor" without a type argument is an open generic type
89+
// and cannot be used as a base class directly.
90+
public class OracleQueryExecutor : QueryExecutor
91+
92+
// ✅ CORRECT — provide the concrete connection type
93+
public class OracleQueryExecutor : QueryExecutor<OracleConnection>
94+
```
95+
96+
### Missing `<TResult>` on the override method name
97+
98+
```csharp
99+
// ❌ WRONG — "ExecuteQueryAsync(" without <TResult> declares a new non-generic
100+
// method; it does not override the base class generic method.
101+
public override async Task<TResult?> ExecuteQueryAsync(
102+
103+
// ✅ CORRECT — include <TResult> so the compiler matches the base signature
104+
public override async Task<TResult?> ExecuteQueryAsync<TResult>(
105+
```
106+
107+
### Missing `<Nullable>enable</Nullable>` in the project file
108+
109+
Without this setting, `TResult?` on an unconstrained generic type parameter is not
110+
valid C# syntax and produces:
111+
112+
```text
113+
Cannot implicitly convert type 'TResult' to 'TResult?'.
114+
An explicit conversion exists (are you missing a cast?)
115+
```
116+
117+
Add `<Nullable>enable</Nullable>` to the `<PropertyGroup>` of your `.csproj` file.
118+
119+
---
120+
121+
## 4. Registering the custom executor
122+
123+
Wire up your executor in the dependency-injection container during startup, replacing
124+
the default executor for your data source type:
125+
126+
```csharp
127+
services.AddSingleton<IQueryExecutor, OracleQueryExecutor>();
128+
```
129+
130+
---
131+
132+
## 5. Supported override points
133+
134+
`QueryExecutor<TConnection>` exposes the following `virtual` members that you can
135+
override to customize behavior:
136+
137+
| Member | Description |
138+
|---|---|
139+
| `CreateConnection(string)` | Create and return a new database connection. |
140+
| `ExecuteQueryAsync<TResult>(...)` | Async query execution with retry. |
141+
| `ExecuteQuery<TResult>(...)` | Sync query execution with retry. |
142+
| `ExecuteQueryAgainstDbAsync<TResult>(...)` | Async execution against an open connection. |
143+
| `ExecuteQueryAgainstDb<TResult>(...)` | Sync execution against an open connection. |
144+
| `PrepareDbCommand(...)` | Build the `DbCommand` from SQL text and parameters. |
145+
| `GetSessionParamsQuery(...)` | Return optional session-context SQL to prepend. |
146+
| `PopulateDbTypeForParameter(...)` | Set the `DbType` on a `DbParameter`. |
147+
| `SetManagedIdentityAccessTokenIfAnyAsync(...)` | Attach a Managed Identity token to the connection. |
148+
| `SetManagedIdentityAccessTokenIfAny(...)` | Sync variant of the above. |
149+
| `GetMultipleResultSetsIfAnyAsync(...)` | Process multiple result sets from a reader. |
150+
| `ConnectionStringBuilders` | Dictionary of data-source-nameconnection-string builder. |

src/Core/Resolvers/QueryExecutor.cs

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,73 @@
2020
namespace Azure.DataApiBuilder.Core.Resolvers
2121
{
2222
/// <summary>
23-
/// Encapsulates query execution apis.
23+
/// Encapsulates query execution APIs for a specific database connection type.
2424
/// </summary>
25+
/// <typeparam name="TConnection">
26+
/// The <see cref="DbConnection"/> subtype for the target database (for example,
27+
/// <c>SqlConnection</c> for SQL Server, <c>NpgsqlConnection</c> for PostgreSQL,
28+
/// or <c>OracleConnection</c> for Oracle). The type must be a concrete class with
29+
/// a public parameterless constructor.
30+
/// </typeparam>
31+
/// <remarks>
32+
/// <para>
33+
/// To add support for a new database type, create a class that inherits from
34+
/// <c>QueryExecutor&lt;TConnection&gt;</c>, substituting the concrete connection type
35+
/// for <c>TConnection</c>. For example:
36+
/// </para>
37+
/// <code language="csharp">
38+
/// // The project file must enable nullable reference types:
39+
/// // &lt;Nullable&gt;enable&lt;/Nullable&gt;
40+
///
41+
/// public class OracleQueryExecutor : QueryExecutor&lt;OracleConnection&gt;
42+
/// {
43+
/// public OracleQueryExecutor(
44+
/// DbExceptionParser dbExceptionParser,
45+
/// ILogger&lt;IQueryExecutor&gt; logger,
46+
/// RuntimeConfigProvider configProvider,
47+
/// IHttpContextAccessor httpContextAccessor,
48+
/// HotReloadEventHandler&lt;HotReloadEventArgs&gt;? handler)
49+
/// : base(dbExceptionParser, logger, configProvider, httpContextAccessor, handler)
50+
/// {
51+
/// }
52+
///
53+
/// // Override only the methods that require Oracle-specific behavior.
54+
/// // Note: the method name must include the generic type parameter &lt;TResult&gt;,
55+
/// // and the project must have &lt;Nullable&gt;enable&lt;/Nullable&gt; for TResult? to compile.
56+
/// public override async Task&lt;TResult?&gt; ExecuteQueryAsync&lt;TResult&gt;(
57+
/// string sqltext,
58+
/// IDictionary&lt;string, DbConnectionParam&gt; parameters,
59+
/// Func&lt;DbDataReader, List&lt;string&gt;?, Task&lt;TResult&gt;&gt;? dataReaderHandler,
60+
/// string dataSourceName,
61+
/// HttpContext? httpContext = null,
62+
/// List&lt;string&gt;? args = null)
63+
/// {
64+
/// // Add Oracle-specific logic here, then delegate to the base implementation.
65+
/// return await base.ExecuteQueryAsync(
66+
/// sqltext, parameters, dataReaderHandler, dataSourceName, httpContext, args);
67+
/// }
68+
/// }
69+
/// </code>
70+
/// <para>
71+
/// <strong>Common pitfalls:</strong>
72+
/// <list type="bullet">
73+
/// <item><description>
74+
/// Omitting the generic type argument — <c>: QueryExecutor</c> is invalid;
75+
/// the correct form is <c>: QueryExecutor&lt;OracleConnection&gt;</c>.
76+
/// </description></item>
77+
/// <item><description>
78+
/// Omitting <c>&lt;TResult&gt;</c> on the override method name —
79+
/// <c>ExecuteQueryAsync(</c> will not be recognized as an override of the
80+
/// base generic method; use <c>ExecuteQueryAsync&lt;TResult&gt;(</c>.
81+
/// </description></item>
82+
/// <item><description>
83+
/// Missing <c>&lt;Nullable&gt;enable&lt;/Nullable&gt;</c> in the project file —
84+
/// without this setting, <c>TResult?</c> on an unconstrained type parameter
85+
/// produces a compile error.
86+
/// </description></item>
87+
/// </list>
88+
/// </para>
89+
/// </remarks>
2590
public class QueryExecutor<TConnection> : IQueryExecutor
2691
where TConnection : DbConnection, new()
2792
{
@@ -163,6 +228,14 @@ public QueryExecutor(DbExceptionParser dbExceptionParser,
163228
}
164229

165230
/// <inheritdoc/>
231+
/// <remarks>
232+
/// When overriding this method in a derived class, ensure that:
233+
/// <list type="bullet">
234+
/// <item><description>The method name includes the generic type parameter: <c>ExecuteQueryAsync&lt;TResult&gt;</c>.</description></item>
235+
/// <item><description>The return type is <c>Task&lt;TResult?&gt;</c> (nullable).</description></item>
236+
/// <item><description>The containing project has <c>&lt;Nullable&gt;enable&lt;/Nullable&gt;</c> in its .csproj file.</description></item>
237+
/// </list>
238+
/// </remarks>
166239
public virtual async Task<TResult?> ExecuteQueryAsync<TResult>(
167240
string sqltext,
168241
IDictionary<string, DbConnectionParam> parameters,

0 commit comments

Comments
 (0)