docs(ipsen-movein): rewrite task plan to async QuerySingleAsync helper
Match the shipped InboundDatabaseHelper throughout the implementation plan: QuerySingle->QuerySingleAsync (and Query->QueryAsync) — async signatures (async Task<T?>), awaited async ADO.NET (ExecuteScalarAsync/ExecuteReaderAsync/ ReadAsync with the deadline token), async Task test methods with await, and the architecture/step/acceptance prose + pseudocode now call await Database.QuerySingleAsync<T>(...). Sibling fix to the design-doc correction.
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
**Goal:** Turn the stub `POST /api/IpsenMESMoveIn` into a real MoveIn that resolves the SAP number to a reactor instance via BTDB, then writes the MoveIn onto the correct Left/Right MES-receiver child — gated on that receiver's `MoveInReadyFlag`.
|
**Goal:** Turn the stub `POST /api/IpsenMESMoveIn` into a real MoveIn that resolves the SAP number to a reactor instance via BTDB, then writes the MoveIn onto the correct Left/Right MES-receiver child — gated on that receiver's `MoveInReadyFlag`.
|
||||||
|
|
||||||
**Architecture:** The inbound `/api` script gains a scoped, read-only DB helper (`Database.QuerySingle`) so it can do the `dbo.Machine.SAPID → Code` lookup, derive the bare instance code, then `Route.To(instance).Call("IpsenMoveIn", {…params, side})`. A new on-demand template script `IpsenMoveIn` on the reactor template (`IpsenFurnaceTitan`, T1) gates on `MoveInReadyFlag` and writes the fields onto `Children["{side}MESReceiver"]` (DCL → Galaxy authorized write).
|
**Architecture:** The inbound `/api` script gains a scoped, async DB helper (`await Database.QuerySingleAsync`) so it can do the `dbo.Machine.SAPID → Code` lookup, derive the bare instance code, then `Route.To(instance).Call("IpsenMoveIn", {…params, side})`. A new on-demand template script `IpsenMoveIn` on the reactor template (`IpsenFurnaceTitan`, T1) gates on `MoveInReadyFlag` and writes the fields onto `Children["{side}MESReceiver"]` (DCL → Galaxy authorized write).
|
||||||
|
|
||||||
**Tech Stack:** .NET 10, Roslyn CSharpScript (inbound + template scripts), Akka.NET (Instance/Script actors), `IDatabaseGateway`/ADO.NET (SQL Server BTDB), ScadaBridge management API (`POST /management`).
|
**Tech Stack:** .NET 10, Roslyn CSharpScript (inbound + template scripts), Akka.NET (Instance/Script actors), `IDatabaseGateway`/ADO.NET (SQL Server BTDB), ScadaBridge management API (`POST /management`).
|
||||||
|
|
||||||
@@ -62,19 +62,19 @@ public class InboundDatabaseHelperTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void QuerySingle_returns_first_column_with_bound_parameter()
|
public async Task QuerySingleAsync_returns_first_column_with_bound_parameter()
|
||||||
{
|
{
|
||||||
var helper = new InboundDatabaseHelper(SeededGateway(), CancellationToken.None);
|
var helper = new InboundDatabaseHelper(SeededGateway(), CancellationToken.None);
|
||||||
var code = helper.QuerySingle<string>("BTDB",
|
var code = await helper.QuerySingleAsync<string>("BTDB",
|
||||||
"SELECT Code FROM Machine WHERE SAPID=@s", new { s = "131453" });
|
"SELECT Code FROM Machine WHERE SAPID=@s", new { s = "131453" });
|
||||||
Assert.Equal("Z28061A", code);
|
Assert.Equal("Z28061A", code);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void QuerySingle_returns_default_when_no_rows()
|
public async Task QuerySingleAsync_returns_default_when_no_rows()
|
||||||
{
|
{
|
||||||
var helper = new InboundDatabaseHelper(SeededGateway(), CancellationToken.None);
|
var helper = new InboundDatabaseHelper(SeededGateway(), CancellationToken.None);
|
||||||
var code = helper.QuerySingle<string>("BTDB",
|
var code = await helper.QuerySingleAsync<string>("BTDB",
|
||||||
"SELECT Code FROM Machine WHERE SAPID=@s", new { s = "999999" });
|
"SELECT Code FROM Machine WHERE SAPID=@s", new { s = "999999" });
|
||||||
Assert.Null(code);
|
Assert.Null(code);
|
||||||
}
|
}
|
||||||
@@ -94,10 +94,11 @@ using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
|||||||
namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Read-only database access exposed to inbound API scripts. All ADO.NET stays
|
/// Async database access exposed to inbound API scripts. All ADO.NET stays
|
||||||
/// internal here — scripts call QuerySingle/Query by name and never reference
|
/// internal here — scripts call (and await) QuerySingleAsync/QueryAsync by name and
|
||||||
/// System.Data. Named connections only; parameters are bound (anonymous-object
|
/// never reference System.Data. Named connections only; parameters are bound
|
||||||
/// properties become @-prefixed SQL parameters), never string-concatenated.
|
/// (anonymous-object properties become @-prefixed SQL parameters), never
|
||||||
|
/// string-concatenated.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class InboundDatabaseHelper
|
public sealed class InboundDatabaseHelper
|
||||||
{
|
{
|
||||||
@@ -108,29 +109,29 @@ public sealed class InboundDatabaseHelper
|
|||||||
{ _gateway = gateway; _ct = ct; }
|
{ _gateway = gateway; _ct = ct; }
|
||||||
|
|
||||||
/// <summary>First column of the first row converted to T (default if no rows).</summary>
|
/// <summary>First column of the first row converted to T (default if no rows).</summary>
|
||||||
public T? QuerySingle<T>(string connectionName, string sql, object? parameters = null)
|
public async Task<T?> QuerySingleAsync<T>(string connectionName, string sql, object? parameters = null)
|
||||||
{
|
{
|
||||||
using var conn = _gateway.GetConnectionAsync(connectionName, _ct).GetAwaiter().GetResult();
|
await using var conn = await _gateway.GetConnectionAsync(connectionName, _ct);
|
||||||
using var cmd = conn.CreateCommand();
|
await using var cmd = conn.CreateCommand();
|
||||||
cmd.CommandText = sql;
|
cmd.CommandText = sql;
|
||||||
AddParameters(cmd, parameters);
|
AddParameters(cmd, parameters);
|
||||||
var result = cmd.ExecuteScalar();
|
var result = await cmd.ExecuteScalarAsync(_ct);
|
||||||
if (result is null or DBNull) return default;
|
if (result is null or DBNull) return default;
|
||||||
if (result is T t) return t;
|
if (result is T t) return t;
|
||||||
return (T)Convert.ChangeType(result, typeof(T), CultureInfo.InvariantCulture);
|
return (T)Convert.ChangeType(result, typeof(T), CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>All rows as column→value dictionaries (case-insensitive keys).</summary>
|
/// <summary>All rows as column→value dictionaries (case-insensitive keys).</summary>
|
||||||
public IReadOnlyList<IReadOnlyDictionary<string, object?>> Query(
|
public async Task<IReadOnlyList<IReadOnlyDictionary<string, object?>>> QueryAsync(
|
||||||
string connectionName, string sql, object? parameters = null)
|
string connectionName, string sql, object? parameters = null)
|
||||||
{
|
{
|
||||||
using var conn = _gateway.GetConnectionAsync(connectionName, _ct).GetAwaiter().GetResult();
|
await using var conn = await _gateway.GetConnectionAsync(connectionName, _ct);
|
||||||
using var cmd = conn.CreateCommand();
|
await using var cmd = conn.CreateCommand();
|
||||||
cmd.CommandText = sql;
|
cmd.CommandText = sql;
|
||||||
AddParameters(cmd, parameters);
|
AddParameters(cmd, parameters);
|
||||||
using var reader = cmd.ExecuteReader();
|
await using var reader = await cmd.ExecuteReaderAsync(_ct);
|
||||||
var rows = new List<IReadOnlyDictionary<string, object?>>();
|
var rows = new List<IReadOnlyDictionary<string, object?>>();
|
||||||
while (reader.Read())
|
while (await reader.ReadAsync(_ct))
|
||||||
{
|
{
|
||||||
var row = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
var row = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||||
for (var i = 0; i < reader.FieldCount; i++)
|
for (var i = 0; i < reader.FieldCount; i++)
|
||||||
@@ -162,7 +163,7 @@ public sealed class InboundDatabaseHelper
|
|||||||
**Step 5 — Commit:**
|
**Step 5 — Commit:**
|
||||||
`git add src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundDatabaseHelper.cs tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/InboundDatabaseHelperTests.cs && git commit -m "feat(inbound): read-only InboundDatabaseHelper for inbound scripts"`
|
`git add src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundDatabaseHelper.cs tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/InboundDatabaseHelperTests.cs && git commit -m "feat(inbound): read-only InboundDatabaseHelper for inbound scripts"`
|
||||||
|
|
||||||
**Acceptance:** `QuerySingle<T>` binds anonymous-object params, returns first column or default; ADO.NET stays internal.
|
**Acceptance:** `QuerySingleAsync<T>` binds anonymous-object params and (awaited) returns first column or default; ADO.NET stays internal.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -176,7 +177,7 @@ public sealed class InboundDatabaseHelper
|
|||||||
- Modify: `src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs` (the `InboundScriptContext` class ~321-353; the `ExecuteAsync` context construction ~246-249; add `using Microsoft.Extensions.DependencyInjection;`)
|
- Modify: `src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs` (the `InboundScriptContext` class ~321-353; the `ExecuteAsync` context construction ~246-249; add `using Microsoft.Extensions.DependencyInjection;`)
|
||||||
- Test: `tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/InboundScriptExecutorTests.cs` (add cases)
|
- Test: `tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/InboundScriptExecutorTests.cs` (add cases)
|
||||||
|
|
||||||
**Step 1 — Write failing tests:** (a) a script body `return new { v = Database.QuerySingle<string>("BTDB","SELECT Code FROM Machine WHERE SAPID=@s", new { s = (string)Parameters["sap"] }) };` resolves via a seeded SQLite gateway; (b) an existing script that does NOT use `Database` still compiles + runs (no regression). Use the same SQLite-gateway fake from Task 1; construct the executor with a real `IServiceProvider` (e.g. `new ServiceCollection().AddScoped<IDatabaseGateway>(_ => gateway).BuildServiceProvider()`).
|
**Step 1 — Write failing tests:** (a) a script body `return new { v = await Database.QuerySingleAsync<string>("BTDB","SELECT Code FROM Machine WHERE SAPID=@s", new { s = (string)Parameters["sap"] }) };` resolves via a seeded SQLite gateway; (b) an existing script that does NOT use `Database` still compiles + runs (no regression). Use the same SQLite-gateway fake from Task 1; construct the executor with a real `IServiceProvider` (e.g. `new ServiceCollection().AddScoped<IDatabaseGateway>(_ => gateway).BuildServiceProvider()`).
|
||||||
|
|
||||||
**Step 2 — Run, expect FAIL** (`Database` not a member of context).
|
**Step 2 — Run, expect FAIL** (`Database` not a member of context).
|
||||||
|
|
||||||
@@ -218,14 +219,14 @@ var context = new InboundScriptContext(
|
|||||||
cts.Token);
|
cts.Token);
|
||||||
```
|
```
|
||||||
|
|
||||||
Add `using Microsoft.Extensions.DependencyInjection;` at the top. No `ScriptOptions.WithReferences` change is needed — `InboundDatabaseHelper` lives in the same assembly as `RouteHelper`, which is already referenced (line ~157). Leave imports as-is (script calls `Database.QuerySingle`, fully qualified through the globals object; no `using` required).
|
Add `using Microsoft.Extensions.DependencyInjection;` at the top. No `ScriptOptions.WithReferences` change is needed — `InboundDatabaseHelper` lives in the same assembly as `RouteHelper`, which is already referenced (line ~157). Leave imports as-is (script calls `await Database.QuerySingleAsync`, fully qualified through the globals object; no `using` required).
|
||||||
|
|
||||||
**Step 4 — Run tests, expect PASS** (both the Database-using script and the no-Database script).
|
**Step 4 — Run tests, expect PASS** (both the Database-using script and the no-Database script).
|
||||||
|
|
||||||
**Step 5 — Commit:**
|
**Step 5 — Commit:**
|
||||||
`git add -p` the two files; `git commit -m "feat(inbound): expose read-only Database helper on InboundScriptContext"`
|
`git add -p` the two files; `git commit -m "feat(inbound): expose read-only Database helper on InboundScriptContext"`
|
||||||
|
|
||||||
**Acceptance:** inbound scripts can call `Database.QuerySingle`; the scope is disposed after each execution; existing no-DB scripts still compile + run; `ScriptTrustValidator` still passes (no forbidden API in the helper-call text).
|
**Acceptance:** inbound scripts can call `await Database.QuerySingleAsync`; the scope is disposed after each execution; existing no-DB scripts still compile + run; `ScriptTrustValidator` still passes (no forbidden API in the helper-call text).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -296,7 +297,7 @@ try {
|
|||||||
if (side == null)
|
if (side == null)
|
||||||
return new { WasSuccessful = false, ErrorText = "Unsupported side suffix '" + suf + "' (only -A/-B supported)", BatchID = 0 };
|
return new { WasSuccessful = false, ErrorText = "Unsupported side suffix '" + suf + "' (only -A/-B supported)", BatchID = 0 };
|
||||||
|
|
||||||
var code = Database.QuerySingle<string>("BTDB",
|
var code = await Database.QuerySingleAsync<string>("BTDB",
|
||||||
"SELECT TOP 1 Code FROM dbo.Machine WHERE SAPID=@s", new { s = sap });
|
"SELECT TOP 1 Code FROM dbo.Machine WHERE SAPID=@s", new { s = sap });
|
||||||
if (string.IsNullOrEmpty(code))
|
if (string.IsNullOrEmpty(code))
|
||||||
return new { WasSuccessful = false, ErrorText = "No machine found for SAP " + sap, BatchID = 0 };
|
return new { WasSuccessful = false, ErrorText = "No machine found for SAP " + sap, BatchID = 0 };
|
||||||
|
|||||||
Reference in New Issue
Block a user