| title | Entity Definition | ||||||||
|---|---|---|---|---|---|---|---|---|---|
| category | core-features | ||||||||
| order | 1 | ||||||||
| keywords |
|
||||||||
| related |
|
Documentation > Core Features > Entity Definition
This guide covers comprehensive entity definition patterns in Oproto.FluentDynamoDb, including advanced features like computed keys, extracted keys, Global Secondary Indexes, and queryable attributes.
Every DynamoDB entity requires:
[DynamoDbTable]attribute - Specifies the table namepartialkeyword - Enables source generation- At least one
[PartitionKey]- Defines the partition key [DynamoDbAttribute]on properties - Maps properties to DynamoDB attributes
using Oproto.FluentDynamoDb.Attributes;
[DynamoDbTable("users")]
public partial class User
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string UserId { get; set; } = string.Empty;
[DynamoDbAttribute("email")]
public string Email { get; set; } = string.Empty;
}Marks a class as a DynamoDB entity and specifies the table name:
// Simple table name
[DynamoDbTable("users")]
public partial class User { }
// Legacy entity discriminator (deprecated - use new discriminator properties)
[DynamoDbTable("entities", EntityDiscriminator = "USER")]
public partial class User { }Entity Discriminator Use Case: When storing multiple entity types in a single table (single-table design), the discriminator helps identify the entity type.
The library supports flexible discriminator strategies for single-table designs:
Use a dedicated attribute to identify entity types:
[DynamoDbTable("entities",
DiscriminatorProperty = "entity_type",
DiscriminatorValue = "USER")]
public partial class User { }
[DynamoDbTable("entities",
DiscriminatorProperty = "entity_type",
DiscriminatorValue = "ORDER")]
public partial class Order { }Use sort key prefixes to identify entity types:
[DynamoDbTable("entities",
DiscriminatorProperty = "SK",
DiscriminatorPattern = "USER#*")]
public partial class User
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string TenantId { get; set; } = string.Empty;
[SortKey]
[DynamoDbAttribute("sk")]
public string SortKey { get; set; } = string.Empty; // e.g., "USER#user123"
}Discriminator patterns support wildcard matching:
| Pattern | Matches | Example |
|---|---|---|
USER#* |
Starts with "USER#" | USER#123, USER#abc |
*#USER |
Ends with "#USER" | TENANT#abc#USER |
*#USER#* |
Contains "#USER#" | TENANT#abc#USER#123 |
USER |
Exact match | USER only |
Different discriminators can be used for GSI queries:
[DynamoDbTable("entities",
DiscriminatorProperty = "SK",
DiscriminatorPattern = "USER#*")]
public partial class User
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string TenantId { get; set; } = string.Empty;
[SortKey]
[DynamoDbAttribute("sk")]
public string SortKey { get; set; } = string.Empty;
// GSI uses different discriminator pattern
[GlobalSecondaryIndex("StatusIndex",
IsPartitionKey = true,
DiscriminatorProperty = "GSI1SK",
DiscriminatorPattern = "USER#*")]
[DynamoDbAttribute("status")]
public string Status { get; set; } = string.Empty;
[GlobalSecondaryIndex("StatusIndex", IsSortKey = true)]
[DynamoDbAttribute("gsi1sk")]
public string StatusSortKey { get; set; } = string.Empty;
}Backward Compatibility: The legacy EntityDiscriminator property is still supported but deprecated. It's equivalent to setting DiscriminatorProperty="entity_type" and DiscriminatorValue to the discriminator value.
Maps C# properties to DynamoDB attribute names:
[DynamoDbTable("products")]
public partial class Product
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string ProductId { get; set; } = string.Empty;
// Property name: Name
// DynamoDB attribute name: product_name
[DynamoDbAttribute("product_name")]
public string Name { get; set; } = string.Empty;
// Property name: Price
// DynamoDB attribute name: price
[DynamoDbAttribute("price")]
public decimal Price { get; set; }
}Best Practice: Use short, generic attribute names (like pk, sk) for single-table design, or descriptive names (like userId, email) for dedicated tables.
Every entity must have exactly one partition key:
[DynamoDbTable("users")]
public partial class User
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string UserId { get; set; } = string.Empty;
}Generated Code:
// UserKeys.g.cs
public static class UserKeys
{
public static string Pk(string userId)
{
return userId;
}
}Add a sort key for composite primary keys:
[DynamoDbTable("orders")]
public partial class Order
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string CustomerId { get; set; } = string.Empty;
[SortKey]
[DynamoDbAttribute("sk")]
public string OrderId { get; set; } = string.Empty;
}Generated Code:
// OrderKeys.g.cs
public static class OrderKeys
{
public static string Pk(string customerId)
{
return customerId;
}
public static string Sk(string orderId)
{
return orderId;
}
}Add prefixes to partition and sort keys for better organization:
[DynamoDbTable("entities")]
public partial class User
{
[PartitionKey(Prefix = "USER")]
[DynamoDbAttribute("pk")]
public string UserId { get; set; } = string.Empty;
[SortKey(Prefix = "PROFILE")]
[DynamoDbAttribute("sk")]
public string ProfileType { get; set; } = "MAIN";
}Generated Code:
// UserKeys.g.cs
public static class UserKeys
{
public static string Pk(string userId)
{
return $"USER#{userId}";
}
public static string Sk(string profileType)
{
return $"PROFILE#{profileType}";
}
}Usage:
// UserKeys.Pk("user123") returns "USER#user123"
// UserKeys.Sk("MAIN") returns "PROFILE#MAIN"
await table.Get
.WithKey(UserFields.UserId, UserKeys.Pk("user123"))
.WithKey(UserFields.ProfileType, UserKeys.Sk("MAIN"))
.ExecuteAsync<User>();Change the default separator from # to another character:
[DynamoDbTable("entities")]
public partial class User
{
[PartitionKey(Prefix = "USER", Separator = "|")]
[DynamoDbAttribute("pk")]
public string UserId { get; set; } = string.Empty;
}
// Generated: UserKeys.Pk("user123") returns "USER|user123"Use the [Computed] attribute to create keys from multiple properties or with custom formatting:
[DynamoDbTable("products")]
public partial class Product
{
// Source property
public string ProductId { get; set; } = string.Empty;
// Computed from ProductId with format string
[PartitionKey]
[Computed(nameof(ProductId), Format = "PRODUCT#{0}")]
[DynamoDbAttribute("pk")]
public string PartitionKey { get; set; } = string.Empty;
}Generated Code:
public static class ProductKeys
{
public static string Pk(string productId)
{
return $"PRODUCT#{productId}";
}
}Combine multiple properties into a single key:
[DynamoDbTable("events")]
public partial class Event
{
public string TenantId { get; set; } = string.Empty;
public string EventType { get; set; } = string.Empty;
// Computed from TenantId and EventType
[PartitionKey]
[Computed(nameof(TenantId), nameof(EventType), Format = "TENANT#{0}#EVENT#{1}")]
[DynamoDbAttribute("pk")]
public string PartitionKey { get; set; } = string.Empty;
}Generated Code:
public static class EventKeys
{
public static string Pk(string tenantId, string eventType)
{
return $"TENANT#{tenantId}#EVENT#{eventType}";
}
}Usage:
await table.Get
.WithKey(EventFields.PartitionKey, EventKeys.Pk("tenant123", "LOGIN"))
.ExecuteAsync<Event>();Use .NET format specifiers for DateTime properties:
[DynamoDbTable("logs")]
public partial class LogEntry
{
public DateTime Timestamp { get; set; }
// Format as ISO 8601: 2024-03-15T10:30:00.000Z
[PartitionKey]
[Computed(nameof(Timestamp), Format = "LOG#{0:o}")]
[DynamoDbAttribute("pk")]
public string PartitionKey { get; set; } = string.Empty;
// Format as date only: 2024-03-15
[SortKey]
[Computed(nameof(Timestamp), Format = "{0:yyyy-MM-dd}")]
[DynamoDbAttribute("sk")]
public string DateKey { get; set; } = string.Empty;
}Common DateTime Formats:
:o- ISO 8601 (2024-03-15T10:30:00.000Z):yyyy-MM-dd- Date only (2024-03-15):yyyy-MM- Year-month (2024-03):yyyyMMddHHmmss- Compact timestamp (20240315103000)
Format numbers with padding or precision:
[DynamoDbTable("versions")]
public partial class Version
{
public int VersionNumber { get; set; }
// Format with zero-padding: v001, v002, v010, v100
[SortKey]
[Computed(nameof(VersionNumber), Format = "v{0:D3}")]
[DynamoDbAttribute("sk")]
public string VersionKey { get; set; } = string.Empty;
}Common Numeric Formats:
:D3- Zero-padded integer (001, 010, 100):F2- Fixed-point with 2 decimals (10.50):N0- Number with thousands separator (1,000)
If you don't specify a format, properties are joined with the separator:
[DynamoDbTable("composite")]
public partial class CompositeEntity
{
public string TenantId { get; set; } = string.Empty;
public string UserId { get; set; } = string.Empty;
// Uses default separator "#"
[PartitionKey]
[Computed(nameof(TenantId), nameof(UserId))]
[DynamoDbAttribute("pk")]
public string PartitionKey { get; set; } = string.Empty;
}
// Generated: CompositeEntityKeys.Pk("tenant1", "user1") returns "tenant1#user1"Custom Separator:
[PartitionKey]
[Computed(nameof(TenantId), nameof(UserId), Separator = "|")]
[DynamoDbAttribute("pk")]
public string PartitionKey { get; set; } = string.Empty;
// Generated: CompositeEntityKeys.Pk("tenant1", "user1") returns "tenant1|user1"Use the [Extracted] attribute to extract components from composite keys:
[DynamoDbTable("events")]
public partial class Event
{
// Composite partition key: "TENANT#tenant123"
[PartitionKey]
[DynamoDbAttribute("pk")]
public string PartitionKey { get; set; } = string.Empty;
// Extract tenant ID from partition key (index 1 after splitting by #)
[Extracted(nameof(PartitionKey), 1, Separator = "#")]
public string TenantId { get; set; } = string.Empty;
}How It Works:
PartitionKeyvalue:"TENANT#tenant123"- Split by separator
#:["TENANT", "tenant123"] - Extract index
1:"tenant123" - Assign to
TenantId
Extract multiple components from the same key:
[DynamoDbTable("hierarchical")]
public partial class HierarchicalEntity
{
// Composite key: "ORG#org1#DEPT#dept2#USER#user3"
[PartitionKey]
[DynamoDbAttribute("pk")]
public string PartitionKey { get; set; } = string.Empty;
// Extract organization (index 1)
[Extracted(nameof(PartitionKey), 1, Separator = "#")]
public string OrganizationId { get; set; } = string.Empty;
// Extract department (index 3)
[Extracted(nameof(PartitionKey), 3, Separator = "#")]
public string DepartmentId { get; set; } = string.Empty;
// Extract user (index 5)
[Extracted(nameof(PartitionKey), 5, Separator = "#")]
public string UserId { get; set; } = string.Empty;
}Extract from sort keys as well:
[DynamoDbTable("timeseries")]
public partial class TimeSeriesData
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string SensorId { get; set; } = string.Empty;
// Sort key: "2024-03-15#10:30:00"
[SortKey]
[DynamoDbAttribute("sk")]
public string TimestampKey { get; set; } = string.Empty;
// Extract date (index 0)
[Extracted(nameof(TimestampKey), 0, Separator = "#")]
public string Date { get; set; } = string.Empty;
// Extract time (index 1)
[Extracted(nameof(TimestampKey), 1, Separator = "#")]
public string Time { get; set; } = string.Empty;
}Use Case: Query by date range while maintaining time precision in the sort key.
Define Global Secondary Indexes (GSIs) using the [GlobalSecondaryIndex] attribute:
[DynamoDbTable("users")]
public partial class User
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string UserId { get; set; } = string.Empty;
// GSI partition key
[GlobalSecondaryIndex("EmailIndex", IsPartitionKey = true)]
[DynamoDbAttribute("email")]
public string Email { get; set; } = string.Empty;
}Generated Code:
// UserIndexes.g.cs
public static class UserIndexes
{
public const string EmailIndex = "EmailIndex";
}
// UserFields.g.cs
public static class UserFields
{
public const string Email = "email";
}Usage:
// Query by email using GSI
var response = await table.Query
.UsingIndex(UserIndexes.EmailIndex)
.Where($"{UserFields.Email} = {{0}}", "john@example.com")
.ExecuteAsync<User>();[DynamoDbTable("orders")]
public partial class Order
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string OrderId { get; set; } = string.Empty;
// GSI partition key
[GlobalSecondaryIndex("StatusIndex", IsPartitionKey = true)]
[DynamoDbAttribute("status")]
public string Status { get; set; } = string.Empty;
// GSI sort key
[GlobalSecondaryIndex("StatusIndex", IsSortKey = true)]
[DynamoDbAttribute("createdAt")]
public DateTime CreatedAt { get; set; }
}Usage:
// Query orders by status, sorted by creation date
var response = await table.Query
.UsingIndex(OrderIndexes.StatusIndex)
.Where($"{OrderFields.Status} = {{0}}", "pending")
.ExecuteAsync<Order>();Define multiple GSIs on the same entity:
[DynamoDbTable("products")]
public partial class Product
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string ProductId { get; set; } = string.Empty;
// GSI 1: Query by category
[GlobalSecondaryIndex("CategoryIndex", IsPartitionKey = true)]
[DynamoDbAttribute("category")]
public string Category { get; set; } = string.Empty;
// GSI 2: Query by vendor
[GlobalSecondaryIndex("VendorIndex", IsPartitionKey = true)]
[DynamoDbAttribute("vendorId")]
public string VendorId { get; set; } = string.Empty;
// GSI 2 sort key
[GlobalSecondaryIndex("VendorIndex", IsSortKey = true)]
[DynamoDbAttribute("price")]
public decimal Price { get; set; }
}Generated Code:
public static class ProductIndexes
{
public const string CategoryIndex = "CategoryIndex";
public const string VendorIndex = "VendorIndex";
}Combine GSIs with computed keys for advanced patterns:
[DynamoDbTable("transactions")]
public partial class Transaction
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string TransactionId { get; set; } = string.Empty;
public string TenantId { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
// GSI partition key: "TENANT#tenant123#STATUS#pending"
[GlobalSecondaryIndex("TenantStatusIndex", IsPartitionKey = true)]
[Computed(nameof(TenantId), nameof(Status), Format = "TENANT#{0}#STATUS#{1}")]
[DynamoDbAttribute("gsi1pk")]
public string TenantStatusKey { get; set; } = string.Empty;
// GSI sort key
[GlobalSecondaryIndex("TenantStatusIndex", IsSortKey = true)]
[DynamoDbAttribute("gsi1sk")]
public DateTime CreatedAt { get; set; }
}Use Case: Query all pending transactions for a tenant, sorted by creation date.
Mark properties as queryable to document supported operations (useful for future LINQ support):
[DynamoDbTable("products")]
public partial class Product
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string ProductId { get; set; } = string.Empty;
// Supports equality and range comparisons
[QueryableAttribute(
SupportedOperations = new[] {
DynamoDbOperation.Equals,
DynamoDbOperation.GreaterThan,
DynamoDbOperation.LessThan,
DynamoDbOperation.Between
}
)]
[DynamoDbAttribute("price")]
public decimal Price { get; set; }
// Supports string operations
[QueryableAttribute(
SupportedOperations = new[] {
DynamoDbOperation.Equals,
DynamoDbOperation.BeginsWith,
DynamoDbOperation.Contains
}
)]
[DynamoDbAttribute("name")]
public string Name { get; set; } = string.Empty;
// Available in specific indexes
[QueryableAttribute(
SupportedOperations = new[] { DynamoDbOperation.Equals },
AvailableInIndexes = new[] { "CategoryIndex", "VendorIndex" }
)]
[DynamoDbAttribute("category")]
public string Category { get; set; } = string.Empty;
}Supported Operations:
Equals- Equality comparison (=)BeginsWith- String prefix matchingBetween- Range comparisonGreaterThan- Greater than (>)LessThan- Less than (<)Contains- Set/string containsIn- Multiple value matching
Note: This attribute is primarily for documentation and future LINQ support. It doesn't enforce query restrictions at runtime.
// ✅ Good - consistent key format enforced by source generator
[PartitionKey]
[Computed(nameof(UserId), Format = "USER#{0}")]
[DynamoDbAttribute("pk")]
public string PartitionKey { get; set; } = string.Empty;
// ❌ Avoid - manual key construction prone to errors
[PartitionKey]
[DynamoDbAttribute("pk")]
public string PartitionKey { get; set; } = string.Empty;
// Manual construction elsewhere:
user.PartitionKey = $"USER#{user.UserId}"; // Easy to make mistakes// ✅ Good - can query by TenantId without parsing
[PartitionKey]
[DynamoDbAttribute("pk")]
public string PartitionKey { get; set; } = string.Empty;
[Extracted(nameof(PartitionKey), 1, Separator = "#")]
public string TenantId { get; set; } = string.Empty;
// Query: WHERE TenantId = 'tenant123'// ✅ Good - supports multiple query patterns
[PartitionKey]
[DynamoDbAttribute("pk")]
public string OrderId { get; set; } = string.Empty;
// Access pattern 1: Query by customer
[GlobalSecondaryIndex("CustomerIndex", IsPartitionKey = true)]
[DynamoDbAttribute("customerId")]
public string CustomerId { get; set; } = string.Empty;
// Access pattern 2: Query by status
[GlobalSecondaryIndex("StatusIndex", IsPartitionKey = true)]
[DynamoDbAttribute("status")]
public string Status { get; set; } = string.Empty;// ✅ Good - sortable timestamp format
[SortKey]
[Computed(nameof(Timestamp), Format = "{0:yyyy-MM-ddTHH:mm:ss.fffZ}")]
[DynamoDbAttribute("sk")]
public string TimestampKey { get; set; } = string.Empty;
// ❌ Avoid - not sortable
[SortKey]
[Computed(nameof(Timestamp), Format = "{0:MM/dd/yyyy}")]
[DynamoDbAttribute("sk")]
public string TimestampKey { get; set; } = string.Empty;// ✅ Good - documents supported operations
[QueryableAttribute(
SupportedOperations = new[] {
DynamoDbOperation.Equals,
DynamoDbOperation.GreaterThan
}
)]
[DynamoDbAttribute("price")]
public decimal Price { get; set; }// ✅ Good - simple, readable format
[Computed(nameof(UserId), Format = "USER#{0}")]
// ❌ Avoid - overly complex format
[Computed(nameof(UserId), nameof(TenantId), nameof(Region),
Format = "TENANT#{1}#REGION#{2}#USER#{0}#ACTIVE")]Here's a comprehensive entity using all features:
using Oproto.FluentDynamoDb.Attributes;
[DynamoDbTable("orders", EntityDiscriminator = "ORDER")]
public partial class Order
{
// Source properties
public string OrderId { get; set; } = string.Empty;
public string CustomerId { get; set; } = string.Empty;
public string Status { get; set; } = "pending";
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }
// Computed partition key: "ORDER#order123"
[PartitionKey]
[Computed(nameof(OrderId), Format = "ORDER#{0}")]
[DynamoDbAttribute("pk")]
public string PartitionKey { get; set; } = string.Empty;
// Computed sort key: "METADATA"
[SortKey]
[Computed(Format = "METADATA")]
[DynamoDbAttribute("sk")]
public string SortKey { get; set; } = "METADATA";
// Extract OrderId from partition key
[Extracted(nameof(PartitionKey), 1, Separator = "#")]
public string ExtractedOrderId { get; set; } = string.Empty;
// GSI 1: Query by customer
[GlobalSecondaryIndex("CustomerIndex", IsPartitionKey = true)]
[Computed(nameof(CustomerId), Format = "CUSTOMER#{0}")]
[DynamoDbAttribute("gsi1pk")]
public string CustomerKey { get; set; } = string.Empty;
[GlobalSecondaryIndex("CustomerIndex", IsSortKey = true)]
[DynamoDbAttribute("gsi1sk")]
public DateTime CustomerIndexSortKey { get; set; }
// GSI 2: Query by status
[GlobalSecondaryIndex("StatusIndex", IsPartitionKey = true)]
[DynamoDbAttribute("status")]
public string StatusKey { get; set; } = string.Empty;
[GlobalSecondaryIndex("StatusIndex", IsSortKey = true)]
[DynamoDbAttribute("createdAt")]
public DateTime StatusIndexSortKey { get; set; }
// Regular attributes
[QueryableAttribute(
SupportedOperations = new[] {
DynamoDbOperation.Equals,
DynamoDbOperation.GreaterThan,
DynamoDbOperation.LessThan
}
)]
[DynamoDbAttribute("total")]
public decimal Total { get; set; }
[DynamoDbAttribute("items")]
public List<OrderItem> Items { get; set; } = new();
}
public class OrderItem
{
public string ProductId { get; set; } = string.Empty;
public int Quantity { get; set; }
public decimal Price { get; set; }
}- Basic Operations - CRUD operations with entities
- Querying Data - Query and scan operations
- Global Secondary Indexes - Advanced GSI patterns
- Attribute Reference - Complete attribute documentation
Previous: Core Features | Next: Basic Operations
See Also: