Amazon QLDB driver for .NET – Cookbook reference
Important
End of support notice: Existing customers will be able to use Amazon QLDB until end of support on 07/31/2025. For more details, see
Migrate an Amazon QLDB Ledger to Amazon Aurora PostgreSQL
This reference guide shows common use cases of the Amazon QLDB driver for .NET. It provides C# code examples that demonstrate how to use the driver to run basic create, read, update, and delete (CRUD) operations. It also includes code examples for processing Amazon Ion data. In addition, this guide highlights best practices for making transactions idempotent and implementing uniqueness constraints.
Note
This topic provides code examples of processing Amazon Ion data using the Ion object mapper
Importing the driver
The following code example imports the driver.
using Amazon.QLDB.Driver; using Amazon.QLDB.Driver.Generic; using Amazon.QLDB.Driver.Serialization;
using Amazon.QLDB.Driver; using Amazon.IonDotnet.Builders;
Instantiating the driver
The following code example creates an instance of the driver that connects to a specified ledger name using default settings.
CRUD operations
QLDB runs create, read, update, and delete (CRUD) operations as part of a transaction.
Warning
As a best practice, make your write transactions strictly idempotent.
Making transactions idempotent
We recommend that you make write transactions idempotent to avoid any unexpected side effects in the case of retries. A transaction is idempotent if it can run multiple times and produce identical results each time.
For example, consider a transaction that inserts a document into a table named
Person
. The transaction should first check whether or not the document
already exists in the table. Without this check, the table might end up with duplicate
documents.
Suppose that QLDB successfully commits the transaction on the server side, but the client times out while waiting for a response. If the transaction isn't idempotent, the same document could be inserted more than once in the case of a retry.
Using indexes to avoid full table scans
We also recommend that you run statements with a WHERE
predicate clause using
an equality operator on an indexed field or a document ID; for example,
WHERE indexedField = 123
or WHERE indexedField IN (456, 789)
.
Without this indexed lookup, QLDB needs to do a table scan, which can lead to transaction
timeouts or optimistic concurrency control (OCC) conflicts.
For more information about OCC, see Amazon QLDB concurrency model.
Implicitly created transactions
The Amazon.QLDB.Driver.IQldbDriver.ExecuteTransactionExecutor
wraps an implicitly created
transaction.
You can run statements within the lambda function by using the Execute
method of the transaction executor. The driver implicitly commits the transaction when
the lambda function returns.
The following sections show how to run basic CRUD operations, specify custom retry logic, and implement uniqueness constraints.
Contents
Creating tables
Creating indexes
Reading documents
// Assumes that Person table has documents as follows: // { "GovId": "TOYENC486FH", "FirstName" : "Brent" } // Person class is defined as follows: // public class Person // { // public string GovId { get; set; } // public string FirstName { get; set; } // } IAsyncResult<Person> result = await driver.Execute(async txn => { return await txn.Execute(txn.Query<Person>("SELECT * FROM Person WHERE GovId = 'TOYENC486FH'")); }); await foreach (Person person in result) { Console.WriteLine(person.GovId); // Prints TOYENC486FH. Console.WriteLine(person.FirstName); // Prints Brent. }
Note
When you run a query without an indexed lookup, it invokes a full table scan. In this
example, we recommend having an index on
the GovId
field to optimize performance. Without an index on
GovId
, queries can have more latency and can also lead to OCC conflict
exceptions or transaction timeouts.
Using query parameters
The following code example uses a C# type query parameter.
IAsyncResult<Person> result = await driver.Execute(async txn => { return await txn.Execute(txn.Query<Person>("SELECT * FROM Person WHERE FirstName = ?", "Brent")); }); await foreach (Person person in result) { Console.WriteLine(person.GovId); // Prints TOYENC486FH. Console.WriteLine(person.FirstName); // Prints Brent. }
The following code example uses multiple C# type query parameters.
IAsyncResult<Person> result = await driver.Execute(async txn => { return await txn.Execute(txn.Query<Person>("SELECT * FROM Person WHERE GovId = ? AND FirstName = ?", "TOYENC486FH", "Brent")); }); await foreach (Person person in result) { Console.WriteLine(person.GovId); // Prints TOYENC486FH. Console.WriteLine(person.FirstName); // Prints Brent. }
The following code example uses an array of C# type query parameters.
// Assumes that Person table has documents as follows: // { "GovId": "TOYENC486FH", "FirstName" : "Brent" } // { "GovId": "ROEE1C1AABH", "FirstName" : "Jim" } // { "GovId": "YH844DA7LDB", "FirstName" : "Mary" } string[] ids = { "TOYENC486FH", "ROEE1C1AABH", "YH844DA7LDB" }; IAsyncResult<Person> result = await driver.Execute(async txn => { return await txn.Execute(txn.Query<Person>("SELECT * FROM Person WHERE GovId IN (?,?,?)", ids)); }); await foreach (Person person in result) { Console.WriteLine(person.FirstName); // Prints Brent on first iteration. // Prints Jim on second iteration. // Prints Mary on third iteration. }
The following code example uses a C# list as a value.
// Assumes that Person table has document as follows: // { "GovId": "TOYENC486FH", // "FirstName" : "Brent", // "Vehicles": [ // { "Make": "Volkswagen", // "Model": "Golf"}, // { "Make": "Honda", // "Model": "Civic"} // ] // } // Person class is defined as follows: // public class Person // { // public string GovId { get; set; } // public string FirstName { get; set; } // public List<Vehicle> Vehicles { get; set; } // } // Vehicle class is defined as follows: // public class Vehicle // { // public string Make { get; set; } // public string Model { get; set; } // } List<Vehicle> vehicles = new List<Vehicle> { new Vehicle { Make = "Volkswagen", Model = "Golf" }, new Vehicle { Make = "Honda", Model = "Civic" } }; IAsyncResult<Person> result = await driver.Execute(async txn => { return await txn.Execute(txn.Query<Person>("SELECT * FROM Person WHERE Vehicles = ?", vehicles)); }); await foreach (Person person in result) { Console.WriteLine("{"); Console.WriteLine($" GovId: {person.GovId},"); Console.WriteLine($" FirstName: {person.FirstName},"); Console.WriteLine(" Vehicles: ["); foreach (Vehicle vehicle in person.Vehicles) { Console.WriteLine(" {"); Console.WriteLine($" Make: {vehicle.Make},"); Console.WriteLine($" Model: {vehicle.Model},"); Console.WriteLine(" },"); } Console.WriteLine(" ]"); Console.WriteLine("}"); // Prints: // { // GovId: TOYENC486FH, // FirstName: Brent, // Vehicles: [ // { // Make: Volkswagen, // Model: Golf // }, // { // Make: Honda, // Model: Civic // }, // ] // } }
Note
When you run a query without an indexed lookup, it invokes a full table scan. In this
example, we recommend having an index on
the GovId
field to optimize performance. Without an index on
GovId
, queries can have more latency and can also lead to OCC conflict
exceptions or transaction timeouts.
The following code example uses an Ion type query parameter.
The following code example uses multiple query parameters.
The following code example uses a list of query parameters.
The following code example uses an Ion list as a value. To learn more about using different Ion types, see Working with Amazon Ion data types in Amazon QLDB.
Inserting documents
The following code example inserts Ion data types.
string govId = "TOYENC486FH"; Person person = new Person { GovId = "TOYENC486FH", FirstName = "Brent" }; await driver.Execute(async txn => { // Check if a document with GovId:TOYENC486FH exists // This is critical to make this transaction idempotent IAsyncResult<Person> result = await txn.Execute(txn.Query<Person>("SELECT * FROM Person WHERE GovId = ?", govId)); // Check if there is a record in the cursor. int count = await result.CountAsync(); if (count > 0) { // Document already exists, no need to insert return; } // Insert the document. await txn.Execute(txn.Query<Document>("INSERT INTO Person ?", person)); });
This transaction inserts a document into the Person
table. Before
inserting, it first checks if the document already exists in the table. This check makes the transaction idempotent in nature.
Even if you run this transaction multiple times, it won't cause any unintended side effects.
Note
In this example, we recommend having an index on the GovId
field
to optimize performance. Without an index on GovId
, statements can
have more latency and can also lead to OCC conflict exceptions or transaction timeouts.
Inserting multiple documents in one statement
To insert multiple documents by using a single INSERT
statement, you can pass a C# List
parameter to the statement as
follows.
Person person1 = new Person { FirstName = "Brent", GovId = "TOYENC486FH" }; Person person2 = new Person { FirstName = "Jim", GovId = "ROEE1C1AABH" }; List<Person> people = new List<Person>(); people.Add(person1); people.Add(person2); IAsyncResult<Document> result = await driver.Execute(async txn => { return await txn.Execute(txn.Query<Document>("INSERT INTO Person ?", people)); }); await foreach (Document row in result) { Console.WriteLine("{ documentId: " + row.DocumentId + " }"); // The statement returns the created documents' ID: // { documentId: 6BFt5eJQDFLBW2aR8LPw42 } // { documentId: K5Zrcb6N3gmIEHgGhwoyKF } }
To insert multiple documents by using a single INSERT statement, you can pass a parameter of type Ion list to the statement as follows.
You don't enclose the variable placeholder (?
) in double angle
brackets ( <<...>>
) when passing an Ion list. In manual
PartiQL statements, double angle brackets denote an unordered collection known
as a bag.
Updating documents
string govId = "TOYENC486FH"; string firstName = "John"; IAsyncResult<Document> result = await driver.Execute(async txn => { return await txn.Execute(txn.Query<Document>("UPDATE Person SET FirstName = ? WHERE GovId = ?", firstName , govId)); }); await foreach (Document row in result) { Console.WriteLine("{ documentId: " + row.DocumentId + " }"); // The statement returns the updated document ID: // { documentId: Djg30Zoltqy5M4BFsA2jSJ } }
Note
In this example, we recommend having an index on the GovId
field
to optimize performance. Without an index on GovId
, statements can
have more latency and can also lead to OCC conflict exceptions or transaction timeouts.
Deleting documents
string govId = "TOYENC486FH"; IAsyncResult<Document> result = await driver.Execute(async txn => { return await txn.Execute(txn.Query<Document>("DELETE FROM Person WHERE GovId = ?", govId)); }); await foreach (Document row in result) { Console.WriteLine("{ documentId: " + row.DocumentId + " }"); // The statement returns the updated document ID: // { documentId: Djg30Zoltqy5M4BFsA2jSJ } }
Note
In this example, we recommend having an index on the GovId
field
to optimize performance. Without an index on GovId
, statements can
have more latency and can also lead to OCC conflict exceptions or transaction timeouts.
Running multiple statements in a transaction
// This code snippet is intentionally trivial. In reality you wouldn't do this because you'd // set your UPDATE to filter on vin and insured, and check if you updated something or not. public static async Task<bool> InsureVehicle(IAsyncQldbDriver driver, string vin) { return await driver.Execute(async txn => { // Check if the vehicle is insured. Amazon.QLDB.Driver.Generic.IAsyncResult<Vehicle> result = await txn.Execute( txn.Query<Vehicle>("SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", vin)); if (await result.CountAsync() > 0) { // If the vehicle is not insured, insure it. await txn.Execute( txn.Query<Document>("UPDATE Vehicles SET insured = TRUE WHERE vin = ?", vin)); return true; } return false; }); }
Retry logic
For information about the driver's built-in retry logic, see Understanding retry policy with the driver in Amazon QLDB.
Implementing uniqueness constraints
QLDB doesn't support unique indexes, but you can implement this behavior in your application.
Suppose that you want to implement a uniqueness constraint on the
GovId
field in the Person
table. To do this, you can write a
transaction that does the following:
-
Assert that the table has no existing documents with a specified
GovId
. -
Insert the document if the assertion passes.
If a competing transaction concurrently passes the assertion, only one of the transactions will commit successfully. The other transaction will fail with an OCC conflict exception.
The following code example shows how to implement this uniqueness constraint logic.
string govId = "TOYENC486FH"; Person person = new Person { GovId = "TOYENC486FH", FirstName = "Brent" }; await driver.Execute(async txn => { // Check if a document with GovId:TOYENC486FH exists // This is critical to make this transaction idempotent IAsyncResult<Person> result = await txn.Execute(txn.Query<Person>("SELECT * FROM Person WHERE GovId = ?", govId)); // Check if there is a record in the cursor. int count = await result.CountAsync(); if (count > 0) { // Document already exists, no need to insert return; } // Insert the document. await txn.Execute(txn.Query<Document>("INSERT INTO Person ?", person)); });
Note
In this example, we recommend having an index on the GovId
field
to optimize performance. Without an index on GovId
, statements can
have more latency and can also lead to OCC conflict exceptions or transaction timeouts.
Working with Amazon Ion
There are multiple ways to process Amazon Ion data in QLDB. You can use the Ion library
The following sections provide code examples of processing Ion data using both techniques.
Contents
Importing the Ion module
using Amazon.IonObjectMapper;
using Amazon.IonDotnet.Builders;
Creating Ion types
The following code example shows how to create Ion values from C# objects using the Ion object mapper.
// Assumes that Person class is defined as follows: // public class Person // { // public string FirstName { get; set; } // public int Age { get; set; } // } // Initialize the Ion Object Mapper IonSerializer ionSerializer = new IonSerializer(); // The C# object to be serialized Person person = new Person { FirstName = "John", Age = 13 }; // Serialize the C# object into stream using the Ion Object Mapper Stream stream = ionSerializer.Serialize(person); // Load will take in stream and return a datagram; a top level container of Ion values. IIonValue ionDatagram = IonLoader.Default.Load(stream); // To get the Ion value within the datagram, we call GetElementAt(0). IIonValue ionPerson = ionDatagram.GetElementAt(0); Console.WriteLine(ionPerson.GetField("firstName").StringValue); Console.WriteLine(ionPerson.GetField("age").IntValue);
The following code examples show the two ways to create Ion values using the Ion library.
Using ValueFactory
using Amazon.IonDotnet.Tree; using Amazon.IonDotnet.Tree.Impl; IValueFactory valueFactory = new ValueFactory(); IIonValue ionPerson = valueFactory.NewEmptyStruct(); ionPerson.SetField("firstName", valueFactory.NewString("John")); ionPerson.SetField("age", valueFactory.NewInt(13)); Console.WriteLine(ionPerson.GetField("firstName").StringValue); Console.WriteLine(ionPerson.GetField("age").IntValue);
Using IonLoader
using Amazon.IonDotnet.Builders; using Amazon.IonDotnet.Tree; // Load will take in Ion text and return a datagram; a top level container of Ion values. IIonValue ionDatagram = IonLoader.Default.Load("{firstName: \"John\", age: 13}"); // To get the Ion value within the datagram, we call GetElementAt(0). IIonValue ionPerson = ionDatagram.GetElementAt(0); Console.WriteLine(ionPerson.GetField("firstName").StringValue); Console.WriteLine(ionPerson.GetField("age").IntValue);
Getting an Ion binary dump
// Initialize the Ion Object Mapper with Ion binary serialization format IonSerializer ionSerializer = new IonSerializer(new IonSerializationOptions { Format = IonSerializationFormat.BINARY }); // The C# object to be serialized Person person = new Person { FirstName = "John", Age = 13 }; MemoryStream stream = (MemoryStream) ionSerializer.Serialize(person); Console.WriteLine(BitConverter.ToString(stream.ToArray()));
// ionObject is an Ion struct MemoryStream stream = new MemoryStream(); using (var writer = IonBinaryWriterBuilder.Build(stream)) { ionObject.WriteTo(writer); writer.Finish(); } Console.WriteLine(BitConverter.ToString(stream.ToArray()));
Getting an Ion text dump
// Initialize the Ion Object Mapper IonSerializer ionSerializer = new IonSerializer(new IonSerializationOptions { Format = IonSerializationFormat.TEXT }); // The C# object to be serialized Person person = new Person { FirstName = "John", Age = 13 }; MemoryStream stream = (MemoryStream) ionSerializer.Serialize(person); Console.WriteLine(System.Text.Encoding.UTF8.GetString(stream.ToArray()));
// ionObject is an Ion struct StringWriter sw = new StringWriter(); using (var writer = IonTextWriterBuilder.Build(sw)) { ionObject.WriteTo(writer); writer.Finish(); } Console.WriteLine(sw.ToString());
For more information about working with Ion, see the Amazon Ion documentation