PostgreSQL · Create

Create without a catalog

A service created with only a connection string, in four scenarios: ARX reads the table's id column and column types from the database before the create, for a generated id and for a supplied text id; then the two tables it can't resolve an id for, one with no primary key and one with a compound primary key, refused with ARX1019, and the one line that fixes each.

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.

Before scenario 1 the sample runs this, so the service and the rows the scenarios use are there:

Set up
var service = new ArxPgDbService(connectionString);

1 A generated id, read from the database

The usual case: account's primary key is a bigserial, so ARX knows the id is generated and returns it.

Already in scope: service (ArxPgDbService)

C#
var entity = new ArxEntity("account");
entity["name"] = "Adatum Corporation";
entity["account_number"] = "ACC-N001";
entity["employee_count"] = 120;

ArxRecordId id = await service.CreateAsync(entity);

Console.WriteLine($"created account {id.AsInt64()} (id column \"{id.Column}\")");

Check it in the database

SQL

Run it in any SQL tool connected to the database, after the code has run.

SELECT id, name, employee_count
FROM account
WHERE account_number = 'ACC-N001';

Expect: 1 row, Adatum Corporation, employee_count 120.

2 A supplied text id

country's primary key is a char(2) with no default, so the id is supplied: set it like any attribute.

Already in scope: service (ArxPgDbService)

C#
var norway = new ArxEntity("country");
norway["code"] = "NO";
norway["name"] = "Norway";

ArxRecordId id = await service.CreateAsync(norway);

Console.WriteLine($"created country {id.AsString()}");

Check it in the database

SQL

Run it in any SQL tool connected to the database, after the code has run.

SELECT code, name
FROM country
WHERE code = 'NO';

Expect: 1 row, Norway.

3 A table with no primary key

activity_log has no primary key: ARX1019, and the fix, declaring that on the entity.

Already in scope: service (ArxPgDbService)

C#
var entry = new ArxEntity("activity_log");
entry["action"] = "exported the price list";

try
{
    await service.CreateAsync(entry);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.TableNotResolvable)
{
    Console.WriteLine($"refused: {error.Message}");
}

// Say so on the entity (or in a catalog): the row is written and there is no id to return.
entry.IdColumn = ArxIdColumn.NoIdColumn;
ArxRecordId none = await service.CreateAsync(entry);

Console.WriteLine($"logged \"{entry["action"]}\" (id {none})");

Check it in the database

SQL

Run it in any SQL tool connected to the database, after the code has run.

SELECT action, logged_at
FROM activity_log
WHERE action = 'exported the price list';

Expect: 1 row (the refused call wrote nothing).

4 A compound primary key

price_list's primary key is two columns (region, sku): no single id, so ARX1019 too, with that reason.

Already in scope: service (ArxPgDbService)

C#
var price = new ArxEntity("price_list");
price["region"] = "NO";
price["sku"] = "SKU-N00001";
price["unit_price"] = 12.00m;
price["valid_from"] = new DateOnly(2026, 1, 1);

try
{
    await service.CreateAsync(price);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.TableNotResolvable)
{
    Console.WriteLine($"refused: {error.Message}");
}

// The region and SKU identify the row: declare no id column, and the two as its key.
price.IdColumn = ArxIdColumn.NoIdColumn;
price.Keys = new[] { new ArxKey("region", "sku") };
await service.CreateAsync(price);

Console.WriteLine($"created price region {price["region"]}, sku {price["sku"]}");

Check it in the database

SQL

Run it in any SQL tool connected to the database, after the code has run.

SELECT region, sku, unit_price
FROM price_list
WHERE sku = 'SKU-N00001';

Expect: 1 row, NO, unit_price 12.00.

Good to know

Without a catalog, every call first reads the tables it writes from the database: one more round trip per call, so it is much slower than a service with a catalog. It is meant for simple CRUD in small applications and scripts; for anything more, declare the catalog or preload it (Create Advanced6), or remember what was read (Create Advanced10).

ARX still never guesses: it uses the primary key the table declares. A table with no primary key, a compound one, or one of a type an id can't be is ARX1019 TableNotResolvable when the call needs the id, with the reason and the fix in the message.

A service created with a catalog never resolves: a table missing from it is still ARX1016 IdNotDeclared.

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

  1. .NET SDK: .NET SDK 10.0.100 or later 10.0.x (target net10.0).
  2. Create the project:
    Terminal
    dotnet new console -n ArxSample -f net10.0
    cd ArxSample
  3. Install the packages:
    Terminal
    dotnet add package ARX.SDK.PostgreSql --version 1.0.0-rc.1
  4. Database: a PostgreSQL 16 (tested with 16.15) database with the sample schema.
  5. Configuration: set ARX_SAMPLES_POSTGRESQL to its connection string. Nothing is hard-coded.
  6. Source code: replace Program.cs and add the 3 other files below, with the file names shown.
  7. Run it:
    Terminal
    dotnet run
  8. Expected output: below.

The files

Program.cs

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 Basic6_WithoutACatalog().RunAsync(new SampleContext(connectionString));
Basic6_WithoutACatalog.cs

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>
/// A service created with only a connection string, in four scenarios: ARX reads the table's id column and column types
/// from the database before the create, for a generated id and for a supplied text id; then the two tables it can't
/// resolve an id for, one with no primary key and one with a compound primary key, refused with ARX1019, and the one
/// line that fixes each.
/// </summary>
/// <remarks>
/// <para>Without a catalog, every call first reads the tables it writes from the database: one more round trip per call,
/// so it is much slower than a service with a catalog. It is meant for simple CRUD in small applications and scripts;
/// for anything more, declare the catalog or preload it (Create Advanced6), or remember what was read (Create
/// Advanced10).</para>
/// <para>ARX still never guesses: it uses the primary key the table declares. A table with no primary key, a compound
/// one, or one of a type an id can't be is ARX1019 TableNotResolvable when the call needs the id, with the reason and
/// the fix in the message.</para>
/// <para>A service created with a catalog never resolves: a table missing from it is still ARX1016 IdNotDeclared.</para>
/// </remarks>
public sealed class Basic6_WithoutACatalog : ISample
{
    public string Name => "create-basics-6-without-a-catalog";

    public string Title => "Create without a catalog";

    public async Task RunAsync(SampleContext dbContext)
    {
        var service = new ArxPgDbService(dbContext.ConnectionString);

        await Scenario1_AGeneratedIdAsync(service);

        await Scenario2_ASuppliedTextIdAsync(service);

        await Scenario3_ATableWithNoPrimaryKeyAsync(service);

        await Scenario4_ACompoundPrimaryKeyAsync(service);
    }

    /// <summary>1. The usual case: account's primary key is a bigserial, so ARX knows the id is generated and returns it.</summary>
    private static async Task Scenario1_AGeneratedIdAsync(ArxPgDbService service)
    {
        Step("1. A generated id, read from the database");

        var entity = new ArxEntity("account");
        entity["name"] = "Adatum Corporation";
        entity["account_number"] = "ACC-N001";
        entity["employee_count"] = 120;

        ArxRecordId id = await service.CreateAsync(entity);

        Show($"created account {id.AsInt64()} (id column \"{id.Column}\")");

        /* ===========================================================
         * [DIRECT QUERY CHECK]
         * -----------------------------------------------------------
         * SELECT id, name, employee_count
         * FROM account
         * WHERE account_number = 'ACC-N001';
         * -- Expect: 1 row, Adatum Corporation, employee_count 120.
         * ===========================================================
         */
    }

    /// <summary>2. country's primary key is a char(2) with no default, so the id is supplied: set it like any attribute.</summary>
    private static async Task Scenario2_ASuppliedTextIdAsync(ArxPgDbService service)
    {
        Step("2. A supplied text id");

        var norway = new ArxEntity("country");
        norway["code"] = "NO";
        norway["name"] = "Norway";

        ArxRecordId id = await service.CreateAsync(norway);

        Show($"created country {id.AsString()}");

        /* ===========================================================
         * [DIRECT QUERY CHECK]
         * -----------------------------------------------------------
         * SELECT code, name
         * FROM country
         * WHERE code = 'NO';
         * -- Expect: 1 row, Norway.
         * ===========================================================
         */
    }

    /// <summary>3. activity_log has no primary key: ARX1019, and the fix, declaring that on the entity.</summary>
    private static async Task Scenario3_ATableWithNoPrimaryKeyAsync(ArxPgDbService service)
    {
        Step("3. A table with no primary key");

        var entry = new ArxEntity("activity_log");
        entry["action"] = "exported the price list";

        try
        {
            await service.CreateAsync(entry);
        }
        catch (ArxValidationException error) when (error.Code == ArxErrorCode.TableNotResolvable)
        {
            Show($"refused: {error.Message}");
        }

        // Say so on the entity (or in a catalog): the row is written and there is no id to return.
        entry.IdColumn = ArxIdColumn.NoIdColumn;
        ArxRecordId none = await service.CreateAsync(entry);

        Show($"logged \"{entry["action"]}\" (id {none})");

        /* ===========================================================
         * [DIRECT QUERY CHECK]
         * -----------------------------------------------------------
         * SELECT action, logged_at
         * FROM activity_log
         * WHERE action = 'exported the price list';
         * -- Expect: 1 row (the refused call wrote nothing).
         * ===========================================================
         */
    }

    /// <summary>4. price_list's primary key is two columns (region, sku): no single id, so ARX1019 too, with that reason.</summary>
    private static async Task Scenario4_ACompoundPrimaryKeyAsync(ArxPgDbService service)
    {
        Step("4. A compound primary key");

        var price = new ArxEntity("price_list");
        price["region"] = "NO";
        price["sku"] = "SKU-N00001";
        price["unit_price"] = 12.00m;
        price["valid_from"] = new DateOnly(2026, 1, 1);

        try
        {
            await service.CreateAsync(price);
        }
        catch (ArxValidationException error) when (error.Code == ArxErrorCode.TableNotResolvable)
        {
            Show($"refused: {error.Message}");
        }

        // The region and SKU identify the row: declare no id column, and the two as its key.
        price.IdColumn = ArxIdColumn.NoIdColumn;
        price.Keys = new[] { new ArxKey("region", "sku") };
        await service.CreateAsync(price);

        Show($"created price region {price["region"]}, sku {price["sku"]}");

        /* ===========================================================
         * [DIRECT QUERY CHECK]
         * -----------------------------------------------------------
         * SELECT region, sku, unit_price
         * FROM price_list
         * WHERE sku = 'SKU-N00001';
         * -- Expect: 1 row, NO, unit_price 12.00.
         * ===========================================================
         */
    }
}
SampleContext.cs

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 -- &lt;name&gt;</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; }
}
SampleOutput.cs

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.

Output
  > 1. A generated id, read from the database
      created account 1 (id column "id")
  > 2. A supplied text id
      created country NO
  > 3. A table with no primary key
      refused: ARX1019 TableNotResolvable: Can't resolve table 'activity_log': it has no primary key, so there is no id for this call to use. Declare it in a schema catalog with .Table("activity_log", ArxIdColumn.NoIdColumn) (Create then returns ArxRecordId.None), find its rows by a key or columns (ArxMatch.Key, ArxMatch.Columns), or give it a primary key.
      logged "exported the price list" (id (none))
  > 4. A compound primary key
      refused: ARX1019 TableNotResolvable: Can't resolve table 'price_list': compound primary key (region, sku), so there is no id for this call to use. Declare it in a schema catalog with .Table("price_list", ArxIdColumn.NoIdColumn) (Create then returns ArxRecordId.None), find its rows by a key or columns (ArxMatch.Key, ArxMatch.Columns), or give it a primary key.
      created price region NO, sku SKU-N00001