Amazon DynamoDB · Update
Set up the service from DynamoDB's tables
The service set up from DynamoDB's description of the lead table, in two scenarios: preload at startup, then update by the preloaded key with Strict on.
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.
Before scenario 1 the sample runs this, so the service and the rows the scenarios use are there:
var lead = new ArxEntity("lead");
lead["email"] = "lead-d251@example.com";
lead["full_name"] = "Nuno Teixeira";
lead["score"] = 40;
await service.CreateAsync(lead);1 Preload at startup
Preload the lead table once, at startup, and make the service Strict.
Already in scope: client (IAmazonDynamoDB), model (ArxDynamoModel)
var catalog = await new ArxPreLoadTable(new ArxPreLoadTableRegister().PreLoadTable("lead")).LoadAsync(client, model, schema => schema
.Column("lead", new ArxColumnDefinition("full_name", typeof(string), "S", true))
.Column("lead", new ArxColumnDefinition("score", typeof(int), "N", true)));
catalog.TryGetTable("lead", out var table);
Console.WriteLine($"lead: key {string.Join(", ", table.Keys)}, attributes {string.Join(", ", table.Columns.Keys)}");
return new ArxDynamoDbService(client, model, catalog) { Strict = true };Check it in the database
Note: In the AWS CLI: aws dynamodb describe-table --table-name lead --endpoint-url http://localhost:18000 KeySchema email (HASH).
2 Update by the preloaded key
Update by the preloaded key; a misspelt attribute is refused (ARX1018), the right one is written.
Already in scope: service (ArxDynamoDbService)
var typo = new ArxEntity("lead");
typo["email"] = "lead-d251@example.com";
typo["scroe"] = 90;
try
{
await service.UpdateAsync(typo, new ArxUpdateOptions { Match = ArxMatch.Key("email") });
}
catch (ArxValidationException exception) when (exception.Code == ArxErrorCode.NotPreloaded)
{
Console.WriteLine(exception.Message);
}
var change = new ArxEntity("lead");
change["email"] = "lead-d251@example.com";
change["score"] = 90;
await service.UpdateAsync(change, new ArxUpdateOptions { Match = ArxMatch.Key("email") });
Console.WriteLine("score set to 90");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 * FROM "lead" WHERE "email" = 'lead-d251@example.com'Expect: 1 item, score 90 and no scroe attribute.
Good to know
The preload makes the table's key (email) a business key, so ArxMatch.Key("email") finds the item; with Strict, an attribute the configuration doesn't declare is ARX1018 before DynamoDB.
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.Update.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 Advanced2_DynamicTableSchemaServiceSetup().RunAsync(dbContext);
The sample, as it is in the repository.
using Amazon.DynamoDBv2;
using ARX.SDK;
using ARX.SDK.DynamoDb.Model;
using ARX.SDK.DynamoDb.Services;
using ARX.SDK.Models;
using ARX.SDK.Services;
using static ArxSamples.DynamoDb.SampleOutput;
namespace ArxSamples.DynamoDb.Operations.Update.AdvancedCases;
/// <summary>
/// The service set up from DynamoDB's description of the lead table, in two scenarios: preload at startup, then update
/// by the preloaded key with Strict on.
/// </summary>
/// <remarks>
/// The preload makes the table's key (<c>email</c>) a business key, so <c>ArxMatch.Key("email")</c> finds the item;
/// with <c>Strict</c>, an attribute the configuration doesn't declare is ARX1018 before DynamoDB.
/// </remarks>
public sealed class Advanced2_DynamicTableSchemaServiceSetup : ISample
{
public string Name => "update-advanced-2-dynamic-table-schema-service-setup";
public string Title => "Set up the service from DynamoDB's tables";
public async Task RunAsync(SampleContext dbContext)
{
var service = await Scenario1_PreloadAtStartupAsync(dbContext.Client, dbContext.Model);
var lead = new ArxEntity("lead");
lead["email"] = "lead-d251@example.com";
lead["full_name"] = "Nuno Teixeira";
lead["score"] = 40;
await service.CreateAsync(lead);
await Scenario2_UpdateByThePreloadedKeyAsync(service);
}
/// <summary>1. Preload the lead table once, at startup, and make the service Strict.</summary>
private static async Task<ArxDynamoDbService> Scenario1_PreloadAtStartupAsync(IAmazonDynamoDB client, ArxDynamoModel model)
{
Step("1. Preload at startup");
var catalog = await new ArxPreLoadTable(new ArxPreLoadTableRegister().PreLoadTable("lead")).LoadAsync(client, model, schema => schema
.Column("lead", new ArxColumnDefinition("full_name", typeof(string), "S", true))
.Column("lead", new ArxColumnDefinition("score", typeof(int), "N", true)));
catalog.TryGetTable("lead", out var table);
Show($"lead: key {string.Join(", ", table.Keys)}, attributes {string.Join(", ", table.Columns.Keys)}");
/* ===========================================================
* [DIRECT QUERY CHECK]
* -----------------------------------------------------------
* -- In the AWS CLI: aws dynamodb describe-table --table-name lead --endpoint-url http://localhost:18000
* -- Expect: KeySchema email (HASH).
* ===========================================================
*/
return new ArxDynamoDbService(client, model, catalog) { Strict = true };
}
/// <summary>2. Update by the preloaded key; a misspelt attribute is refused (ARX1018), the right one is written.</summary>
private static async Task Scenario2_UpdateByThePreloadedKeyAsync(ArxDynamoDbService service)
{
Step("2. Update by the preloaded key");
var typo = new ArxEntity("lead");
typo["email"] = "lead-d251@example.com";
typo["scroe"] = 90;
try
{
await service.UpdateAsync(typo, new ArxUpdateOptions { Match = ArxMatch.Key("email") });
}
catch (ArxValidationException exception) when (exception.Code == ArxErrorCode.NotPreloaded)
{
ShowError(exception);
}
var change = new ArxEntity("lead");
change["email"] = "lead-d251@example.com";
change["score"] = 90;
await service.UpdateAsync(change, new ArxUpdateOptions { Match = ArxMatch.Key("email") });
Show("score set to 90");
/* ===========================================================
* [DIRECT QUERY CHECK]
* -----------------------------------------------------------
* SELECT * FROM "lead" WHERE "email" = 'lead-d251@example.com'
* -- Expect: 1 item, score 90 and no scroe attribute.
* ===========================================================
*/
}
}
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. Preload at startup
lead: key (email), attributes full_name, score, email
> 2. Update by the preloaded key
caught ArxValidationException: ARX1018 NotPreloaded: Column 'scroe' of 'lead' isn't in the preloaded schema, and the service is Strict.
score set to 90