PostgreSQL · Execute
A procedure that returns tables
A procedure that returns tables, in two scenarios. A PostgreSQL procedure returns rows through cursors: each table is an INOUT refcursor parameter, declared with AddCursorParameter. ARX runs the call and reads every declared cursor in one transaction (two round trips), and returns each table as a result set named after its parameter.
Set up for this page
The scenarios build on the sample application: SampleContext.cs (the dbContext every sample is given); SampleOutput.cs (Step and Show, which print); SampleContext.Application.cs (dbContext.Service, the application's ARX service); SampleCatalog.cs (the tables' ids, keys, lookups and display names).
Before scenario 1 the sample runs this, so the service and the rows the scenarios use are there:
var account = new ArxEntity("account");
account["name"] = "Tables Inc";
account["account_number"] = "ACC-E401";
var accountId = await service.CreateAsync(account);
foreach (var (sku, price) in new[] { ("SKU-E401", 10m), ("SKU-E402", 4.5m) })
{
var product = new ArxEntity("product");
product["sku"] = sku;
product["name"] = $"Product {sku}";
product["unit_price"] = price;
await service.CreateAsync(product);
}
var place = new ArxProcedureExpression("sp_place_order");
place.AddParameter("p_account_id", accountId.AsInt64());
place.AddParameter("p_order_number", "SO-E401");
place.AddParameter("p_skus", new[] { "SKU-E401", "SKU-E402" });
place.AddParameter("p_quantity", 2);
place.AddOutParameter<long>("p_order_id");
place.AddOutParameter<decimal>("p_total");
var orderId = (long)(await service.ExecuteAsync(place)).Outputs["p_order_id"]!;1 One table
sp_order_with_lines asked for its lines only: the other cursor parameter is left at its default.
Already in scope: service (ArxPgDbService), orderId (long)
var procedure = new ArxProcedureExpression("sp_order_with_lines");
procedure.AddParameter("p_order_id", orderId);
procedure.AddCursorParameter("p_lines");
var lines = (await service.ExecuteAsync(procedure)).ResultSets["p_lines"];
Console.WriteLine(string.Join(", ", lines.Entities.Select(x => $"{x["sku"]} x{x["quantity"]} at {x["unit_price"]:N2}")));Check it in the database
Run it in any SQL tool connected to the database, after the code has run.
SELECT p.sku, l.quantity, l.unit_price
FROM order_line l
JOIN sales_order o ON o.id = l.order_id
JOIN product p ON p.id = l.product_id
WHERE o.order_number = 'SO-E401'
ORDER BY p.sku;Expect: 2 rows: SKU-E401 2 10.00, SKU-E402 2 4.50.
2 Two tables, by name
Both tables: the order and its lines, by name (and by position, in declaration order).
Already in scope: service (ArxPgDbService), orderId (long)
var procedure = new ArxProcedureExpression("sp_order_with_lines");
procedure.AddParameter("p_order_id", orderId);
procedure.AddCursorParameter("p_order");
procedure.AddCursorParameter("p_lines");
var result = await service.ExecuteAsync(procedure);
var order = result.ResultSets["p_order"].Entities.Single();
Console.WriteLine($"{order["order_number"]} for {order["account"]}, total {order["total"]:N2}; {result.ResultSets[1].Entities.Count} lines (ResultSets[1] is \"{result.ResultSets[1].EntityName}\")");Check it in the database
Run it in any SQL tool connected to the database, after the code has run.
SELECT o.order_number, a.name, o.total, (SELECT count(*) FROM order_line l WHERE l.order_id = o.id) AS lines
FROM sales_order o
JOIN account a ON a.id = o.account_id
WHERE o.order_number = 'SO-E401';Expect: SO-E401, Tables Inc, 29.00, 2.
Run the complete sample
The code above comes from one program, built in a fresh project and run against the database. To run it yourself, here is every file and step.
The program, its files and how to run it
Run it locally
- .NET SDK: .NET SDK 10.0.100 or later 10.0.x (target
net10.0). - Create the project:
Terminal dotnet new console -n ArxSample -f net10.0 cd ArxSample - Install the packages:
Terminal dotnet add package ARX.SDK.PostgreSql --version 1.0.0-rc.1 - Database: a PostgreSQL 16 (tested with 16.15) database with the sample schema.
- Configuration: set
ARX_SAMPLES_POSTGRESQLto its connection string. Nothing is hard-coded. - Source code: add the files of the sample application this sample uses (
SampleContext.cs,SampleOutput.cs,SampleContext.Application.cs,SampleCatalog.cs), then replaceProgram.csand add the other file below, with the file names shown. - Run it:
Terminal dotnet run - Expected output: below.
The files
Runs the sample. Generated for this page; it reads the connection string from the environment.
using ArxSamples.PostgreSql;
using ArxSamples.PostgreSql.Operations.Execute.Basics;
// The connection string comes from the environment, never from the code (see "Configure the connection").
var connectionString = Environment.GetEnvironmentVariable("ARX_SAMPLES_POSTGRESQL")
?? throw new InvalidOperationException("Set the ARX_SAMPLES_POSTGRESQL environment variable to your PostgreSQL connection string.");
await new Basic4_AProcedureThatReturnsTables().RunAsync(new SampleContext(connectionString));
The sample, as it is in the repository.
using ARX.SDK.Query;
using ARX.SDK.Services;
using static ArxSamples.PostgreSql.SampleOutput;
namespace ArxSamples.PostgreSql.Operations.Execute.Basics;
/// <summary>
/// A procedure that returns tables, in two scenarios. A PostgreSQL procedure returns rows through cursors:
/// each table is an <c>INOUT refcursor</c> parameter, declared with <c>AddCursorParameter</c>. ARX runs the call and reads
/// every declared cursor in one transaction (two round trips), and returns each table as a result set named after its
/// parameter.
/// </summary>
public sealed class Basic4_AProcedureThatReturnsTables : ISample
{
public string Name => "execute-basics-4-a-procedure-that-returns-tables";
public string Title => "A procedure that returns tables";
public async Task RunAsync(SampleContext dbContext)
{
var account = new ArxEntity("account");
account["name"] = "Tables Inc";
account["account_number"] = "ACC-E401";
var accountId = await dbContext.Service.CreateAsync(account);
foreach (var (sku, price) in new[] { ("SKU-E401", 10m), ("SKU-E402", 4.5m) })
{
var product = new ArxEntity("product");
product["sku"] = sku;
product["name"] = $"Product {sku}";
product["unit_price"] = price;
await dbContext.Service.CreateAsync(product);
}
var place = new ArxProcedureExpression("sp_place_order");
place.AddParameter("p_account_id", accountId.AsInt64());
place.AddParameter("p_order_number", "SO-E401");
place.AddParameter("p_skus", new[] { "SKU-E401", "SKU-E402" });
place.AddParameter("p_quantity", 2);
place.AddOutParameter<long>("p_order_id");
place.AddOutParameter<decimal>("p_total");
var orderId = (long)(await dbContext.Service.ExecuteAsync(place)).Outputs["p_order_id"]!;
await Scenario1_OneTableAsync(dbContext, orderId);
await Scenario2_TwoTablesByNameAsync(dbContext, orderId);
}
/// <summary>1. <c>sp_order_with_lines</c> asked for its lines only: the other cursor parameter is left at its default.</summary>
private static async Task Scenario1_OneTableAsync(SampleContext dbContext, long orderId)
{
Step("1. One table");
var procedure = new ArxProcedureExpression("sp_order_with_lines");
procedure.AddParameter("p_order_id", orderId);
procedure.AddCursorParameter("p_lines");
var lines = (await dbContext.Service.ExecuteAsync(procedure)).ResultSets["p_lines"];
Show(string.Join(", ", lines.Entities.Select(x => $"{x["sku"]} x{x["quantity"]} at {x["unit_price"]:N2}")));
/* ===========================================================
* [DIRECT QUERY CHECK]
* -----------------------------------------------------------
* SELECT p.sku, l.quantity, l.unit_price
* FROM order_line l
* JOIN sales_order o ON o.id = l.order_id
* JOIN product p ON p.id = l.product_id
* WHERE o.order_number = 'SO-E401'
* ORDER BY p.sku;
* -- Expect: 2 rows: SKU-E401 2 10.00, SKU-E402 2 4.50.
* ===========================================================
*/
}
/// <summary>2. Both tables: the order and its lines, by name (and by position, in declaration order).</summary>
private static async Task Scenario2_TwoTablesByNameAsync(SampleContext dbContext, long orderId)
{
Step("2. Two tables, by name");
var procedure = new ArxProcedureExpression("sp_order_with_lines");
procedure.AddParameter("p_order_id", orderId);
procedure.AddCursorParameter("p_order");
procedure.AddCursorParameter("p_lines");
var result = await dbContext.Service.ExecuteAsync(procedure);
var order = result.ResultSets["p_order"].Entities.Single();
Show($"{order["order_number"]} for {order["account"]}, total {order["total"]:N2}; {result.ResultSets[1].Entities.Count} lines (ResultSets[1] is \"{result.ResultSets[1].EntityName}\")");
/* ===========================================================
* [DIRECT QUERY CHECK]
* -----------------------------------------------------------
* SELECT o.order_number, a.name, o.total, (SELECT count(*) FROM order_line l WHERE l.order_id = o.id) AS lines
* FROM sales_order o
* JOIN account a ON a.id = o.account_id
* WHERE o.order_number = 'SO-E401';
* -- Expect: SO-E401, Tables Inc, 29.00, 2.
* ===========================================================
*/
}
}
Expected output
What dotnet run printed when the sample was verified, on a freshly created sample schema. Ids differ when the tables already hold rows.
> 1. One table
SKU-E401 x2 at 10.00, SKU-E402 x2 at 4.50
> 2. Two tables, by name
SO-E401 for Tables Inc, total 29.00; 2 lines (ResultSets[1] is "p_lines")