DynamoSharp is an ORM (Object-Relational Mapping) library for .NET that simplifies interaction with Amazon DynamoDB, with a focus on Single-Table Design. It offers a simple and efficient way to map data models to DynamoDB tables and perform CRUD (Create, Read, Update, Delete) operations intuitively, as well as tracking changes in entities.
Features:
-
Model Mapping: Support for defining primary and secondary keys, global indexes, and relationships between entities.
-
Efficient Queries: Provides a query builder that allows complex searches using partition and sort keys, as well as secondary indexes.
-
Relationship Support: Handles one-to-many and many-to-many relationships between entities.
-
Focus on Single Table Design: Optimizes the use of DynamoDB through single table design, allowing multiple entity types to be stored in a single table and enabling efficient queries.
-
Entity Tracking: Automatically tracks changes in entities to help detect and manage data updates effectively.
-
Version control with Optimistic Locking: Makes sure that updates do not overwrite each other by checking the version of the data before saving changes.
-
Retry Strategies: Automatically handles retries for transient DynamoDB errors such as InternalServerError, LimitExceeded, ProvisionedThroughputExceeded, RequestLimitExceeded, ServiceUnavailable, and Throttling. This feature ensures reliability and removes the need to implement retry logic in your business layer.
DynamoSharp simplifies the use of DynamoDB in .NET applications, providing an abstraction layer that allows developers to focus on business logic without worrying about the details of database interaction, while taking advantage of the benefits of single table design to optimize performance and data efficiency.
dotnet add package DynamoSharpbuilder.Services.AddDynamoSharp(RegionEndpoint.USEast1);
builder.Services.AddDynamoSharpContext<AppContext>(
new TableSchema.Builder()
.WithTableName("dynamosharp")
.WithPartitionKeyName("PK")
.WithSortKeyName("SK")
.AddGlobalSecondaryIndex("GSI1PK-GSI1SK-index", "GSI1PK", "GSI1SK")
.AddGlobalSecondaryIndex("GSI2PK-GSI2SK-index", "GSI2PK", "GSI2SK")
.Build()
);builder.Services.AddDynamoSharp(RegionEndpoint.USEast1, "http://localhost:4566");public class ModelItem
{
public Guid Id { get; set; }
}
public class AppContext : DynamoSharpContext
{
public IDynamoDbSet<ModelItem> Items { get; private set; } = null!;
public AppContext(IDynamoDbContextAdapter dynamoDbContextAdapter,TableSchema tableSchema)
: base(dynamoDbContextAdapter, tableSchema)
{ }
public override void OnModelCreating(IModelBuilder modelBuilder)
{ }
}var newModelItem = new ModelItem { ... };
appContext.Items.Add(newModelItem);
await appContext.BatchWriter.SaveChangesAsync(cancellationToken);var user = await appContext.Query<User>()
.PartitionKey(id.ToString())
.ToEntityAsync(cancellationToken);public class User
{
// IMPORTANT
// The model must have a property named Id
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public SubscriptionLevel SubscriptionLevel { get; set; }
...
}public class AppContext : DynamoSharpContext
{
public IDynamoDbSet<User> Users { get; private set; } = null!;
public AppContext(IDynamoDbContextAdapter dynamoDbContextAdapter, TableSchema tableSchema)
: base(dynamoDbContextAdapter, tableSchema)
{ }
/*
Partition Key and Sort Key are not defined,
so they will be automatically generated by the library.
Example Partition Key:
USER#1
Example Sort Key:
USER#1
*/
}| PartitionKey | SortKey | Id | Name | SubscriptionLevel | |
|---|---|---|---|---|---|
| USER#1 | USER#1 | 1 | Chris | [email protected] | Ultimate |
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public SubscriptionLevel SubscriptionLevel { get; set; }
...
}public class AppContext : DynamoSharpContext
{
public IDynamoDbSet<User> Users { get; private set; } = null!;
public AppContext(IDynamoDbContextAdapter dynamoDbContextAdapter, TableSchema tableSchema)
: base(dynamoDbContextAdapter, tableSchema)
{ }
public override void OnModelCreating(IModelBuilder modelBuilder)
{
// Example Partition Key: USR#[email protected]
modelBuilder.Entity<User>()
.HasPartitionKey(u => u.Email, "USR");
// Example Sort Key: NAME#Chris
modelBuilder.Entity<User>()
.HasSortKey(u => u.Name, "NAME");
}
}| PartitionKey | SortKey | Id | Name | SubscriptionLevel | |
|---|---|---|---|---|---|
| USR#[email protected] | NAME#Chris | 1 | Chris | [email protected] | Ultimate |
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
...
}public class AppContext : DynamoSharpContext
{
public IDynamoDbSet<User> Users { get; private set; } = null!;
public AppContext(IDynamoDbContextAdapter dynamoDbContextAdapter, TableSchema tableSchema)
: base(dynamoDbContextAdapter, tableSchema)
{ }
public override void OnModelCreating(IModelBuilder modelBuilder)
{
/*
Partition Key and Sort Key are not defined,
so they will be automatically generated by the library.
- User
Example Partition Key:
USER#1
Example Sort Key:
USER#1
*/
// Example Global Secondary Index Partition Key:
// USER#D1ADDCA5-2B7B-4DE5-A221-C626C0A677F9
modelBuilder.Entity<User>()
.HasGlobalSecondaryIndexPartitionKey("GSI1PK", u => u.Id, "USER");
// Example Global Secondary Index Sort Key:
// EMAIL#[email protected]
modelBuilder.Entity<User>()
.HasGlobalSecondaryIndexSortKey("GSI1SK", u => u.Email, "EMAIL");
}
}| PartitionKey | SortKey | Id | Name | GSI1PK | GSI1SK | |
|---|---|---|---|---|---|---|
| USER#1 | USER#1 | 1 | Chris | [email protected] | USER#1 | EMAIL#[email protected] |
Sparse indexes to provide a global filter on an item type.
public class Organization
{
public string Name { get; private set; }
public SubscriptionLevel SubscriptionLevel { get; private set; }
public List<User> Users { get; private set; }
...
}
public class User
{
public string Name { get; private set; }
public SubscriptionLevel SubscriptionLevel { get; private set; }
...
}
public enum SubscriptionLevel
{
Admin,
Member,
Pro,
Enterprise
}public class AppContext : DynamoSharpContext
{
public IDynamoDbSet<Organization> Organizations { get; private set; } = null!;
public IDynamoDbSet<User> Users { get; private set; } = null!;
public AppContext(IDynamoDbContextAdapter dynamoDbContextAdapter, TableSchema tableSchema)
: base(dynamoDbContextAdapter, tableSchema)
{ }
public override void OnModelCreating(IModelBuilder modelBuilder)
{
// Example Partition Key: ORG#DynamoSharp
modelBuilder.Entity<Organization>()
.HasPartitionKey(o => o.Name, "ORG");
// Example Sort Key: ORG#DynamoSharp
modelBuilder.Entity<Organization>()
.HasSortKey(o => o.Name, "ORG");
// Example Global Secondary Index Partition Key: ORGANIZATIONS
modelBuilder.Entity<Organization>()
.HasGlobalSecondaryIndexPartitionKey("GSI1PK", "ORGANIZATIONS"); // <- SPARSE INDEX
// Example Global Secondary Index Sort Key: ORG#DynamoSharp
modelBuilder.Entity<Organization>()
.HasGlobalSecondaryIndexSortKey("GSI1SK", o => o.Name);
modelBuilder.Entity<Organization>()
.HasOneToMany(o => o.Users);
// Example Sort Key: USER#Chris
modelBuilder.Entity<User>()
.HasSortKey(u => u.Name, "USER");
}
}| PartitionKey | SortKey | Name | SubscriptionLevel | GSI1PK | GSI1SK |
|---|---|---|---|---|---|
| ORG#Amazon | ORG#Amazon | Amazon | Admin | ORGANIZATIONS | Amazon |
| ORG#DynamoSharp | ORG#DynamoSharp | DynamoSharp | Admin | ORGANIZATIONS | DynamoSharp |
| ORG#Amazon | USER#Chris | Chris | Member |
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
public string Country { get; set; }
....
}
public class Store
{
public int Id { get; set; }
public string Name { get; set; }
public string Phone { get; set; }
public string Email { get; set; }
public Address Address { get; set; }
...
}public class AppContext : DynamoSharpContext
{
public IDynamoDbSet<User> Users { get; private set; } = null!;
public AppContext(IDynamoDbContextAdapter dynamoDbContextAdapter, TableSchema tableSchema)
: base(dynamoDbContextAdapter, tableSchema)
{ }
public override void OnModelCreating(
IModelBuilder modelBuilder)
{
// Example Partition Key: COUNTRY#Mexico
modelBuilder.Entity<Store>()
.HasPartitionKey(s => s.Country, "COUNTRY");
// Example Sort Key: STATE#Tabasco
modelBuilder.Entity<Store>()
.HasSortKey(s => s.State, "STATE");
// Example Global Secondary Index Partition Key:
// CITY#Villahermosa
modelBuilder.Entity<Store>()
.HasGlobalSecondaryIndexPartitionKey("GSI1PK", s => s.City, "CITY");
// Example Global Secondary Index Sort Key:
// ZIPCODE#00000
modelBuilder.Entity<Store>()
.HasGlobalSecondaryIndexSortKey("GSI1SK", s => u.ZipCode, "ZIPCODE");
}
}| PartitionKey | SortKey | Id | Name | Phone | Address | GSI1PK | GSI1SK | |
|---|---|---|---|---|---|---|---|---|
| COUNTRY#Mexico | STATE#Tabasco | 1 | Tacos El Guero | 1234567890 | [email protected] | { ... } | CITY#Villahermosax | ZIPCODE#00000x |
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
public string Country { get; set; }
....
}
public class Store
{
public int Id { get; set; }
public string Name { get; set; }
public string Phone { get; set; }
public string Email { get; set; }
public Address Address { get; set; }
...
}public class AppContext : DynamoSharpContext
{
public IDynamoDbSet<User> Users { get; private set; } = null!;
public AppContext(IDynamoDbContextAdapter dynamoDbContextAdapter, TableSchema tableSchema)
: base(dynamoDbContextAdapter, tableSchema)
{ }
public override void OnModelCreating(
IModelBuilder modelBuilder)
{
// Example Partition Key: COUNTRY#Mexico
modelBuilder.Entity<Store>()
.HasPartitionKey(s => s.Country, "COUNTRY");
// Example Sort Key: STATE#Tabasco#CITY#Villahermosa#ZIPCODE#00000
modelBuilder.Entity<Store>()
.HasSortKey(s => s.State, "STATE")
.Include(s => s.Address.City, "CITY");
// Example Global Secondary Index Partition Key: Mexico
modelBuilder.Entity<Store>()
.HasGlobalSecondaryIndexPartitionKey("GSI1PK", s => s.Country);
// Example Global Secondary Index Sort Key:
// Tabasco#Villahermosa#00000
modelBuilder.Entity<Store>()
.HasGlobalSecondaryIndexSortKey("GSI1SK", s => s.State)
.Include(s => s.Address.City, "CITY")
.Include(s => s.Address.ZipCode, "ZIPCODE");
}
}| PartitionKey | SortKey | Id | Name | Phone | Address | GSI1PK | GSI1SK | |
|---|---|---|---|---|---|---|---|---|
| COUNTRY#Mexico | STATE#Tabasco#CITY#Villahermosa | 1 | Tacos El Guero | 1234567890 | [email protected] | { ... } | Mexico | Tabasco#Villahermosa#00000 |
public class Item
{
public int Id { get; set; }
public string ProductName { get; set; }
public decimal UnitPrice { get; set; }
public int Units { get; set; }
...
}
public class Order
{
public int Id { get; set; }
public int BuyerId { get; set; }
public Address Address { get; set; }
public Status Status { get; set; }
public DateTime Date { get; set; }
private readonly List<Item> _items = new List<Item>();
public IReadOnlyCollection<Item> Items => _items;
...
}public class AppContext : DynamoSharpContext
{
public IDynamoDbSet<Order> Orders { get; private set; } = null!;
public AppContext(IDynamoDbContextAdapter dynamoDbContextAdapter, TableSchema tableSchema)
: base(dynamoDbContextAdapter, tableSchema)
{ }
public override void OnModelCreating(IModelBuilder modelBuilder)
{
/*
Partition Key and Sort Key are not defined,
so they will be automatically generated by the library.
- Order
Example Partition Key:
ORDER#1
Example Sort Key:
ORDER#1
- Item
Example Partition Key:
ORDER#1
Example Sort Key:
ITEM#2
*/
modelBuilder.Entity<Order>()
.HasOneToMany(o => o.Items);
}
}| PartitionKey | SortKey | Id | BuyerId | Address | Status | Date | ProductName | UnitPrice | Units |
|---|---|---|---|---|---|---|---|---|---|
| ORDER#1 | ORDER#1 | 1 | 26 | { ... } | Shipped | 2024-08-28T19:41:50.9509387-06:00 | |||
| ORDER#1 | ITEM#2 | 2 | Product 1 | 26.0 | 5 |
public class Organization
{
public string Name { get; private set; }
public SubscriptionLevel SubscriptionLevel { get; private set; }
public List<User> Users { get; private set; }
...
}
public class User
{
public string Name { get; private set; }
public SubscriptionLevel SubscriptionLevel { get; private set; }
...
}
public enum SubscriptionLevel
{
Admin,
Member,
Pro,
Enterprise
}public class AppContext : DynamoSharpContext
{
public IDynamoDbSet<Organization> Organizations { get; private set; } = null!;
public IDynamoDbSet<User> Users { get; private set; } = null!;
public AppContext(IDynamoDbContextAdapter dynamoDbContextAdapterAdapter, TableSchema tableSchema)
: base(dynamoDbContextAdapterAdapter, tableSchema)
{ }
public override void OnModelCreating(
IModelBuilder modelBuilder)
{
// Example Partition Key: ORG#DynamoSharp
modelBuilder.Entity<Organization>()
.HasPartitionKey(o => o.Name, "ORG");
// Example Sort Key: ORG#DynamoSharp
modelBuilder.Entity<Organization>()
.HasSortKey(o => o.Name, "ORG");
modelBuilder.Entity<Organization>()
.HasOneToMany(o => o.Users);
// Example Sort Key: USER#Chris
modelBuilder.Entity<User>()
.HasSortKey(o => o.Name, "USER");
}
}| PartitionKey | SortKey | Name | SubscriptionLevel |
|---|---|---|---|
| ORG#Amazon | ORG#Amazon | Amazon | Admin |
| ORG#Amazon | USER#Chris | Chris | Member |
public class Movie
{
// IMPORTANT
// The model must have a property named Id
public int Id { get; set; }
public string Title { get; set; }
public List<Performance> Actors { get; set; }
...
}
public class Performance
{
// IMPORTANT
// The model must have both Ids, in this case MovieId and ActorId
public int MovieId { get; set; }
public int ActorId { get; set; }
public string MovieTitle { get; set; }
public string ActorName { get; set; }
public string RoleName { get; set; }
...
}
public partial class Actor
{
// IMPORTANT
// The model must have a property named Id
public int Id { get; set; }
public string Name { get; set; }
public List<Performance> Movies { get; set; }
...
}public class AppContext : DynamoSharpContext
{
public IDynamoDbSet<Movie> Movies { get; private set; } = null!;
public IDynamoDbSet<Actor> Actors { get; private set; } = null!;
public AppContext(IDynamoDbContextAdapter dynamoDbContextAdapter, TableSchema tableSchema)
: base(dynamoDbContextAdapter, tableSchema)
{ }
public override void OnModelCreating(IModelBuilder modelBuilder)
{
/*
Partition Key and Sort Key are not defined,
so they will be automatically generated by the library.
- Movie
Example Partition Key:
MOVIE#1
Example Sort Key:
MOVIE#1
- Actor
Example Partition Key:
ACTOR#2
Example Sort Key:
ACTOR#2
Global Secondary Index Partition Key:
ACTOR#2
Global Secondary Index Sort Key:
ACTOR#2
- Performance
Example Partition Key:
MOVIE#1
Example Sort Key:
ACTOR#2
Global Secondary Index Partition Key:
ACTOR#2
Global Secondary Index Sort Key:
MOVIE#1
*/
modelBuilder.Entity<Movie>()
.HasManyToMany(m => m.Actors);
modelBuilder.Entity<Actor>()
.HasManyToMany(a => a.Movies);
}
}| PartitionKey | SortKey | Id | Title | MovieId | ActorId | MovieTitle | ActorName | RoleName | Name | GSI1PK | GSI1SK |
|---|---|---|---|---|---|---|---|---|---|---|---|
| MOVIE#1 | MOVIE#1 | 1 | The Matrix | ||||||||
| MOVIE#1 | ACTOR#2 | 1 | 2 | The Matrix | Keanu Reeves | Neo | ACTOR#2 | MOVIE#1 | |||
| ACTOR#2 | ACTOR#2 | 2 | Keanu Reeves | ACTOR#2 | ACTOR#2 |
public class Movie
{
public string Title { get; set; }
public List<Performance> Actors { get; set; }
...
}
public class Performance
{
public string MovieTitle { get; set; }
public string ActorName { get; set; }
public string RoleName { get; set; }
...
}
public partial class Actor
{
public string Name { get; set; }
public List<Performance> Movies { get; set; }
...
}public class AppContext : DynamoSharpContext
{
public IDynamoDbSet<Movie> Movies { get; private set; } = null!;
public IDynamoDbSet<Actor> Actors { get; private set; } = null!;
public AppContext(IDynamoDbContextAdapter dynamoDbContextAdapter, TableSchema tableSchema)
: base(dynamoDbContextAdapter, tableSchema)
{ }
public override void OnModelCreating(
IModelBuilder modelBuilder)
{
MovieModelBuilder(modelBuilder);
ActorModelBuilder(modelBuilder);
PerformanceModelBuilder(modelBuilder);
}
private void MovieModelBuilder(
IModelBuilder modelBuilder)
{
// Example Partition Key: MOVIE#The Matrix
modelBuilder.Entity<Movie>()
.HasPartitionKey(m => m.Title, "MOVIE");
// Example Sort Key: MOVIE#The Matrix
modelBuilder.Entity<Movie>()
.HasSortKey(m => m.Title, "MOVIE");
modelBuilder.Entity<Movie>()
.HasManyToMany(m => m.Actors);
}
private void ActorModelBuilder(
IModelBuilder modelBuilder)
{
// Example Partition Key: ACTOR#Keanu Reeves
modelBuilder.Entity<Actor>()
.HasPartitionKey(a => a.Name, "ACTOR");
// Example Sort Key: ACTOR#Keanu Reeves
modelBuilder.Entity<Actor>()
.HasSortKey(a => a.Name, "ACTOR");
modelBuilder.Entity<Actor>()
.HasManyToMany(a => a.Movies);
// Example Global Secondary Index Partition Key: ACTOR#Keanu Reeves
modelBuilder.Entity<Actor>()
.HasGlobalSecondaryIndexPartitionKey("GSI1PK", a => a.Name, "ACTOR");
// Example Global Secondary Index Sort Key: ACTOR#Keanu Reeves
modelBuilder.Entity<Actor>()
.HasGlobalSecondaryIndexSortKey("GSI1SK", a => a.Name, "ACTOR");
}
private void PerformanceModelBuilder(
IModelBuilder modelBuilder)
{
// Example Partition Key: MOVIE#The Matrix
modelBuilder.Entity<Performance>()
.HasPartitionKey(p => p.MovieTitle, "MOVIE");
// Example Sort Key: ACTOR#Keanu Reeves
modelBuilder.Entity<Performance>()
.HasSortKey(p => p.ActorName, "ACTOR");
// Example Global Secondary Index Partition Key: ACTOR#Keanu Reeves
modelBuilder.Entity<Performance>()
.HasGlobalSecondaryIndexPartitionKey("GSI1PK", p => p.ActorName, "ACTOR");
// Example Global Secondary Index Sort Key: MOVIE#The Matrix
modelBuilder.Entity<Performance>()
.HasGlobalSecondaryIndexSortKey("GSI1SK", p => p.MovieTitle, "MOVIE");
}
}| PartitionKey | SortKey | Title | MovieTitle | ActorName | RoleName | Name | GSI1PK | GSI1SK |
|---|---|---|---|---|---|---|---|---|
| MOVIE#The Matrix | MOVIE#The Matrix | The Matrix | ||||||
| MOVIE#The Matrix | ACTOR#Keanu Reeves | The Matrix | Keanu Reeves | Neo | ACTOR#Keanu Reeves | MOVIE#The Matrix | ||
| ACTOR#Keanu Reeves | ACTOR#Keanu Reeves | Keanu Reeves | ACTOR#Keanu Reeves | ACTOR#Keanu Reeves |
appContext.Users.Add(newUser);
// Save with batch
await appContext.BatchWriter.SaveChangesAsync(cancellationToken);
// Save with transaction
await appContext.TransactWriter.SaveChangesAsync(cancellationToken);appContext.Users.Add(newUser);
await appContext.BatchWriter.SaveChangesAsync(cancellationToken);
newUser.Email = "[email protected]";
// Update with batch
await appContext.BatchWriter.SaveChangesAsync(cancellationToken);
// Update with transaction
await appContext.TransactWriter.SaveChangesAsync(cancellationToken);// Find user by id...
_appContext.Users.Remove(newUser);
// Save with batch
await appContext.BatchWriter.SaveChangesAsync(cancellationToken);
// Save with transaction
await appContext.TransactWriter.SaveChangesAsync(cancellationToken);var user = await appContext.Query<User>()
.PartitionKey($"USER#{id}")
.ToEntityAsync(cancellationToken); var item = await appContext.Query<Item>()
.PartitionKey($"ORDER#{orderId}")
.SortKey(QueryOperator.Equal, $"ITEM#{itemId}")
.ToEntityAsync(cancellationToken);var user = await appContext.Query<User>()
.IndexName("GSI1PK-GSI1SK-index")
.PartitionKey("GSI1PK", $"USER#{id}")
.ToEntityAsync(cancellationToken);var item = await appContext.Query<Item>()
.IndexName("GSI1PK-GSI1SK-index")
.PartitionKey("GSI1PK", $"ORDER#{orderId}")
.SortKey("GSI1SK", QueryOperator.BeginsWith, "ITEM#")
.ToListAsync(cancellationToken);var item = await appContext.Query<Item>()
.IndexName("GSI1PK-GSI1SK-index")
.PartitionKey("GSI1PK", $"ORDER#{orderId}")
.SortKey("GSI1SK", QueryOperator.BeginsWith, "ITEM#")
.Filter(item => item.Units >= 5 && item.Units < 10)
.ToListAsync(cancellationToken);var items = await appContext.Query<Item>()
.IndexName("GSI1PK-GSI1SK-index")
.PartitionKey("GSI1PK", $"ORDER#{orderId}")
.SortKey("GSI1SK", QueryOperator.BeginsWith, "ITEM#")
.Limit(limit)
.ConsistentRead()
.ScanIndexForward()
.AsNoTracking()
.ToListAsync(cancellationToken);builder.Services.AddDynamoSharpContext<AppContext>(
new TableSchema.Builder()
.WithTableName("dynamosharp")
.WithVersionName("v")// attribute used for optimistic locking
.Build()
);public class EcommerceContext : DynamoSharpContext
{
...
public override void OnModelCreating(IModelBuilder modelBuilder)
{
...
// Enable optimistic locking for Order entity
modelBuilder.Entity<Order>()
.HasVersioning();
// Enable optimistic locking for Item entity
modelBuilder.Entity<Item>()
.HasVersioning();
}
}DynamoSharp does not support concurrent operations on the same DynamoSharpContext instance. Avoid running multiple asynchronous queries in parallel or accessing the context from multiple threads simultaneously. Always await async calls immediately or use separate DynamoSharpContext instances for parallel operations.
