Amazon DynamoDB · Create
Create in an ARX transaction
Writes across tables in one ARX transaction, in two scenarios: committed together, and a commit that fails and writes nothing.
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); SampleContext.Application.cs (dbContext.Service, the application's ARX service); SampleModel.cs (the DynamoDB layout and the schema catalog); SampleContext.Direct.cs (dbContext.Direct); DynamoDirectHelper.cs (the checks behind dbContext.Direct), in the files below.
dbContext.Direct is the samples' own checking code on the plain driver, not part of ARX: leave it out of your application.
1 Committed together
A customer (single table) and a product (its own table) committed together.
Already in scope: service (ArxDynamoDbService)
await using var connection = new ArxDynamoConnection(service);
await using var transaction = await connection.BeginTransactionAsync();
var customer = new ArxEntity("customer");
customer["customer_id"] = "C-D701";
customer["name"] = "Wide World Importers";
await service.CreateAsync(customer, transaction);
var product = new ArxEntity("product");
product["sku"] = "SKU-D701";
product["name"] = "Import crate";
await service.CreateAsync(product, transaction);
await transaction.CommitAsync();
Console.WriteLine("committed the customer and the product");Check it in the database
A PartiQL statement: run it in NoSQL Workbench, or with aws dynamodb execute-statement --statement, after the code has run.
SELECT "name" FROM "product" WHERE "sku" = 'SKU-D701'Expect: 1 item, name "Import crate".
2 A commit that fails
The second customer repeats C-D701: the commit fails and the new product isn't written either.
Already in scope: service (ArxDynamoDbService)
await using var connection = new ArxDynamoConnection(service);
await using var transaction = await connection.BeginTransactionAsync();
var product = new ArxEntity("product");
product["sku"] = "SKU-D702";
await service.CreateAsync(product, transaction);
var duplicate = new ArxEntity("customer");
duplicate["customer_id"] = "C-D701";
await service.CreateAsync(duplicate, transaction);
try
{
await transaction.CommitAsync();
}
catch (ArxDatabaseException exception)
{
Console.WriteLine(exception.Message);
}Check it in the database
A PartiQL statement: run it in NoSQL Workbench, or with aws dynamodb execute-statement --statement, after the code has run.
SELECT "sku" FROM "product" WHERE "sku" = 'SKU-D702'Expect: no items.
Good to know
DynamoDB has no open transaction: ARX collects the writes and sends them as one TransactWriteItems at the commit, all or nothing, across tables, up to 100. A failing write (a key that exists) shows at the commit.
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.DynamoDb --version 1.0.0-rc.1 - Database: a Amazon DynamoDB, or DynamoDB Local for development (tested with DynamoDB Local 2.5.2) database with the sample schema.
- Configuration: set
ARX_SAMPLES_DYNAMODBto its connection string. Nothing is hard-coded. - Source code: replace
Program.csand add the 7 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 and first remakes the sample database from sample-schema.json.
using ArxSamples.DynamoDb;
using ArxSamples.DynamoDb.Operations.Create.AdvancedCases;
// The connection string comes from the environment, never from the code (see "Configure the connection").
var connectionString = Environment.GetEnvironmentVariable("ARX_SAMPLES_DYNAMODB")
?? throw new InvalidOperationException("Set the ARX_SAMPLES_DYNAMODB environment variable to your Amazon DynamoDB connection string.");
var dbContext = new SampleContext(connectionString);
// Every run starts from the sample data: this deletes and makes again what sample-schema.json lists
// (save the file in the project folder, see Get started).
await dbContext.Direct.ResetAsync();
await new Advanced1_ArxTransaction().RunAsync(dbContext);
The sample, as it is in the repository.
using ARX.SDK;
using ARX.SDK.DynamoDb.Services;
using ARX.SDK.Services;
using static ArxSamples.DynamoDb.SampleOutput;
namespace ArxSamples.DynamoDb.Operations.Create.AdvancedCases;
/// <summary>
/// Writes across tables in one ARX transaction, in two scenarios: committed together, and a commit that fails and
/// writes nothing.
/// </summary>
/// <remarks>
/// DynamoDB has no open transaction: ARX collects the writes and sends them as one <c>TransactWriteItems</c> at the
/// commit, all or nothing, across tables, up to 100. A failing write (a key that exists) shows at the commit.
/// </remarks>
public sealed class Advanced1_ArxTransaction : ISample
{
public string Name => "create-advanced-1-arx-transaction";
public string Title => "Create in an ARX transaction";
public async Task RunAsync(SampleContext dbContext)
{
await Scenario1_CommittedTogetherAsync(dbContext.Service);
await Scenario2_ACommitThatFailsAsync(dbContext.Service);
}
/// <summary>1. A customer (single table) and a product (its own table) committed together.</summary>
private static async Task Scenario1_CommittedTogetherAsync(ArxDynamoDbService service)
{
Step("1. Committed together");
await using var connection = new ArxDynamoConnection(service);
await using var transaction = await connection.BeginTransactionAsync();
var customer = new ArxEntity("customer");
customer["customer_id"] = "C-D701";
customer["name"] = "Wide World Importers";
await service.CreateAsync(customer, transaction);
var product = new ArxEntity("product");
product["sku"] = "SKU-D701";
product["name"] = "Import crate";
await service.CreateAsync(product, transaction);
await transaction.CommitAsync();
Show("committed the customer and the product");
/* ===========================================================
* [DIRECT QUERY CHECK]
* -----------------------------------------------------------
* SELECT "name" FROM "product" WHERE "sku" = 'SKU-D701'
* -- Expect: 1 item, name "Import crate".
* ===========================================================
*/
}
/// <summary>2. The second customer repeats C-D701: the commit fails and the new product isn't written either.</summary>
private static async Task Scenario2_ACommitThatFailsAsync(ArxDynamoDbService service)
{
Step("2. A commit that fails");
await using var connection = new ArxDynamoConnection(service);
await using var transaction = await connection.BeginTransactionAsync();
var product = new ArxEntity("product");
product["sku"] = "SKU-D702";
await service.CreateAsync(product, transaction);
var duplicate = new ArxEntity("customer");
duplicate["customer_id"] = "C-D701";
await service.CreateAsync(duplicate, transaction);
try
{
await transaction.CommitAsync();
}
catch (ArxDatabaseException exception)
{
ShowError(exception);
}
/* ===========================================================
* [DIRECT QUERY CHECK]
* -----------------------------------------------------------
* SELECT "sku" FROM "product" WHERE "sku" = 'SKU-D702'
* -- Expect: no items.
* ===========================================================
*/
}
}
What every sample is given: the connection string (and the ISample interface the samples implement).
namespace ArxSamples.DynamoDb;
/// <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 AWS SDK 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.DynamoDb;
/// <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}");
}
The application's DynamoDB client, model, schema catalog and ARX service, created once and shared by the samples.
using Amazon.DynamoDBv2;
using Amazon.Runtime;
using ARX.SDK.DynamoDb.Model;
using ARX.SDK.DynamoDb.Services;
using ARX.SDK.Services;
namespace ArxSamples.DynamoDb;
/// <summary>The application's DynamoDB client, model, catalog and ARX service, shared by the samples.</summary>
public sealed partial class SampleContext
{
private IAmazonDynamoDB? _client;
private ArxDynamoModel? _model;
private ArxSchemaCatalog? _catalog;
private ArxDynamoDbService? _service;
/// <summary>
/// The DynamoDB client for the endpoint the connection string names. Credentials come from the AWS SDK's own chain
/// (environment, profile, role), never from code; DynamoDB Local on this machine accepts any, so a local endpoint
/// without credentials in the environment gets placeholder ones.
/// </summary>
public IAmazonDynamoDB Client => _client ??= CreateClient(ConnectionString);
/// <summary>How the entities are laid out (SampleModel.cs).</summary>
public ArxDynamoModel Model => _model ??= SampleModel.Build();
/// <summary>The ids, lookups and selection sets (SampleModel.cs).</summary>
public ArxSchemaCatalog Catalog => _catalog ??= SampleModel.Catalog();
/// <summary>One service for the whole application: it holds no connection.</summary>
public ArxDynamoDbService Service => _service ??= new ArxDynamoDbService(Client, Model, Catalog);
private static IAmazonDynamoDB CreateClient(string endpoint)
{
var config = new AmazonDynamoDBConfig { ServiceURL = endpoint, AuthenticationRegion = Environment.GetEnvironmentVariable("AWS_REGION") ?? "us-east-1" };
var local = Uri.TryCreate(endpoint, UriKind.Absolute, out var uri) && (uri.IsLoopback || uri.Host == "localhost");
return local && Environment.GetEnvironmentVariable("AWS_ACCESS_KEY_ID") == null
? new AmazonDynamoDBClient(new BasicAWSCredentials("local", "local"), config)
: new AmazonDynamoDBClient(config);
}
}
How the application lays out its entities in Amazon DynamoDB, and its schema catalog.
using ARX.SDK.DynamoDb.Model;
using ARX.SDK.Services;
namespace ArxSamples.DynamoDb;
/// <summary>
/// How the application lays out its entities in DynamoDB, in both designs, and its schema catalog.
/// </summary>
/// <remarks>
/// <para>
/// <b>A table per entity:</b> <c>product</c> has a table of its own, keyed by its <c>sku</c>; <c>lead</c> by its
/// <c>email</c>. The entity's attributes are the item's, keys included.
/// </para>
/// <para>
/// <b>A single table:</b> <c>crm</c> holds customers and their orders together. Each entity's keys are templates:
/// a customer is <c>PK = CUSTOMER#{customer_id}</c>, <c>SK = PROFILE</c>; an order shares its customer's partition,
/// <c>SK = ORDER#{order_number}</c>, so one query reads a customer with its orders. ARX writes the keys, the
/// <c>entity_type</c> attribute that tells the items apart, and the index keys (<c>GSI1</c>: orders by status and date).
/// </para>
/// <para>
/// <b>Write sharding:</b> <c>events</c> spreads the page views of one page over four partitions (a suffix from a hash of
/// the time), so a popular page doesn't overload one partition; ARX reads them back merged.
/// </para>
/// <para>
/// <b>The catalog</b> declares each entity's id (supplied: DynamoDB generates none, so ids come from the application),
/// the lookup from an order to its customer, the customer's display name and the order statuses.
/// </para>
/// </remarks>
public static class SampleModel
{
public static ArxDynamoModel Build() => ArxDynamoModel.Create()
.Table("product", partitionKey: "sku")
.Table("lead", partitionKey: "email")
.SingleTable("crm", "PK", "SK", crm => crm
.Entity("customer", pk: "CUSTOMER#{customer_id}", sk: "PROFILE")
.Entity("sales_order", pk: "CUSTOMER#{customer_id}", sk: "ORDER#{order_number}")
.Index("GSI1", "GSI1PK", "GSI1SK")
.EntityIndex("sales_order", "GSI1", pk: "STATUS#{status}", sk: "{ordered_on}"))
.SingleTable("events", "PK", "SK", events => events
.Entity("page_view", pk: "PAGE#{page}", sk: "VIEW#{viewed_at}"))
.WriteSharding("page_view", shards: 4, by: "viewed_at")
.Build();
public static ArxSchemaCatalog Catalog() => ArxSchemaCatalog.Create()
.Table("product", ArxIdColumn.Supplied<string>("sku"))
.DisplayName("product", "name")
.Table("lead", ArxIdColumn.Supplied<string>("email"))
.Table("customer", ArxIdColumn.Supplied<string>("customer_id"))
.DisplayName("customer", "name")
.Table("sales_order", ArxIdColumn.Supplied<string>("order_number"))
.Lookup("sales_order", "customer_id", "customer")
.Table("page_view", ArxIdColumn.Supplied<string>("viewed_at"))
.Add("OrderStatus", ArxSelectionSetKind.String, ("OPEN", "Open"), ("SHIPPED", "Shipped"), ("CANCELLED", "Cancelled"))
.Build();
}
The plain AWSSDK.DynamoDBv2 read-back helper, for the samples that show what Amazon DynamoDB stored.
namespace ArxSamples.DynamoDb;
/// <summary>The plain AWS SDK outside ARX, for the samples that show what DynamoDB stored.</summary>
public sealed partial class SampleContext
{
private DynamoDirectHelper? _direct;
/// <summary>The plain AWS SDK outside ARX, used only to reset the tables and to show what was stored.</summary>
public DynamoDirectHelper Direct => _direct ??= new DynamoDirectHelper(Client);
}
Plain AWSSDK.DynamoDBv2, not part of ARX: reads rows back so a sample can show what Amazon DynamoDB stored.
using System.Text.Json;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
namespace ArxSamples.DynamoDb;
/// <summary>
/// The plain AWS SDK, outside ARX: it resets the sample tables from Schema/sample-schema.json and runs the PartiQL
/// statements of the samples' checks. Samples never write through it.
/// </summary>
public sealed class DynamoDirectHelper
{
private readonly IAmazonDynamoDB _client;
public DynamoDirectHelper(IAmazonDynamoDB client)
{
_client = client;
}
/// <summary>
/// The sample schema: Schema/sample-schema.json next to the program (the samples' own project), or sample-schema.json
/// in the project folder (a sample copied from its page, as Get started says).
/// </summary>
private static string SchemaPath()
{
var candidates = new[]
{
Path.Combine(AppContext.BaseDirectory, "Schema", "sample-schema.json"),
Path.Combine(Directory.GetCurrentDirectory(), "sample-schema.json"),
Path.Combine(Directory.GetCurrentDirectory(), "Schema", "sample-schema.json")
};
return candidates.FirstOrDefault(File.Exists)
?? throw new FileNotFoundException("sample-schema.json isn't in the project folder: download it from the Get started page and save it next to Program.cs.");
}
/// <summary>
/// Deletes and creates every table sample-schema.json lists, with its keys and indexes; no other table is touched. It
/// runs only against DynamoDB Local (a localhost endpoint): set ARX_SAMPLES_ALLOW_REMOTE_RESET=1 to reset the sample
/// tables of a real AWS account deliberately.
/// </summary>
public async Task ResetAsync()
{
var endpoint = _client.Config.ServiceURL ?? string.Empty;
var local = Uri.TryCreate(endpoint, UriKind.Absolute, out var uri) && (uri.IsLoopback || uri.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase));
if (!local && Environment.GetEnvironmentVariable("ARX_SAMPLES_ALLOW_REMOTE_RESET") != "1")
{
throw new InvalidOperationException(
$"The samples reset their tables (delete and create them), and the client points at '{(endpoint.Length == 0 ? "AWS" : endpoint)}', not DynamoDB Local. "
+ "Point it at http://localhost:8000 (or your DynamoDB Local port), or set ARX_SAMPLES_ALLOW_REMOTE_RESET=1 to reset the sample tables there.");
}
using var schema = JsonDocument.Parse(await File.ReadAllTextAsync(SchemaPath()));
foreach (var table in schema.RootElement.GetProperty("tables").EnumerateArray())
{
var name = table.GetProperty("name").GetString()!;
try
{
await _client.DeleteTableAsync(name);
}
catch (ResourceNotFoundException)
{
}
var definitions = new Dictionary<string, string>();
var keys = Keys(table, definitions);
var request = new CreateTableRequest { TableName = name, KeySchema = keys, BillingMode = BillingMode.PAY_PER_REQUEST };
if (table.TryGetProperty("indexes", out var indexes))
{
request.GlobalSecondaryIndexes = indexes.EnumerateArray().Select(index => new GlobalSecondaryIndex
{
IndexName = index.GetProperty("name").GetString(),
KeySchema = Keys(index, definitions),
Projection = new Projection { ProjectionType = ProjectionType.ALL }
}).ToList();
}
request.AttributeDefinitions = definitions.Select(x => new AttributeDefinition(x.Key, x.Value)).ToList();
await _client.CreateTableAsync(request);
}
}
private static List<KeySchemaElement> Keys(JsonElement table, Dictionary<string, string> definitions)
{
var keys = new List<KeySchemaElement>();
var pk = table.GetProperty("partitionKey").GetString()!;
definitions[pk] = table.TryGetProperty("partitionKeyType", out var pkType) ? pkType.GetString()! : "S";
keys.Add(new KeySchemaElement(pk, KeyType.HASH));
if (table.TryGetProperty("sortKey", out var sk))
{
definitions[sk.GetString()!] = table.TryGetProperty("sortKeyType", out var skType) ? skType.GetString()! : "S";
keys.Add(new KeySchemaElement(sk.GetString(), KeyType.RANGE));
}
return keys;
}
/// <summary>The items a PartiQL statement finds, as JSON, one line each.</summary>
public async Task<IReadOnlyList<string>> QueryAsync(string statement)
{
var response = await _client.ExecuteStatementAsync(new ExecuteStatementRequest { Statement = statement, ConsistentRead = true });
return response.Items.Select(item => JsonSerializer.Serialize(item.ToDictionary(x => x.Key, x => x.Value.S ?? x.Value.N ?? (x.Value.IsBOOLSet ? x.Value.BOOL.ToString() : "…")))).ToList();
}
}
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. Committed together
committed the customer and the product
> 2. A commit that fails
caught ArxDatabaseException: ARX3001 UniqueViolation: Commit: create of 'customer': an item with the same key already exists. Nothing was written.