PostgreSQL · Create
Where to declare: on the entity, or a schema for one call, never both
Where a table's id column and keys are declared, in three scenarios: on the entity (for a service with no schema), in a schema passed to one call (which overrides the service's schema for that call), and the rule that a table is declared in one place only.
Set up for this page
Besides the sample itself, it uses SampleContext.cs (the dbContext every sample is given); SampleOutput.cs (Step and Show, which print), in the files below.
1 On the entity
A service with no schema: each entity says its id column (none for the price) and its key.
Already in scope: connectionString (string)
// No schema catalog: the country and the price below declare themselves.
var service = new ArxPgDbService(connectionString);
// A price's region must be a country that exists, so add the country first.
var austria = new ArxEntity("country");
austria.IdColumn = ArxIdColumn.Supplied<string>("code");
austria["code"] = "AT";
austria["name"] = "Austria";
await service.CreateAsync(austria);
var price = new ArxEntity("price_list");
price.IdColumn = ArxIdColumn.NoIdColumn;
price.Keys = new[] { new ArxKey("region", "sku") };
price["region"] = new ArxEntityReference("country", "AT"); // a lookup to the country, by its code
price["sku"] = "SKU-000042";
price["unit_price"] = 4.90m;
price["valid_from"] = new DateOnly(2026, 1, 1);
await service.CreateAsync(price);
Console.WriteLine($"created price region {austria["code"]}, sku {price["sku"]}");Check it in the database
Run it in any SQL tool connected to the database, after the code has run.
SELECT p.region, c.name AS country, p.sku, p.unit_price
FROM price_list p
JOIN country c ON c.code = p.region
WHERE p.region = 'AT';Expect: 1 row, Austria, SKU-000042, unit_price 4.90.
2 A schema for one call
The service's schema says the database can generate a device's uuid. This import wants every device to bring its own, so it passes a schema for this one call that declares the id Supplied: it overrides the service's declaration of that table, and ARX then refuses a device without an id.
Already in scope: connectionString (string)
var schema = ArxSchemaCatalog.Create()
.Table("device", ArxIdColumn.Generated<Guid>("id"))
.Build();
var service = new ArxPgDbService(connectionString, schema);
// Only for this import: the same table, declared stricter.
var importSchema = ArxSchemaCatalog.Create()
.Table("device", ArxIdColumn.Supplied<Guid>("id"))
.Build();
var noId = new ArxEntity("device");
noId["serial_number"] = "IMPORT-0001";
try
{
await service.CreateMultipleAsync(new List<ArxEntity> { noId }, importSchema);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.InvalidArgument)
{
Console.WriteLine($"refused before anything was sent ({error.AttributeName} is required)");
}
var withId = new ArxEntity("device");
withId["id"] = Guid.NewGuid();
withId["serial_number"] = "IMPORT-0002";
var ids = await service.CreateMultipleAsync(new List<ArxEntity> { withId }, importSchema);
Console.WriteLine($"created device {ids[0]}");Check it in the database
Run it in any SQL tool connected to the database, after the code has run.
SELECT id, serial_number
FROM device
WHERE serial_number IN ('IMPORT-0001', 'IMPORT-0002');Expect: 1 row, IMPORT-0002: IMPORT-0001 had no id and was refused.
3 One place only
One place only: when there is a schema (the service's, or one passed to the call), an entity that also declares its own id column or keys is refused, so a table is never declared twice.
Already in scope: connectionString (string)
var schema = ArxSchemaCatalog.Create()
.Table("device", ArxIdColumn.Generated<Guid>("id"))
.Build();
var service = new ArxPgDbService(connectionString, schema);
var device = new ArxEntity("device");
device.IdColumn = ArxIdColumn.Generated<Guid>("id"); // declared again on the entity: not allowed
device["serial_number"] = "TWICE-0001";
try
{
await service.CreateAsync(device);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.InvalidArgument)
{
Console.WriteLine($"refused ({error.Code}): the service has a schema, so the entity can't declare its own");
}Check it in the database
Run it in any SQL tool connected to the database, after the code has run.
SELECT count(*)
FROM device
WHERE serial_number = 'TWICE-0001';Expect: 0: refused before anything was sent.
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: replace
Program.csand add the 3 other files 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.Create.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 Basic5_WhereToDeclare().RunAsync(new SampleContext(connectionString));
The sample, as it is in the repository.
using ARX.SDK;
using ARX.SDK.PostgreSql.Services;
using ARX.SDK.Services;
using static ArxSamples.PostgreSql.SampleOutput;
namespace ArxSamples.PostgreSql.Operations.Create.Basics;
/// <summary>
/// Where a table's id column and keys are declared, in three scenarios: on the entity (for a service with no schema),
/// in a schema passed to one call (which overrides the service's schema for that call), and the rule that a table is
/// declared in one place only.
/// </summary>
public sealed class Basic5_WhereToDeclare : ISample
{
public string Name => "create-basics-5-where-to-declare";
public string Title => "Where to declare: on the entity, or a schema for one call, never both";
public async Task RunAsync(SampleContext dbContext)
{
await Scenario1_OnTheEntity_AServiceWithNoSchemaAsync(dbContext.ConnectionString);
await Scenario2_ASchemaForOneCall_OverridesTheServicesAsync(dbContext.ConnectionString);
await Scenario3_OnePlaceOnlyAsync(dbContext.ConnectionString);
}
/// <summary>1. A service with no schema: each entity says its id column (none for the price) and its key.</summary>
private static async Task Scenario1_OnTheEntity_AServiceWithNoSchemaAsync(string connectionString)
{
Step("1. On the entity");
// No schema catalog: the country and the price below declare themselves.
var service = new ArxPgDbService(connectionString);
// A price's region must be a country that exists, so add the country first.
var austria = new ArxEntity("country");
austria.IdColumn = ArxIdColumn.Supplied<string>("code");
austria["code"] = "AT";
austria["name"] = "Austria";
await service.CreateAsync(austria);
var price = new ArxEntity("price_list");
price.IdColumn = ArxIdColumn.NoIdColumn;
price.Keys = new[] { new ArxKey("region", "sku") };
price["region"] = new ArxEntityReference("country", "AT"); // a lookup to the country, by its code
price["sku"] = "SKU-000042";
price["unit_price"] = 4.90m;
price["valid_from"] = new DateOnly(2026, 1, 1);
await service.CreateAsync(price);
Show($"created price region {austria["code"]}, sku {price["sku"]}");
/* ===========================================================
* [DIRECT QUERY CHECK]
* -----------------------------------------------------------
* SELECT p.region, c.name AS country, p.sku, p.unit_price
* FROM price_list p
* JOIN country c ON c.code = p.region
* WHERE p.region = 'AT';
* -- Expect: 1 row, Austria, SKU-000042, unit_price 4.90.
* ===========================================================
*/
}
/// <summary>
/// 2. The service's schema says the database can generate a device's uuid. This import wants every device to bring
/// its own, so it passes a schema for this one call that declares the id Supplied: it overrides the service's
/// declaration of that table, and ARX then refuses a device without an id.
/// </summary>
private static async Task Scenario2_ASchemaForOneCall_OverridesTheServicesAsync(string connectionString)
{
Step("2. A schema for one call");
var schema = ArxSchemaCatalog.Create()
.Table("device", ArxIdColumn.Generated<Guid>("id"))
.Build();
var service = new ArxPgDbService(connectionString, schema);
// Only for this import: the same table, declared stricter.
var importSchema = ArxSchemaCatalog.Create()
.Table("device", ArxIdColumn.Supplied<Guid>("id"))
.Build();
var noId = new ArxEntity("device");
noId["serial_number"] = "IMPORT-0001";
try
{
await service.CreateMultipleAsync(new List<ArxEntity> { noId }, importSchema);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.InvalidArgument)
{
Show($"refused before anything was sent ({error.AttributeName} is required)");
}
var withId = new ArxEntity("device");
withId["id"] = Guid.NewGuid();
withId["serial_number"] = "IMPORT-0002";
var ids = await service.CreateMultipleAsync(new List<ArxEntity> { withId }, importSchema);
Show($"created device {ids[0]}");
/* ===========================================================
* [DIRECT QUERY CHECK]
* -----------------------------------------------------------
* SELECT id, serial_number
* FROM device
* WHERE serial_number IN ('IMPORT-0001', 'IMPORT-0002');
* -- Expect: 1 row, IMPORT-0002: IMPORT-0001 had no id and was refused.
* ===========================================================
*/
}
/// <summary>
/// 3. One place only: when there is a schema (the service's, or one passed to the call), an entity that also declares
/// its own id column or keys is refused, so a table is never declared twice.
/// </summary>
private static async Task Scenario3_OnePlaceOnlyAsync(string connectionString)
{
Step("3. One place only");
var schema = ArxSchemaCatalog.Create()
.Table("device", ArxIdColumn.Generated<Guid>("id"))
.Build();
var service = new ArxPgDbService(connectionString, schema);
var device = new ArxEntity("device");
device.IdColumn = ArxIdColumn.Generated<Guid>("id"); // declared again on the entity: not allowed
device["serial_number"] = "TWICE-0001";
try
{
await service.CreateAsync(device);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.InvalidArgument)
{
Show($"refused ({error.Code}): the service has a schema, so the entity can't declare its own");
}
/* ===========================================================
* [DIRECT QUERY CHECK]
* -----------------------------------------------------------
* SELECT count(*)
* FROM device
* WHERE serial_number = 'TWICE-0001';
* -- Expect: 0: refused before anything was sent.
* ===========================================================
*/
}
}
What every sample is given: the connection string (and the ISample interface the samples implement).
namespace ArxSamples.PostgreSql;
/// <summary>One documentation sample: a scenario a real application has, shown end to end.</summary>
public interface ISample
{
/// <summary>The name to run it alone: <c>dotnet run -- <name></c>.</summary>
string Name { get; }
/// <summary>One line on what it shows.</summary>
string Title { get; }
Task RunAsync(SampleContext dbContext);
}
/// <summary>
/// What a sample is given. Every sample gets the connection string; the application's service and catalog
/// (SampleContext.Application.cs) and the plain Npgsql read-back helper (SampleContext.Direct.cs) live in their own
/// files, so a sample needs only the files it uses.
/// </summary>
public sealed partial class SampleContext
{
public SampleContext(string connectionString)
{
ConnectionString = connectionString;
}
public string ConnectionString { get; }
}
How the samples print: Step for each scenario, Show for what it did, ShowError for an error caught on purpose.
using ARX.SDK;
namespace ArxSamples.PostgreSql;
/// <summary>How the samples print: a numbered step for each scenario, then what it did.</summary>
public static class SampleOutput
{
/// <summary>The start of a scenario: <c>Step("1. By the id Create returned")</c>.</summary>
public static void Step(string text) => Console.WriteLine($" > {text}");
/// <summary>What the scenario did.</summary>
public static void Show(string text) => Console.WriteLine($" {text}");
/// <summary>An ARX error the scenario caught on purpose.</summary>
public static void ShowError(ArxException exception)
=> Console.WriteLine($" caught {exception.GetType().Name}: {exception.Message}");
}
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. On the entity
created price region AT, sku SKU-000042
> 2. A schema for one call
refused before anything was sent (id is required)
created device 0865d6ea-2f66-448e-a4bf-aed13c5b6985
> 3. One place only
refused (InvalidArgument): the service has a schema, so the entity can't declare its own