PostgreSQL · CreateMultiple

Import a 20,000-item product feed in one call

A supplier's product feed of 20,000 items, imported in one call, in three scenarios: the categories first, the feed itself (you don't split the list: ARX does, and keeps the call all or nothing), and next week's feed with a bad row, which refuses the whole feed.

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); SampleContext.Direct.cs (dbContext.Direct); PGDirectHelper.cs (the checks behind dbContext.Direct).

dbContext.Direct is the samples' own checking code on the plain driver, not part of ARX: leave it out of your application.

1 The categories first, so products can point at them

The categories first, so the products can point at them.

Already in scope: service (ArxPgDbService)

C#
var beverages = new ArxEntity("product_category");
beverages["name"] = "Beverages";

var condiments = new ArxEntity("product_category");
condiments["name"] = "Condiments";

var ids = await service.CreateMultipleAsync(new List<ArxEntity> { beverages, condiments });
Console.WriteLine($"Beverages {ids[0]}, Condiments {ids[1]}");

return (ids[0], ids[1]);

Check it in the database

SQL

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

SELECT id, name
FROM product_category
WHERE name IN ('Beverages', 'Condiments')
ORDER BY id;

Expect: 2 rows.

2 20,000 products in one CreateMultipleAsync

20,000 products in one call, with JSON and array columns.

Already in scope: service (ArxPgDbService), beverages (ArxRecordId), condiments (ArxRecordId)

C#
var products = new List<ArxEntity>();

for (var i = 1; i <= 20_000; i++)
{
    var product = new ArxEntity("product");
    product["sku"] = $"SKU-{i:D6}";
    product["name"] = $"Product {i}";
    product["category_id"] = new ArxEntityReference("product_category", i % 2 == 0 ? beverages : condiments);
    product["unit_price"] = 1.50m + i % 400 * 0.25m;
    product["weight_kg"] = 0.25 + i % 40 * 0.05;
    product["stock_quantity"] = i % 500;
    product["tags"] = new[] { "imported", i % 3 == 0 ? "organic" : "standard" };
    product["attributes"] = new ArxJsonB { SerializedValue = JsonSerializer.Serialize(new { origin = i % 2 == 0 ? "PT" : "BR", shelfLifeDays = 180 }) };
    product["launched_on"] = new DateOnly(2024, 1, 1).AddDays(i % 700);
    products.Add(product);
}

var ids = await service.CreateMultipleAsync(products);

Check it in the database

SQL

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

SELECT count(*) AS products, min(sku) AS first_sku, max(sku) AS last_sku
FROM product
WHERE sku LIKE 'SKU-%';
SELECT sku, unit_price, tags, attributes
FROM product
WHERE sku = 'SKU-000043';

Expect: 20000 products, SKU-000001 to SKU-020000; SKU-000043 has tags {imported,standard}.

3 Next week's feed has a negative price in its last item: nothing from it is stored

Next week's feed has a negative price in its last item: nothing from it is stored.

Already in scope: service (ArxPgDbService)

C#
var nextFeed = new List<ArxEntity>();

for (var i = 1; i <= 5_000; i++)
{
    var product = new ArxEntity("product");
    product["sku"] = $"NEXT-{i:D6}";
    product["name"] = $"New product {i}";
    product["unit_price"] = i == 5_000 ? -1.00m : 9.99m;
    nextFeed.Add(product);
}

try
{
    await service.CreateMultipleAsync(nextFeed);
}
catch (ArxDatabaseException error) when (error.Code == ArxErrorCode.CheckViolation)
{
    Console.WriteLine(error.Message);
}

Check it in the database

SQL

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

SELECT count(*)
FROM product
WHERE sku LIKE 'NEXT-%';

Expect: 0: one negative price refused the whole feed.

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: add the files of the sample application this sample uses (SampleContext.cs, SampleOutput.cs, SampleContext.Application.cs, SampleCatalog.cs, SampleContext.Direct.cs, PGDirectHelper.cs), then replace Program.cs and add the other file 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.CreateMultiple.RealCases;

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

The sample, as it is in the repository.

using ARX.SDK;
using ARX.SDK.Models;
using ARX.SDK.Services;
using System.Text.Json;
using static ArxSamples.PostgreSql.SampleOutput;

namespace ArxSamples.PostgreSql.Operations.CreateMultiple.RealCases;

/// <summary>
/// A supplier's product feed of 20,000 items, imported in one call, in three scenarios: the categories first, the feed
/// itself (you don't split the list: ARX does, and keeps the call all or nothing), and next week's feed with a bad row,
/// which refuses the whole feed.
/// </summary>
public sealed class RealCase2_ImportAProductCatalog : ISample
{
    public string Name => "create-multiple-real-2-import-a-product-catalog";

    public string Title => "Import a 20,000-item product feed in one call";

    public async Task RunAsync(SampleContext dbContext)
    {
        var (beverages, condiments) = await Scenario1_TheCategoriesFirstAsync(dbContext);

        await Scenario2_TwentyThousandProductsInOneCallAsync(dbContext, beverages, condiments);

        await Scenario3_ABadRowRefusesTheWholeFeedAsync(dbContext);
    }

    /// <summary>1. The categories first, so the products can point at them.</summary>
    private static async Task<(ArxRecordId Beverages, ArxRecordId Condiments)> Scenario1_TheCategoriesFirstAsync(SampleContext dbContext)
    {
        Step("1. The categories first, so products can point at them");

        var beverages = new ArxEntity("product_category");
        beverages["name"] = "Beverages";

        var condiments = new ArxEntity("product_category");
        condiments["name"] = "Condiments";

        var ids = await dbContext.Service.CreateMultipleAsync(new List<ArxEntity> { beverages, condiments });
        Show($"Beverages {ids[0]}, Condiments {ids[1]}");

        /* ===========================================================
         * [DIRECT QUERY CHECK]
         * -----------------------------------------------------------
         * SELECT id, name
         * FROM product_category
         * WHERE name IN ('Beverages', 'Condiments')
         * ORDER BY id;
         * -- Expect: 2 rows.
         * ===========================================================
         */

        return (ids[0], ids[1]);
    }

    /// <summary>2. 20,000 products in one call, with JSON and array columns.</summary>
    private static async Task Scenario2_TwentyThousandProductsInOneCallAsync(SampleContext dbContext, ArxRecordId beverages, ArxRecordId condiments)
    {
        Step("2. 20,000 products in one CreateMultipleAsync");

        var products = new List<ArxEntity>();

        for (var i = 1; i <= 20_000; i++)
        {
            var product = new ArxEntity("product");
            product["sku"] = $"SKU-{i:D6}";
            product["name"] = $"Product {i}";
            product["category_id"] = new ArxEntityReference("product_category", i % 2 == 0 ? beverages : condiments);
            product["unit_price"] = 1.50m + i % 400 * 0.25m;
            product["weight_kg"] = 0.25 + i % 40 * 0.05;
            product["stock_quantity"] = i % 500;
            product["tags"] = new[] { "imported", i % 3 == 0 ? "organic" : "standard" };
            product["attributes"] = new ArxJsonB { SerializedValue = JsonSerializer.Serialize(new { origin = i % 2 == 0 ? "PT" : "BR", shelfLifeDays = 180 }) };
            product["launched_on"] = new DateOnly(2024, 1, 1).AddDays(i % 700);
            products.Add(product);
        }

        var ids = await dbContext.Service.CreateMultipleAsync(products);
        Show($"stored {await dbContext.Direct.CountAsync("product"):N0} products, ids {ids[0]} to {ids[^1]}");

        var stored = await dbContext.Direct.TextRowAsync("product", ids[42].AsInt64(), "sku", "unit_price", "tags", "attributes");
        Show($"product 43: {stored["sku"]}, price {stored["unit_price"]}, tags {stored["tags"]}, attributes {stored["attributes"]}");

        /* ===========================================================
         * [DIRECT QUERY CHECK]
         * -----------------------------------------------------------
         * SELECT count(*) AS products, min(sku) AS first_sku, max(sku) AS last_sku
         * FROM product
         * WHERE sku LIKE 'SKU-%';
         *
         * SELECT sku, unit_price, tags, attributes
         * FROM product
         * WHERE sku = 'SKU-000043';
         * -- Expect: 20000 products, SKU-000001 to SKU-020000; SKU-000043 has tags {imported,standard}.
         * ===========================================================
         */
    }

    /// <summary>3. Next week's feed has a negative price in its last item: nothing from it is stored.</summary>
    private static async Task Scenario3_ABadRowRefusesTheWholeFeedAsync(SampleContext dbContext)
    {
        Step("3. Next week's feed has a negative price in its last item: nothing from it is stored");

        var nextFeed = new List<ArxEntity>();

        for (var i = 1; i <= 5_000; i++)
        {
            var product = new ArxEntity("product");
            product["sku"] = $"NEXT-{i:D6}";
            product["name"] = $"New product {i}";
            product["unit_price"] = i == 5_000 ? -1.00m : 9.99m;
            nextFeed.Add(product);
        }

        try
        {
            await dbContext.Service.CreateMultipleAsync(nextFeed);
        }
        catch (ArxDatabaseException error) when (error.Code == ArxErrorCode.CheckViolation)
        {
            ShowError(error);
            Show($"products in the table: still {await dbContext.Direct.CountAsync("product"):N0}");
        }

        /* ===========================================================
         * [DIRECT QUERY CHECK]
         * -----------------------------------------------------------
         * SELECT count(*)
         * FROM product
         * WHERE sku LIKE 'NEXT-%';
         * -- Expect: 0: one negative price refused the whole feed.
         * ===========================================================
         */
    }
}

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. The categories first, so products can point at them
      Beverages 1, Condiments 2
  > 2. 20,000 products in one CreateMultipleAsync
      stored 20,000 products, ids 1 to 20000
      product 43: SKU-000043, price 12.25, tags {imported,standard}, attributes {"origin": "BR", "shelfLifeDays": 180}
  > 3. Next week's feed has a negative price in its last item: nothing from it is stored
      caught ArxDatabaseException: ARX3004 CheckViolation: CreateMultiple on 'product' violates check constraint 'product_unit_price_check'.
      products in the table: still 20,000