Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/servicestack/pocodynamo
C# .NET Typed POCO Client for AWS Dynamo DB
https://github.com/servicestack/pocodynamo
aws c-sharp dynamodb high-performance linq mono net-core net-framework poco
Last synced: about 15 hours ago
JSON representation
C# .NET Typed POCO Client for AWS Dynamo DB
- Host: GitHub
- URL: https://github.com/servicestack/pocodynamo
- Owner: ServiceStack
- Created: 2015-07-31T00:32:14.000Z (over 9 years ago)
- Default Branch: master
- Last Pushed: 2020-10-17T02:59:00.000Z (about 4 years ago)
- Last Synced: 2024-08-18T00:56:00.225Z (3 months ago)
- Topics: aws, c-sharp, dynamodb, high-performance, linq, mono, net-core, net-framework, poco
- Size: 53.7 KB
- Stars: 40
- Watchers: 10
- Forks: 8
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
Follow [@ServiceStack](https://twitter.com/servicestack) for updates, or [StackOverflow](http://stackoverflow.com/questions/ask) or the [Customer Forums](https://forums.servicestack.net/) for support.
# PocoDynamo
is a highly productive, feature-rich, typed .NET client which extends
[ServiceStack's Simple POCO life](http://stackoverflow.com/a/32940275/85785)
by enabling re-use of your code-first data models with Amazon's industrial strength and highly-scalable
NoSQL [DynamoDB](https://aws.amazon.com/dynamodb/).#### First class support for reusable, code-first POCOs
PocoDynamo is conceptually similar to ServiceStack's other code-first
[OrmLite](https://github.com/ServiceStack/ServiceStack.OrmLite) and
[Redis](https://github.com/ServiceStack/ServiceStack.Redis) clients by providing a high-fidelity, managed client that enhances
AWSSDK's low-level [IAmazonDynamoDB client](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/UsingAWSsdkForDotNet.html),
with rich, native support for intuitively mapping your re-usable code-first POCO Data models into
[DynamoDB Data Types](http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Types.html).![](https://raw.githubusercontent.com/ServiceStack/Assets/master/img/aws/pocodynamo/related-customer.png)
#### [AutoQuery DynamoDB](https://github.com/ServiceStack/ServiceStack/wiki/AutoQuery-DynamoDB)
Built on top of PocoDynamo, [AutoQuery Data's](https://github.com/ServiceStack/ServiceStack/wiki/AutoQuery-Data)
`DynamoDbSource` provides the most productive development experience for effortlessly creating rich, queryable
and optimized Services for DynamoDB data stores using only a typed Request DTO.### Quick Preview
A quick CRUD preview of `PocoDynaamo` feature-rich high-level Typed client:
```csharp
using System;
using Amazon;
using Amazon.DynamoDBv2;
using ServiceStack;
using ServiceStack.Text;
using ServiceStack.Aws.DynamoDb;
using ServiceStack.DataAnnotations;var awsDb = new AmazonDynamoDBClient("keyId","key",
new AmazonDynamoDBConfig { ServiceURL="http://localhost:8000"});
var db = new PocoDynamo(awsDb);public class Todo
{
[AutoIncrement]
public long Id { get; set; }
public string Content { get; set; }
public int Order { get; set; }
public bool Done { get; set; }
}db.RegisterTable();
db.DeleteTable(); // Delete existing Todo Table (if any)
db.InitSchema(); // Creates Todo DynamoDB Tablevar newTodo = new Todo {
Content = "Learn PocoDynamo",
Order = 1
};
db.PutItem(newTodo);var savedTodo = db.GetItem(newTodo.Id);
"Saved Todo: {0}".Print(savedTodo.Dump());savedTodo.Done = true;
db.PutItem(savedTodo);var updatedTodo = db.GetItem(newTodo.Id);
"Updated Todo: {0}".Print(updatedTodo.Dump());db.DeleteItem(newTodo.Id);
var remainingTodos = db.GetAll();
"No more Todos: {0}".Print(remainingTodos.Dump());
```## Features
#### Advanced idiomatic .NET client
PocoDynamo provides an idiomatic API that leverages .NET advanced language features with streaming API's returning
`IEnumerable` lazily evaluated responses that transparently performs multi-paged requests
behind-the-scenes whilst the resultset is iterated. It high-level API's provides a clean lightweight adapter to
transparently map between .NET built-in data types and DynamoDB's low-level attribute values. Its efficient batched
API's take advantage of DynamoDB's `BatchWriteItem` and `BatchGetItem` batch operations to perform the minimum number
of requests required to implement each API.#### Typed, LINQ provider for Query and Scan Operations
PocoDynamo also provides rich, typed LINQ-like querying support for constructing DynamoDB Query and Scan operations,
dramatically reducing the effort to query DynamoDB, enhancing readability whilst benefiting from Type safety in .NET.#### Declarative Tables and Indexes
Behind the scenes DynamoDB is built on a dynamic schema which whilst open and flexible, can be cumbersome to work with
directly in typed languages like C#. PocoDynamo bridges the gap and lets your app bind to impl-free and declarative POCO
data models that provide an ideal high-level abstraction for your business logic, hiding a lot of the complexity of
working with DynamoDB - dramatically reducing the code and effort required whilst increasing the readability and
maintainability of your Apps business logic.It includes optimal support for defining simple local indexes which only require declaratively annotating properties
to index with an `[Index]` attribute.Typed POCO Data Models can be used to define more complex Local and Global DynamoDB Indexes by implementing
`IGlobalIndex` or `ILocalIndex` interfaces which PocoDynamo uses along with the POCOs class structure
to construct Table indexes at the same time it creates the tables.In this way the Type is used as a DSL to define DynamoDB indexes where the definition of the index is decoupled from
the imperative code required to create and query it, reducing the effort to create them whilst improving the
visualization and understanding of your DynamoDB architecture which can be inferred at a glance from the POCO's
Type definition. PocoDynamo also includes first-class support for constructing and querying Global and Local Indexes
using a familiar, typed LINQ provider.#### Resilient
Each operation is called within a managed execution which transparently absorbs the variance in cloud services
reliability with automatic retries of temporary errors, using an exponential backoff as recommended by Amazon.#### Enhances existing APIs
PocoDynamo API's are a lightweight layer modeled after DynamoDB API's making it predictable the DynamoDB operations
each API calls under the hood, retaining your existing knowledge investment in DynamoDB.
When more flexibility is needed you can access the low-level `AmazonDynamoDBclient from the `IPocoDynamo.DynamoDb`
property and talk with it directly.Whilst PocoDynamo doesn't save you for needing to learn DynamoDB, its deep integration with .NET and rich support for
POCO's smoothes out the impedance mismatches to enable an type-safe, idiomatic, productive development experience.#### High-level features
PocoDynamo includes its own high-level features to improve the re-usability of your POCO models and the development
experience of working with DynamoDB with support for Auto Incrementing sequences, Query expression builders,
auto escaping and converting of Reserved Words to placeholder values, configurable converters, scoped client
configurations, related items, conventions, aliases, dep-free data annotation attributes and more.## Download
PocoDynamo is contained in ServiceStack's AWS NuGet package:
PM> Install-Package ServiceStack.Aws
PocoDynamo has a 10 Tables [free-quota usage](https://servicestack.net/download#free-quotas) limit which can be unlocked with a [commercial license key](https://servicestack.net/pricing).
To get started we'll need to create an instance of `AmazonDynamoDBClient` with your AWS credentials and Region info:```csharp
var awsDb = new AmazonDynamoDBClient(AWS_ACCESS_KEY, AWS_SECRET_KEY, RegionEndpoint.USEast1);
```Then to create a PocoDynamo client pass the configured AmazonDynamoDBClient instance above:
```csharp
var db = new PocoDynamo(awsDb);
```> Clients are Thread-Safe so you can register them as a singleton and share the same instance throughout your App
### [Source Code](https://github.com/ServiceStack/ServiceStack.Aws/tree/master/src/ServiceStack.Aws/DynamoDb)
The Source Code for PocoDynamo is maintained in [ServiceStack.Aws](https://github.com/ServiceStack/ServiceStack.Aws/) repository.
### Download Local DynamoDB
It's recommended to download [local DynamoDB](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Tools.DynamoDBLocal.html#Tools.DynamoDBLocal.DownloadingAndRunning)
as it lets you develop against a local DynamoDB instance, saving you needing a network connection or AWS account.You can connect to your local DynamoDB instance by configuring the `AmazonDynamoDBClient` to point to the default url
where Local DynamoDB instance is running:```csharp
var awsDb = new AmazonDynamoDBClient("keyId", "key", new AmazonDynamoDBConfig {
ServiceURL = "http://localhost:8000",
});var db = new PocoDynamo(awsDb);
```We've found the latest version of Local DynamoDB to be a robust and fast substitute for AWS, that eliminates waiting
times for things like creating and dropping tables whilst only slightly deviating from the capabilities of AWS where
it doesn't always include the additional limitations imposed when hosted on AWS.## Usage
To illustrate how PocoDynamo simplifies working with DynamoDB, we'll walk-through creating and retrieving the Simple
[Todo model](https://github.com/ServiceStackApps/AwsApps/blob/04dea6472fd73ea2e55f1aa748fff6e8784b339c/src/AwsApps/todo/TodoService.cs#L9)
used in the [DynamoDB-powered AWS Todo Example](http://awsapps.servicestack.net/todo/) and compare it against the code
required when using AWSSDK's `IAmazonDynamoDB` client directly.The simple `Todo` POCO is the same data model used to store TODO's in every major RDBMS's with
[OrmLite](https://github.com/ServiceStack/ServiceStack.OrmLite), in Redis with
[ServiceStack.Redis](https://github.com/ServiceStack/ServiceStack.Redis) as well as
every supported [Caching provider](https://github.com/ServiceStack/ServiceStack/wiki/Caching).PocoDynamo increases the re-use of `Todo` again which can now be used to store TODO's in DynamoDB as well:
```csharp
public class Todo
{
[AutoIncrement]
public long Id { get; set; }
public string Content { get; set; }
public int Order { get; set; }
public bool Done { get; set; }
}
```### Creating a Table with PocoDynamo
PocoDynamo enables a declarative code-first approach where it's able to create DynamoDB Table schemas from just your
POCO class definition. Whilst you could call `db.CreateTable()` API and create the Table directly, the recommended
approach is instead to register all the tables your App uses with PocoDynamo on Startup, then just call `InitSchema()`
which will go through and create all missing tables:```csharp
//PocoDynamo
var db = new PocoDynamo(awsDb)
.RegisterTable();db.InitSchema();
db.GetTableNames().PrintDump();
```In this way your App ends up in the same state with all tables created if it was started with **no tables**, **all tables**
or only a **partial list** of tables. After the tables are created we query DynamoDB to dump its entire list of Tables,
which if you started with an empty DynamoDB instance would print the single **Todo** table name to the Console:[
Todo
]
### Complete PocoDynamo TODO exampleBefore going through the details of how it all works under-the-hood, here's a quick overview of what it looks likes to
use PocoDynamo for developing a simple CRUD App. The ServiceStack
[TodoService](https://github.com/ServiceStackApps/AwsApps/blob/master/src/AwsApps/todo/TodoService.cs)
below contains the full server implementation required to implement the REST API to power
[Backbone's famous TODO App](http://todomvc.com/examples/backbone/), rewritten to store all TODO items in DynamoDB:```csharp
//PocoDynamo
public class TodoService : Service
{
public IPocoDynamo Dynamo { get; set; }public object Get(Todo todo)
{
if (todo.Id != default(long))
return Dynamo.GetItem(todo.Id);return Dynamo.GetAll();
}public Todo Post(Todo todo)
{
Dynamo.PutItem(todo);
return todo;
}public Todo Put(Todo todo)
{
return Post(todo);
}public void Delete(Todo todo)
{
Dynamo.DeleteItem(todo.Id);
}
}
```We can see `IPocoDynamo` is just a normal IOC dependency that provides high-level API's that work directly with POCO's
and built-in .NET data types, enabling the minimum effort to store, get and delete data from DynamoDB.### Creating a DynamoDB Table using AmazonDynamoDBClient
The equivalent imperative code to create the Todo DynamoDB table above would require creating executing the
`CreateTableRequest` below:```csharp
//AWSSDK
var request = new CreateTableRequest
{
TableName = "Todo",
KeySchema = new List
{
new KeySchemaElement("Id", KeyType.HASH),
},
AttributeDefinitions = new List
{
new AttributeDefinition("Id", ScalarAttributeType.N),
},
ProvisionedThroughput = new ProvisionedThroughput
{
ReadCapacityUnits = 10,
WriteCapacityUnits = 5,
}
};
awsDb.CreateTable(request);
```DynamoDB Tables take a little while to create in AWS so we can't use it immediately, instead you'll need to
periodically poll to check the status for when it's ready:```csharp
//AWSSDK
var startAt = DateTime.UtcNow;
var timeout = TimeSpan.FromSeconds(60);
do
{
try
{
var descResponse = awsDb.DescribeTable("Todo");
if (descResponse.Table.TableStatus == DynamoStatus.Active)
break;Thread.Sleep(TimeSpan.FromSeconds(2));
}
catch (ResourceNotFoundException)
{
// DescribeTable is eventually consistent. So you might get resource not found.
}if (DateTime.UtcNow - startAt > timeout)
throw new TimeoutException("Exceeded timeout of {0}".Fmt(timeout));} while (true);
```Once the table is Active we can start using it, to get the list of table names we send a `ListTablesRequest`:
```csharp
//AWSSDK
var listResponse = awsDb.ListTables(new ListTablesRequest());
var tableNames = listResponse.TableNames;
tableNames.PrintDump();
```## Managed DynamoDB Client
As we can see using the `AmazonDynamoDBClient` directly requires a lot more imperative code, but it also ends up doing
a lot less. We've not included the logic to query existing tables so only the missing tables are created, we've not
implemented any error handling or Retry logic (important for Cloud Services) and we're not checking to make sure we've
collected the entire list of results (implementing paging when necessary).Whereas every request in PocoDynamo is invoked inside a managed execution where any temporary errors are retried using the
[AWS recommended retries exponential backoff](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ErrorHandling.html#APIRetries).All PocoDynamo API's returning `IEnumerable` returns a lazy evaluated stream which behind-the-scenes sends multiple
paged requests as needed whilst the sequence is being iterated. As LINQ API's are also lazily evaluated you could use
`Take()` to only download the exact number results you need. So you can query the first 100 table names with:```csharp
//PocoDynamo
var first100TableNames = db.GetTableNames().Take(100).ToList();
```and PocoDynamo will only make the minimum number of requests required to fetch the first 100 results.
## AutoIncrement Primary Keys
Once the `Todo` table is created we can start adding TODOs to it. If we were using OrmLite. the `[AutoIncrement]`
attribute lets us use the RDBMS's native support for auto incrementing sequences to populate the Id primary key.
Unfortunately DynamoDB lacks an auto increment feature and instead recommends the user to supply a unique key as shown
in their [DynamoDB Forum example](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DataModel.html)
where they've chosen a Forum Name as the Hash Key of the Forum and Thread tables, whilst the Reply comment uses a
concatenation of `ForumName` + `#` + `ThreadSubject` as its Hash Key and the `ReplyDateTime` for the Range Key.However auto incrementing Ids have a number of useful properties making it ideal for identifying data:
- **Unique** - Each new item is guaranteed to have a unique Id that's higher than all Ids before it
- **Sequential** - A useful property to ensure consistent results when paging or ordering
- **Never change** - To ensure a constant key that never changes, Ids shouldn't contain data it references
- **Easy to read** - Humans have a better chance to read and remember a number than a concatenated string
- **Easy to reference** - It's easier to reference a predictable numeric field than a concatenated stringThey're also more re-usable as most data stores have native support for integer primary keys. For these reasons we've
added support for Auto-Incrementing integer primary keys in PocoDynamo where Ids annotated with `[AutoIncrement]`
attribute are automatically populated with the next id in its sequence.#### ISequenceSource
The Auto Incrementing functionality is provided by the
[ISequenceSource](https://github.com/ServiceStack/ServiceStack/blob/master/src/ServiceStack.Interfaces/ISequenceSource.cs)
interface:```csharp
public interface ISequenceSource : IRequiresSchema
{
long Increment(string key, int amount = 1);
void Reset(string key, int startingAt = 0);
}
```
#### DynamoDbSequenceGeneratorThe default implementation uses
[DynamoDbSequenceGenerator](https://github.com/ServiceStack/ServiceStack.Aws/blob/master/src/ServiceStack.Aws/DynamoDb/DynamoDbSequenceGenerator.cs)
which stores sequences for each table in the `Seq` DynamoDB Table so no additional services are required. To ensure
unique incrementing sequences in DynamoDB, PocoDynamo uses UpdateItemRequest's `AttributeValueUpdate` feature to perform
atomic value updates. PocoDynamo sequences are also very efficient and only require a single DynamoDB call to populate a
batch of Primary Key Ids which are also guaranteed to be in order (and without gaps) for batches that are stored together.#### RedisSequenceSource
If preferred you can instead instruct PocoDynamo to maintain sequences in Redis using
[RedisSequenceSource](https://github.com/ServiceStack/ServiceStack/blob/master/src/ServiceStack.Server/RedisSequenceSource.cs)
or alternatively inject your own implementation which can be configured in PocoDynamo with:```csharp
var db = new PocoDynamo(awsDb) {
Sequences = new RedisSequenceSource(redisManager),
};
```## Putting items with PocoDynamo
As we can take advantage of Auto Incrementing Id's, storing Items becomes as simple as creating a number of POCO's and
calling PutItems:```csharp
//PocoDynamo
var todos = 100.Times(i => new Todo { Content = "TODO " + i, Order = i });
db.PutItems(todos);
```## Putting items with AmazonDynamoDBClient
To do this manually with `AmazonDynamoDBClient` you'd need to create and `UpdateItemRequest` to update the counter
maintaining your TODO sequences:```csharp
//AWSSDK
var incrRequest = new UpdateItemRequest
{
TableName = "Seq",
Key = new Dictionary {
{"Id", new AttributeValue { S = "Todo" } }
},
AttributeUpdates = new Dictionary {
{
"Counter",
new AttributeValueUpdate {
Action = AttributeAction.ADD,
Value = new AttributeValue { N = "100" }
}
}
},
ReturnValues = ReturnValue.ALL_NEW,
};var response = awsDb.UpdateItem(incrRequest);
var nextSequences = Convert.ToInt64(response.Attributes["Counter"].N);
```After you know which sequence to start with you can start putting items using a Dictionary of Attribute Values:
```csharp
//AWSSDK
for (int i = 0; i < 100; i++)
{
var putRequest = new PutItemRequest("Todo",
new Dictionary {
{ "Id", new AttributeValue { N = (nextSequences - 100 + i).ToString() } },
{ "Content", new AttributeValue("TODO " + i) },
{ "Order", new AttributeValue { N = i.ToString() } },
{ "Done", new AttributeValue { BOOL = false } },
});awsDb.PutItem(putRequest);
}
```Although even without the managed execution this still isn't equivalent to PocoDynamo's example above as to store
multiple items efficiently PocoDynamo `PutItems()` API batches multiple Items in 4x `BatchWriteItemRequest`
behind-the-scenes, the minimum number needed due to DynamoDB's maximum Write Batch size limit of 25 requests.## Getting Items with PocoDynamo
Getting an item just requires the Generic Type and the primary key of the item to fetch:
```csharp
var todo = db.GetItem(1);
todo.PrintDump();
```Which returns the Todo item if it exists, or `null` if it doesn't.
Fetching all table items is where an understanding of DynamoDB's architecture and its limits become important. DynamoDB
achieves its scalability by partitioning your data across multiple partitions based on its hash Key (aka Primary Key).
This means that the only way to efficiently query across data containing multiple primary keys is to either explicitly
create a [Global Secondary Index](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html) or perform a
full-table Scan.However table scans in DynamoDB are more inefficient than full table scans in RDBMS's since it has to scan across
multiple partitions which can quickly use up your table's provisioned throughput, as such scans should be limited
to low usage areas.With that said, you can do Table Scans in PocoDynamo using API's starting with `Scan*` prefix, e.g. to return
all Todo items:```csharp
//PocoDynamo
IEnumerable todos = db.ScanAll();
```As IEnumerable's are lazily executed, it only starts sending `ScanRequest` to fetch all Items once the IEnumerable is
iterated, which it does in **batches of 1000** (configurable with `PocoDynamo.PagingLimit`).To fetch all items you can just call `ToList()`:
```csharp
var allTodos = todos.ToList();
allTodos.PrintDump();
```Which incidentally is also just what `db.GetAll()` does.
## Getting Items with AWSSDK
To fetch the same single item with the AWSSDK client you'd construct and send a `GetItemRequest`, e.g:
```csharp
//AWSSDK
var request = new GetItemRequest
{
TableName = "Todo",
Key = new Dictionary {
{ "Id", new AttributeValue { N = "1"} }
},
ConsistentRead = true,
};var response = awsDb.GetItem(request);
var todo = new Todo
{
Id = Convert.ToInt64(response.Item["Id"].N),
Content = response.Item["Content"].S,
Order = Convert.ToInt32(response.Item["Order"].N),
Done = response.Item["Done"].BOOL,
};
```Although this is a little fragile as it doesn't handle the case when attributes (aka Properties) or the item doesn't exist.
Doing a full-table scan is pretty straight-forward although as you're scanning the entire table you'll want to implement
the paging to scan through all items, which looks like:```csharp
//AWSSDK
var request = new ScanRequest
{
TableName = "Todo",
Limit = 1000,
};var allTodos = new List();
ScanResponse response = null;
do
{
if (response != null)
request.ExclusiveStartKey = response.LastEvaluatedKey;response = awsDb.Scan(request);
foreach (var item in response.Items)
{
var todo = new Todo
{
Id = Convert.ToInt64(item["Id"].N),
Content = item["Content"].S,
Order = Convert.ToInt32(item["Order"].N),
Done = item["Done"].BOOL,
};
allTodos.Add(todo);
}
} while (response.LastEvaluatedKey != null && response.LastEvaluatedKey.Count > 0);allTodos.PrintDump();
```## Deleting an Item with PocoDynamo
Deleting an item is similar to getting an item which just needs the generic type and primary key:
```csharp
//PocoDynamo
db.DeleteItem(1);
```## Deleting an Item with AWSSDK
Which just sends a `DeleteItemRequest` to delete the Item:
```csharp
//AWSSDK
var request = new DeleteItemRequest
{
TableName = "Todo",
Key = new Dictionary {
{ "Id", new AttributeValue { N = "1"} }
},
};awsDb.DeleteItem(request);
```## Updating an Item with PocoDynamo
The simplest usage is to pass in a partially populated POCO where any Hash or Range Keys are added to the
Key Condition and any non-default values are replaced. E.g the query below updates the Customer's Age to **42**:```csharp
db.UpdateItemNonDefaults(new Customer { Id = customer.Id, Age = 42 });
```DynamoDB's UpdateItem supports 3 different operation types:
- `PUT` to replace an Attribute Value
- `ADD` to add to an existing Attribute Value
- `DELETE` to delete the specified AttributesExamples of all 3 are contained in the examples below which changes the Customer's `Nationality` to
**Australian**, reduces their `Age` by **1** and deletes their `Name` and `Orders`:```csharp
db.UpdateItem(customer.Id,
put: () => new Customer {
Nationality = "Australian"
},
add: () => new Customer {
Age = -1
},
delete: x => new { x.Name, x.Orders });
```The same Typed API above is also available in the more flexible and untyped form below:
```csharp
db.UpdateItem(new DynamoUpdateItem
{
Hash = customer.Id,
Put = new Dictionary
{
{ "Nationality", "Australian" },
},
Add = new Dictionary
{
{ "Age", -1 }
},
Delete = new[] { "Name", "Orders" },
});
```### Update with Conditional Expressions
PocoDynamo also has Typed API support for
[DynamoDB Conitional Expressions](http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#API_PutItem_RequestSyntax)
by using the `Condition()` API, e.g:```csharp
var q = db.UpdateExpression(customer.Id)
.Set(() => new Customer { Nationality = "Australian" })
.Add(() => new Customer { Age = decrBy })
.Remove(x => new { x.Name, x.Orders })
.Condition(x => x.Age == 27);var succeeded = db.UpdateItem(q);
```## Querying
The simple Todo example should give you a feel for using PocoDynamo to handle basic CRUD operations.
Another area where PocoDynamo adds a lot of value which can be fairly cumbersome to do without, is in creating
[Query and Scan](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/QueryAndScan.html)
requests to query data in DynamoDB Tables.### QueryExpressions are QueryRequests
The query functionality in PocoDynamo is available on the `QueryExpression` class which is used as a typed query
builder to construct your Query request. An important attribute about QueryExpression's are that they inherit AWSSDK's
[QueryRequest](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LowLevelDotNetQuerying.html) Request DTO.This provides a number of benefits, they're easy to use and highly introspectable since each API just populates
different fields in the Request DTO. They're also highly reusable as QueryExpressions can be executed as-is in AWSSDK
DynamoDB client and vice-versa with PocoDynamo's `Query` API's executing both `QueryExpression` and `QueryRequest`
DTOs. The difference with PocoDynamo's Query API is that they provide managed exeuction, lazy evaluation, paged queries
and auto-conversion of dynamic results into typed POCOs.### Query Usage
[DynamoDB Query's](http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html) enable efficient
querying of data in DynamoDB as it's limited to querying the indexed Hash and Range Keys on your Tables or Table Indexes.
Although it has the major limitation that it always needs to specify a Hash condition, essentially forcing the query
to be scoped to a single partition. This makes it fairly useless for Tables with only a single Hash Primary Key like
`Todo` as the query condition will always limit to a maximum of 1 result.Nevertheless we can still use it to show how to perform server-side queries with PocoDynamo. To create a
QueryExpression use the `FromQuery*` API's. It accepts a `KeyConditionExpression` as the first argument given it's a
mandatory requirement for Query Requests which uses it to identify the partition the query should be executed on:```csharp
var q = db.FromQuery(x => x.Id == 1);
```#### Key Condition and Filter Expressions
PocoDynamo parses this lambda expression to return a populated `QueryExpression` which you can inspect to find
the `TableName` set to **Todo** and the `KeyConditionExpression` set to **(Id = :k0)** with the
`ExpressionAttributeValues` Dictionary containing a Numeric value of **1** for the key **:k0**.From here you can continue constructing the QueryRequest DTO by populating its properties directly or by calling
`QueryExpression` high-level methods (modeled after the properties they populate), e.g. the `KeyCondition()` method
populates the `KeyConditionExpression` property, `Filter()` populates the `FilterExpression` property and any arguments
used in any expression are automatically parameterized and added to the `ExpressionAttributeValues` collection:```csharp
var q = db.FromQuery()
.KeyCondition(x => x.Id == 1) //Equivalent to: db.FromQuery(x => x.Id == 1)
.Filter(x => x.Done);q.TableName // Todo
q.KeyConditionExpression // (Id = :k0)
q.FilterExpression // Done = :true
q.ExpressionAttributeValues // :k0 = AttributeValue {N=1}, :true = AttributeValue {BOOL=true}
```Filter expressions are applied after the query is executed which enable more flexible querying as they're not just
limited to key fields and can be used to query any field to further filter the returned resultset.### Executing Queries
After you've finished populating the Request DTO it can be executed with PocoDynamo's `Query()`. This returns a lazily
evaluated resultset which you can use LINQ methods on to fetch the results. Given the primary key condition we know this
will only return 0 or 1 rows based on whether or not the TODO has been completed which we can check with by calling
LINQ's `FirstOrDefault()` method:```csharp
var todo1Done = db.Query(q).FirstOrDefault();
```Where `todo1Done` will hold the populated `Todo` if it was marked done, otherwise it will be `null`.
#### Expression Chaining
Most `QueryExpression` methods returns itself and an alternative to calling `Query` on PocoDynamo (or AWSSDK) to execute
the Query, you can instead call the `Exec()` alias. This allows you to create and execute your DynamoDb Query in a
single expression which can instead be rewritten as:```csharp
var todo1Done = db.FromQuery(x => x.Id == 1)
.Filter(x => x.Done)
.Exec()
.FirstOrDefault();
```### Scan Operations
Scan Operations work very similar to Query Operations but instead of using a `QueryExpression` you would instead use a `ScanExpression` which as it inherits from AWSSDK's `ScanRequest` Request DTO, provides the same reuse benefits as QueryExpression's.
To create a Scan Request you would use the `FromScan` API, e.g:
```csharp
var q = db.FromScan();
```More examples of how to use typed LINQ expressions for creating and executing Query and Scan requests are described later.
### Related Items
DynamoDB Queries are ideally suited for when the dataset is naturally isolated, e.g. multi-tenant Apps that are
centered around Customer data so any related records are able to share the same `CustomerId` Hash Key.PocoDynamo has good support for maintaining related data which can re-use the same Data Annotations used to define
POCO relationships in OrmLite, often letting you reuse your existing OrmLite RDBMS data models in DynamoDB as well.To illustrate how to use PocoDynamo to maintain related data we'll walk through a typical Customer and Orders example:
```csharp
public class Customer
{
[AutoIncrement]
public int Id { get; set; }
public string Name { get; set; }
public CustomerAddress PrimaryAddress { get; set; }
}public class CustomerAddress
{
[AutoIncrement]
public int Id { get; set; }
public string Address { get; set; }
public string State { get; set; }
public string Country { get; set; }
}[Alias("CustomerOrder")]
public class Order
{
[AutoIncrement]
public int Id { get; set; }[References(typeof(Customer))]
public int CustomerId { get; set; }public string Product { get; set; }
public int Qty { get; set; }[Index]
public virtual decimal Cost { get; set; }
}
```In order to use them we need to tell PocoDynamo which of the Types are Tables that it should create in DynamoDB which
we can do by registering them with PocoDynamo then calling `InitSchema()` which will go through and create any
of the tables that don't yet exist in DynamoDB:```csharp
db = new PocoDynamo(awsDb)
.RegisterTable()
.RegisterTable();db.InitSchema();
````InitSchema()` will also wait until the tables have been created so they're immediately accessible afterwards.
As creating DynamoDB tables can take upwards of a minute in AWS you can use the
[alternative Async APIs](https://github.com/ServiceStack/ServiceStack.Aws/blob/master/src/ServiceStack.Aws/DynamoDb/IPocoDynamoAsync.cs)
if you wanted to continue to doing other stuff whilst the tables are being created in AWS, e.g:```csharp
var task = db.InitSchemaAsync();// do other stuff...
await task;
```## Related Data
After the tables are created we can insert the top-level Customer record as normal:
```csharp
var customer = new Customer
{
Name = "Customer",
PrimaryAddress = new CustomerAddress
{
Address = "1 road",
State = "NT",
Country = "Australia",
}
};db.PutItem(customer);
```Before adding the record, PocoDynamo also populates any `[AutoIncrement]` properties with the next number in the
sequence for that Type. Any complex types stored on the `Customer` POCO like `CustomerAddress` gets persisted along
with the containing `Customer` entry and converted into a **Map** of DynamoDB Attribute Value pairs. We can view the
[DynamoDB Web Console](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ConsoleDynamoDB.html)
to see how this is stored in DynamoDB:![](https://raw.githubusercontent.com/ServiceStack/Assets/master/img/aws/pocodynamo/related-customer.png)
### Related Tables
You can define a related table using the `[References]` attribute to tell PocoDynamo what the parent table is, e.g:
```csharp
[Alias("CustomerOrder")]
public class Order
{
[AutoIncrement]
public int Id { get; set; }
[References(typeof(Customer))]
public int CustomerId { get; set; }
//...
}
```Which PocoDynamo infers to create the table using the parent's `CustomerId` as its Hash Key, relegating its `Id` as
the Range Key for the table. This ensures the Order is kept in the same partition as all other related Customer Data,
necessary in order to efficiently query a Customer's Orders. When both the Hash and Range Key are defined they're treated
as the Composite Key for that table which needs to be unique for each item - guaranteed when using `[AutoIncrement]` Id's.#### Inserting Related Data
After the table is created we can generate and insert random orders like any other table, e.g:
```csharp
var orders = 10.Times(i => new Order
{
CustomerId = customer.Id,
Product = "Item " + (i % 2 == 0 ? "A" : "B"),
Qty = i + 2,
Cost = (i + 2) * 2
});db.PutItems(orders);
```You can also use the alternative `PutRelatedItems()` API and get PocoDynamo to take care of populating the `CustomerId`:
```csharp
var orders = 10.Times(i => new Order
{
Product = "Item " + (i % 2 == 0 ? "A" : "B"),
Qty = i + 2,
Cost = (i + 2) * 2
});db.PutRelatedItems(customer.Id, orders);
```Both examples results in the same data being inserted into the **CustomerOrder** DynamoDB table:
![](https://raw.githubusercontent.com/ServiceStack/Assets/master/img/aws/pocodynamo/related-customer-orders.png)
This also shows how the `[Alias]` attribute can be used to rename the `Order` Type as **CustomerOrder** in DynamoDB.
### Querying Related Tables
Now we have related data we can start querying it, something you may want to do is fetch all Customer Orders:
```csharp
var q = db.FromQuery(x => x.CustomerId == customer.Id);
var dbOrders = db.Query(q);
```As getting related Items for a Hash Key is a popular query, it has an explicit API:
```csharp
var dbOrders = db.GetRelatedItems(customer.Id);
```We can refine the query further by specifying a `FilterExpression` to limit the results DynamoDB returns:
```csharp
var expensiveOrders = q.Clone()
.Filter(x => x.Cost > 10)
.Exec();
```> Using `Clone()` will create and modify a copy of the query, leaving the original one intact.
### [Local Secondary Indexes](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LSI.html)
But filters aren't performed on an Index and can be inefficient if your table has millions of customer rows. By default
only the Hash and Range Key are indexed, in order to efficiently query any other field you will need to create
a [Local Secondary Index](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LSI.html) for it.This is easily done in PocoDynamo by annotating the properties you want indexed with the `[Index]` attribute:
```csharp
public class Order
{
//...
[Index]
public decimal Cost { get; set; }
}
```Which tells PocoDynamo to create a Local Secondary Index for the `Cost` property when it creates the table.
When one exists, you can query a Local Index with `LocalIndex()`:
```csharp
var expensiveOrders = q
.LocalIndex(x => x.Cost > 10)
.Exec();
```Which now performs the Cost query on an index. Although this only returns a partially populated Order, specifically
with just the Hash Key (CustomerId), Range Key (Id) and the field that's indexed (Cost):expensiveOrders.PrintDump();
[
{
Id: 5,
CustomerId: 1,
Qty: 0,
Cost: 12
},
//...
]This is due to [Local Secondary Indexes](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LSI.html)
being just denormalized tables behind the scenes which by default only returns re-projected fields that were defined
when the Index was created.One way to return populated orders is to specify a custom `ProjectionExpression` with the fields you want returned.
E.g. You can create a request with a populated `ProjectionExpression` that returns all Order fields with:```csharp
var expensiveOrders = q
.LocalIndex(x => x.Cost > 10)
.Select() //Equivalent to: SelectTableFields()
.Exec();
```Which now returns:
expensiveOrders.PrintDump();
[
{
Id: 5,
CustomerId: 1,
Product: Item A,
Qty: 6,
Cost: 12
},
//...
]### Typed Local Indexes
Using a custom `ProjectionExpression` is an easy work-around, although for it to work DynamoDB needs to consult the
primary table to fetch the missing fields for each item. For large tables that are frequently accessed, the query
can be made more efficient by projecting the fields you want returned when the Index is created.You can can tell PocoDynamo which additional fields it should reproject by creating a **Typed Local Index** which is
just a POCO implementing `ILocalIndex` containing all the fields the index should contain, e.g:```csharp
public class OrderCostLocalIndex : ILocalIndex
{
[Index]
public decimal Cost { get; set; }
public int CustomerId { get; set; }public int Id { get; set; }
public int Qty { get; set; }
}[References(typeof(OrderCostLocalIndex))]
public class Order { ... }
```Then use the `[References]` attribute to register the Typed Index so PocoDynamo knows which additional indexes needs
to be created with the table. The `[Index]` attribute is used to specify which field is indexed (Range Key) whilst
the `CustomerId` is automatically used the Hash Key for the Local Index Table.#### Querying Typed Indexes
To query a typed Index, use `FromQueryIndex()` which returns a populated Query Request with the Table and Index Name.
As `Cost` is now the Range Key of the Local Index table it can be queried together with the `CustomerId` Hash Key in
the Key Condition expression:```csharp
List expensiveOrderIndexes = db.FromQueryIndex(x =>
x.CustomerId == customer.Id && x.Cost > 10)
.Exec();
```This returns a list of populated indexes that now includes the `Qty` field:
expensiveOrderIndexes.PrintDump();
[
{
Cost: 12,
CustomerId: 1,
Id: 5,
Qty: 6
},
//...
]If preferred you can easily convert Typed Index into Orders by using ServiceStack's
[built-in Auto-Mapping](https://github.com/ServiceStack/ServiceStack/wiki/Auto-mapping), e.g:```csharp
List expensiveOrders = expensiveOrderIndexes
.Map(x => x.ConvertTo());
```### [Global Secondary Indexes](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html)
The major limitation of Local Indexes is that they're limited to querying data in the same partition (Hash Key).
To efficiently query an index spanning the entire dataset, you need to instead use a
[Global Secondary Index](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html).Support for Global Indexes in PocoDynamo is similar to Typed Local Indexes, but instead implements `IGlobalIndex`.
They also free you to choose a new Hash Key, letting you create an Index spanning all Customers.For example we can create a global index that lets us search the cost across all orders containing a particular product:
```csharp
public class OrderCostGlobalIndex : IGlobalIndex
{
[HashKey]
public string Product { get; set; }
[Index]
public decimal Cost { get; set; }public int CustomerId { get; set; }
public int Qty { get; set; }
public int Id { get; set; }
}[References(typeof(OrderCostGlobalIndex))]
public class Order { ... }
```Our Key Condition can now instead query Product and Cost fields across all Customer Orders:
```csharp
var expensiveItemAOrders = db.FromQueryIndex(x =>
x.Product == "Item A" && x.Cost > 10)
.Exec();
```Which will print all **Item A** Orders with a **Cost > 10**:
expensiveItemAOrders.PrintDump();
[
{
Product: Item A,
Cost: 12,
CustomerId: 1,
Qty: 6,
Id: 5
},
//...
]
## Scan RequestsYou'll want to just use queries for any frequently accessed code running in production, although the full querying
flexibility available in full table scan requests can be useful for ad hoc querying and to speed up development cycles
by initially starting with Scan queries then when the data requirements for your App's have been finalized, rewrite
them to use indexes and queries.To create Scan Requests you instead call the `FromScan*` API's, e.g:
```csharp
var allOrders = db.ScanAll();var expensiveOrders = db.FromScan(x => x.Cost > 10)
.Exec();
```You can also perform scans on Global Indexes, but unlike queries they don't need to be limited to the Hash Key:
```csharp
var expensiveOrderIndexes = db
.FromScanIndex(x => x.Cost > 10)
.Exec();
```Just like `QueryExpression` the populated `ScanExpression` inherits from AWSSDK's `ScanRequest` enabling the same
re-use benefits for `ScanRequest` as they do for QueryRequest's.## Query and Scan Expressions
Both Scans and Query expressions benefit from a Typed LINQ-like expression API which can be used to populate the DTO's
- **KeyConditionExpression** - for specifying conditions on tables Hash and Range keys (only: QueryRequest)
- **FilterExpression** - for specifying conditions to filter results on other fields
- **ProjectionExpression** - to specify any custom fields (default: all fields)Each `QueryRequest` needs to provide a key condition which can be done when creating the QueryExpression:
```csharp
var orders = db.FromQuery(x => x.CustomerId == 1).Exec();// Alternative explicit API
var expensiveOrders = db.FromQuery().KeyCondition(x => x.CustomerId == 1).Exec();
```Whilst every condition on a `ScanRequest` is added to the FilterExpression:
```csharp
var expensiveOrders = db.FromScan(x => x.Cost > 10).Exec();// Alternative explicit API
var expensiveOrders = db.FromScan().Filter(x => x.Cost > 10).Exec();
```Calling `Exec()` returns a lazily executed response which transparently sends multiple paged requests to fetch the
results as needed, e.g calling LINQ's `.FirstOrDefault()` only makes a single request whilst `.ToList()` fetches the
entire resultset. All streaming `IEnumerable` requests are sent with the configured `PagingLimit` (default: 1000).#### Custom Limits
Several of PocoDynamo API's have overloads that let you specify a custom limit. API's with limits are instead
executed immediately with the limit specified and returned in a concrete List:```csharp
List expensiveOrders = db.FromScan().Filter(x => x.Cost > 10).Exec(limit:5);
```### Custom Filter Expressions
There are also custom overloads that can be used to execute a custom expression when more flexibility is needed:
```csharp
// Querying by Custom Filter Condition with anon args
var expensiveOrders = db.FromScan().Filter("Cost > :amount", new { amount = 10 }).Exec();// Querying by Custom Filter Condition with loose-typed Dictionary
var expensiveOrders = db.FromScan().Filter("Cost > :amount",
new Dictionary { { "amount", 10 } })
.Exec();
```### Custom Select Projections
By default queries return all fields defined on the POCO model. You can also customize the projected fields that are
returned with the `Select*` and `Exec*` APIs:```csharp
// Return partial fields from anon object
var partialOrders = db.FromScan().Select(x => new { x.CustomerId, x.Cost }).Exec();// Return partial fields from array
var partialOrders = db.FromScan().Select(x => new[] { "CustomerId", "Cost" }).Exec();// Return partial fields defined in a custom Poco
class CustomerCost
{
public int CustomerId { get; set; }
public virtual decimal Cost { get; set; }
}var custCosts = db.FromScan().Select()
.Exec()
.Map(x => x.ConvertTo());// Alternative shorter version of above
var custCosts = db.FromScan().ExecInto().ToList();// Useful when querying and index and returing results in primary Order Poco
List expensiveOrders = db.FromScanIndex(x => x.Cost > 10)
.ExecInto();// Return a single column of fields
List orderIds = db.FromScan().ExecColumn(x => x.Id).ToList();
```### Advanced LINQ Expressions
In addition to basic predicate conditions, DynamoDB also includes support for
[additional built-in functions](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.SpecifyingConditions.html)
which PocoDynamo also provides typed LINQ support for:#### begins_with
Return items where string fields starts with a particular substring:
```csharp
var orders = db.FromScan(x => x.Product.StartsWith("Item A")).Exec();// Equivalent to
var orders = db.FromScan(x => Dynamo.BeginsWith(x.Product, "Item A")).Exec();var orders = db.FromScan().Filter("begins_with(Product, :s)", new { s = "Item A" }).Exec();
```#### contains
Return items where string fields contains a particular substring:
```csharp
var orders = db.FromScan(x => x.Product.Contains("em A")).Exec();// Equivalent to
var orders = db.FromScan(x => Dynamo.Contains(x.Product, "em A")).Exec();var orders = db.FromScan().Filter("contains(Product, :s)", new { s = "em A" }).Exec();
```#### in
Returns items where fields exist in a particular collection:
```csharp
var qtys = new[] { 5, 10 };var orders = db.FromScan(x => qtys.Contains(x.Qty)).Exec();
// Equivalent to
var orders = db.FromScan(x => Dynamo.In(x.Qty, qtys)).Exec();var orders = db.FromScan().Filter("Qty in(:q1,:q2)", new { q1 = 5, q2 = 10 }).Exec();
```#### size
Returns items where the string length equals a particular size:
```csharp
var orders = db.FromScan(x => x.Product.Length == 6).Exec();// Equivalent to
var orders = db.FromScan(x => Dynamo.Size(x.Product) == 6).Exec();var orders = db.FromScan().Filter("size(Product) = :n", new { n = 6 }).Exec();
```Size also works for querying the size of different native DynamoDB collections, e.g:
```csharp
public class IntCollections
{
public int Id { get; set; }public int[] ArrayInts { get; set; }
public HashSet SetInts { get; set; }
public List ListInts { get; set; }
public Dictionary DictionaryInts { get; set; }
}var results = db.FromScan(x =>
x.ArrayInts.Length == 10 &&
x.SetInts.Count == 10 &&
x.ListInts.Count == 10 &&
x.DictionaryInts.Count == 10)
.Exec();
```#### between
Returns items where field values fall within a particular range (inclusive):
```csharp
var orders = db.FromScan(x => Dynamo.Between(x.Qty, 3, 5)).Exec();// Equivalent to
var orders = db.FromScan(x => x.Qty >= 3 && x.Qty <= 5).Exec();var orders = db.FromScan().Filter("Qty between :from and :to", new { from = 3, to = 5 }).Exec();
```#### attribute_type
Return items where field is of a particular type:
```csharp
var orders = db.FromScan(x =>
Dynamo.AttributeType(x.Qty, DynamoType.Number) &&
Dynamo.AttributeType(x.Product, DynamoType.String))
.Exec();// Equivalent to
var orders = db.FromScan().Filter(
"attribute_type(Qty, :n) and attribute_type(Product, :s)", new { n = "N", s = "S"})
.Exec();
```Valid Types: L (List), M (Map), S (String), SS (StringSet), N (Number), NS (NumberSet), B (Binary), BS, BOOL, NULL
#### attribute_exists
Return items where a particular field exists. As the schema of your data models evolve you can use this to determine
whether items are of an old or new schema:```csharp
var newOrderTypes = db.FromScan(x => Dynamo.AttributeExists(x.NewlyAddedField)).Exec();// Equivalent to
var newOrderTypes = db.FromScan().Filter("attribute_exists(NewlyAddedField)").Exec();
```#### attribute_not_exists
Return items where a particular field does not exist:
```csharp
var oldOrderTypes = db.FromScan(x => Dynamo.AttributeNotExists(x.NewlyAddedField)).Exec();// Equivalent to
var oldOrderTypes = db.FromScan().Filter("attribute_not_exists(NewlyAddedField)").Exec();
```### Defaults and Custom Behavior
PocoDynamo is configured with the defaults below which it uses throughout its various API's when used in creating and
querying tables:```csharp
//Defaults:
var db = new PocoDynamo(awsDb) {
PollTableStatus = TimeSpan.FromSeconds(2),
MaxRetryOnExceptionTimeout = TimeSpan.FromSeconds(60),
ReadCapacityUnits = 10,
WriteCapacityUnits = 5,
ConsistentRead = true,
ScanIndexForward = true,
PagingLimit = 1000,
};
```If you wanted to query with different behavior you can create a clone of the client with the custom settings you want,
e.g. you can create a client that performs eventually consistent queries with:```csharp
IPocoDynamo eventuallyConsistentDb = db.ClientWith(consistentRead:false);
```## Table definition
To support different coding styles, readability/dependency preferences and levels of data model reuse, PocoDynamo
enables a wide array of options for specifying a table's Hash and Range Keys, in the following order or precedence:**Note: Hash and Range keys cannot be read-only calculated properties.
### Specifying a Hash Key
Using the AWSSDK's `[DynamoDBHashKey]` attribute:
```csharp
public class Table
{
[DynamoDBHashKey]
public int CustomId { get; set; }
}
```This requires your models to have a dependency to the **AWSSDK.DynamoDBv2** NuGet package which can be avoided by using
**ServiceStack.Interfaces** `[HashKey]` attribute instead which your models already likely have a reference to:```csharp
public class Table
{
[HashKey]
public int CustomId { get; set; }
}
```You can instead avoid any attributes using the explicit **HashKey** Naming convention:
```csharp
public class Table
{
public int HashKey { get; set; }
}
```For improved re-usability of your models you can instead use the generic annotations for defining a model's primary key:
```csharp
public class Table
{
[PrimaryKey]
public int CustomId { get; set; }
}
``````csharp
public class Table
{
[AutoIncrement]
public int CustomId { get; set; }
}
```Alternative using the Universal `Id` naming convention:
```csharp
public class Table
{
public int Id { get; set; }
}
```If preferred both Hash and Range Keys can be defined together with the class-level `[CompositeKey]` attribute:
```csharp
[CompositeKey("CustomHash", "CustomRange")]
public class Table
{
public int CustomHash { get; set; }
public int CustomRange { get; set; }
}
```### Specifying a Range Key
For specifying the Range Key use can use the **AWSSDK.DynamoDBv2** Attribute:
```csharp
public class Table
{
[DynamoDBRangeKey]
public int CustomId { get; set; }
}
```The **ServiceStack.Interfaces** attribute:
```csharp
public class Table
{
[RangeKey]
public int CustomId { get; set; }
}
```Or without attributes, using the explicit `RangeKey` property name:
```csharp
public class Table
{
public int RangeKey { get; set; }
}
```## Examples
### [DynamoDbCacheClient](https://github.com/ServiceStack/ServiceStack.Aws/blob/master/src/ServiceStack.Aws/DynamoDb/DynamoDbCacheClient.cs)
We've been quick to benefit from the productivity advantages of PocoDynamo ourselves where we've used it to rewrite
[DynamoDbCacheClient](https://github.com/ServiceStack/ServiceStack.Aws/blob/master/src/ServiceStack.Aws/DynamoDb/DynamoDbCacheClient.cs)
which is now just 2/3 the size and much easier to maintain than the existing
[Community-contributed version](https://github.com/ServiceStack/ServiceStack/blob/22aca105d39997a8ea4c9dc20b242f78e07f36e0/src/ServiceStack.Caching.AwsDynamoDb/DynamoDbCacheClient.cs)
whilst at the same time extending it with even more functionality where it now implements the `ICacheClientExtended` API.### [DynamoDbAuthRepository](https://github.com/ServiceStack/ServiceStack.Aws/blob/master/src/ServiceStack.Aws/DynamoDb/DynamoDbAuthRepository.cs)
PocoDynamo's code-first Typed API made it much easier to implement value-added DynamoDB functionality like the new
[DynamoDbAuthRepository](https://github.com/ServiceStack/ServiceStack.Aws/blob/master/src/ServiceStack.Aws/DynamoDb/DynamoDbAuthRepository.cs)
which due sharing a similar code-first POCO approach to OrmLite, ended up being a straight-forward port of the existing
[OrmLiteAuthRepository](https://github.com/ServiceStack/ServiceStack/blob/master/src/ServiceStack.Server/Auth/OrmLiteAuthRepository.cs)
where it was able to reuse the existing `UserAuth` and `UserAuthDetails` data models.### [DynamoDbTests](https://github.com/ServiceStack/ServiceStack.Aws/tree/master/tests/ServiceStack.Aws.DynamoDbTests)
Despite its young age we've added a comprehensive test suite behind PocoDynamo which has become our exclusive client
for developing DynamoDB-powered Apps.### [AWS Apps](http://awsapps.servicestack.net/)
The [Live Demos](https://github.com/ServiceStackApps/LiveDemos) below were rewritten from their original RDBMS and OrmLite
backends to utilize a completely managed AWS Stack that now uses PocoDynamo and a DynamoDB-backend:[![](https://raw.githubusercontent.com/ServiceStack/Assets/master/img/aws/pocodynamo/examples-razor-rockstars.png)](http://awsrazor.servicestack.net/)
[![](https://raw.githubusercontent.com/ServiceStack/Assets/master/img/aws/pocodynamo/examples-email-contacts.png)](http://awsapps.servicestack.net/emailcontacts/)
[![](https://raw.githubusercontent.com/ServiceStack/Assets/master/img/aws/pocodynamo/examples-todos.png)](http://awsapps.servicestack.net/todo/)
[![](https://raw.githubusercontent.com/ServiceStack/Assets/master/img/aws/pocodynamo/examples-awsauth.png)](http://awsapps.servicestack.net/awsauth/)
## IPocoClient API
```csharp
// Interface for the code-first PocoDynamo client
public interface IPocoDynamo : IPocoDynamoAsync, IRequiresSchema
{
// Get the underlying AWS DynamoDB low-level client
IAmazonDynamoDB DynamoDb { get; }// Get the numeric unique Sequence generator configured with this client
ISequenceSource Sequences { get; }// Access the converters that converts POCO's into DynamoDB data types
DynamoConverters Converters { get; }// How long should PocoDynamo keep retrying failed operations in an exponential backoff (default 60s)
TimeSpan MaxRetryOnExceptionTimeout { get; }// Get the AWSSDK DocumentModel schema for this Table
Table GetTableSchema(Type table);// Get PocoDynamo Table metadata for this table
DynamoMetadataType GetTableMetadata(Type table);// Calls 'ListTables' to return all Table Names in DynamoDB
IEnumerable GetTableNames();// Creates any tables missing in DynamoDB from the Tables registered with PocoDynamo
bool CreateMissingTables(IEnumerable tables, TimeSpan? timeout = null);// Creates any tables missing from the specified list of tables
bool CreateTables(IEnumerable tables, TimeSpan? timeout = null);// Deletes all DynamoDB Tables
bool DeleteAllTables(TimeSpan? timeout = null);// Deletes the tables in DynamoDB with the specified table names
bool DeleteTables(IEnumerable tableNames, TimeSpan? timeout = null);// Gets the POCO instance with the specified hash
T GetItem(object hash);// Gets the POCO instance with the specified hash and range value
T GetItem(object hash, object range);// Calls 'BatchGetItem' in the min number of batch requests to return POCOs with the specified hashes
List GetItems(IEnumerable hashes);// Calls 'PutItem' to store instance in DynamoDB
T PutItem(T value, bool returnOld = false);// Calls 'BatchWriteItem' to efficiently store items in min number of batched requests
void PutItems(IEnumerable items);// Deletes the instance at the specified hash
T DeleteItem(object hash, ReturnItem returnItem = ReturnItem.None);// Calls 'BatchWriteItem' to efficiently delete all items with the specified hashes
void DeleteItems(IEnumerable hashes);// Calls 'BatchWriteItem' to efficiently delete all items with the specified hash and range pairs
void DeleteItems(IEnumerable hashes);// Calls 'UpdateItem' with ADD AttributeUpdate to atomically increment specific field numeric value
long Increment(object hash, string fieldName, long amount = 1);// Polls 'DescribeTable' until all Tables have an ACTIVE TableStatus
bool WaitForTablesToBeReady(IEnumerable tableNames, TimeSpan? timeout = null);// Polls 'ListTables' until all specified tables have been deleted
bool WaitForTablesToBeDeleted(IEnumerable tableNames, TimeSpan? timeout = null);// Updates item Hash field with hash value then calls 'PutItem' to store the related instance
void PutRelatedItem(object hash, T item);// Updates all item Hash fields with hash value then calls 'PutItems' to store all related instances
void PutRelatedItems(object hash, IEnumerable items);// Calls 'Query' to return all related Items containing the specified hash value
IEnumerable GetRelatedItems(object hash);// Deletes all items with the specified hash and ranges
void DeleteRelatedItems(object hash, IEnumerable ranges);// Calls 'Scan' to return lazy enumerated results that's transparently paged across multiple queries
IEnumerable ScanAll();// Creates a Typed `ScanExpression` for the specified table
ScanExpression FromScan(Expression> filterExpression = null);// Creates a Typed `ScanExpression` for the specified Global Index
ScanExpression FromScanIndex(Expression> filterExpression = null);// Executes the `ScanExpression` returning the specified maximum limit of results
List Scan(ScanExpression request, int limit);// Executes the `ScanExpression` returning lazy results transparently paged across multiple queries
IEnumerable Scan(ScanExpression request);// Executes AWSSDK `ScanRequest` returning the specified maximum limit of results
List Scan(ScanRequest request, int limit);// Executes AWSSDK `ScanRequest` returning lazy results transparently paged across multiple queries
IEnumerable Scan(ScanRequest request);// Executes AWSSDK `ScanRequest` with a custom conversion function to map ScanResponse to results
IEnumerable Scan(ScanRequest request, Func> converter);// Return Live ItemCount using Table ScanRequest
long ScanItemCount();// Return cached ItemCount in summary DescribeTable
long DescribeItemCount();// Creates a Typed `QueryExpression` for the specified table
QueryExpression FromQuery(Expression> keyExpression = null);// Executes the `QueryExpression` returning lazy results transparently paged across multiple queries
IEnumerable Query(QueryExpression request);// Executes the `QueryExpression` returning the specified maximum limit of results
List Query(QueryExpression request, int limit);// Creates a Typed `QueryExpression` for the specified Local or Global Index
QueryExpression FromQueryIndex(Expression> keyExpression = null);// Executes AWSSDK `QueryRequest` returning the specified maximum limit of results
List Query(QueryRequest request, int limit);// Executes AWSSDK `QueryRequest` returning lazy results transparently paged across multiple queries
IEnumerable Query(QueryRequest request);// Executes AWSSDK `QueryRequest` with a custom conversion function to map QueryResponse to results
IEnumerable Query(QueryRequest request, Func> converter);// Create a clone of the PocoDynamo client with different default settings
IPocoDynamo ClientWith(
bool? consistentRead = null,
long? readCapacityUnits = null,
long? writeCapacityUnits = null,
TimeSpan? pollTableStatus = null,
TimeSpan? maxRetryOnExceptionTimeout = null,
int? limit = null,
bool? scanIndexForward = null);// Disposes the underlying IAmazonDynamoDB client
void Close();
}// Available API's with Async equivalents
public interface IPocoDynamoAsync
{
Task CreateMissingTablesAsync(IEnumerable tables,
CancellationToken token = default(CancellationToken));Task WaitForTablesToBeReadyAsync(IEnumerable tableNames,
CancellationToken token = default(CancellationToken));Task InitSchemaAsync();
}
```### PocoDynamo Extension helpers
To maintain a minimumal surface area for PocoDynamo, many additional API's used to provide a more DRY typed API's were moved into
[PocoDynamoExtensions](https://github.com/ServiceStack/ServiceStack.Aws/blob/master/src/ServiceStack.Aws/DynamoDb/PocoDynamoExtensions.cs)```csharp
class PocoDynamoExtensions
{
//Register Table
DynamoMetadataType RegisterTable();
DynamoMetadataType RegisterTable(Type tableType);
void RegisterTables(IEnumerable tableTypes);
void AddValueConverter(Type type, IAttributeValueConverter valueConverter);//Get Table Metadata
Table GetTableSchema();
DynamoMetadataType GetTableMetadata();//Create Table
bool CreateTableIfMissing();
bool CreateTableIfMissing(DynamoMetadataType table);
bool CreateTable(TimeSpan? timeout = null);bool DeleteTable(TimeSpan? timeout = null);
//Decrement API's
long DecrementById(object id, string fieldName, long amount = 1);
long IncrementById(object id, Expression> fieldExpr, long amount = 1);
long DecrementById(object id, Expression> fieldExpr, long amount = 1);List GetAll();
T GetItem(DynamoId id);//Typed API overloads for popular hash object ids
List GetItems(IEnumerable ids);
List GetItems(IEnumerable ids);
List GetItems(IEnumerable ids);void DeleteItems(IEnumerable ids);
void DeleteItems(IEnumerable ids);
void DeleteItems(IEnumerable ids);//Scan Helpers
IEnumerable ScanInto(ScanExpression request);
List ScanInto(ScanExpression request, int limit);//Query Helpers
IEnumerable QueryInto(QueryExpression request);
List QueryInto(QueryExpression request, int limit);
}
```