Skip to main content

Entities

Everything in Oproto.FluentDynamoDb is entity-based. An entity is a C# class that maps directly to items in your DynamoDB table. By decorating your classes with attributes, the source generator creates type-safe table classes, key builders, and field constants that make working with DynamoDB feel natural in C#.

Entities serve as the foundation for:

  • Type-safe operations - Get, Put, Update, and Delete with compile-time checking
  • Key generation - Automatic key formatting with prefixes and computed values
  • Query building - Lambda expressions and format strings that reference your properties

Entity Declaration

Every DynamoDB entity requires:

  1. [DynamoDbTable] attribute - Specifies the table name
  2. partial keyword - Enables source generation to extend your class
  3. At least one [PartitionKey] - Defines the partition key
  4. [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;

[DynamoDbAttribute("name")]
public string Name { get; set; } = string.Empty;
}

The partial keyword is essential—it allows the source generator to create companion code including:

  • A table class (UsersTable) with CRUD operations
  • A fields class (UserFields) with attribute name constants
  • A keys class (UserKeys) with key formatting methods

Partition Keys and Sort Keys

Partition Key

Every entity must have exactly one partition key, which uniquely identifies items (or groups of items when combined with a sort key):

[DynamoDbTable("users")]
public partial class User
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string UserId { get; set; } = string.Empty;
}

Sort Key

Add a sort key for composite primary keys, enabling multiple items to share the same partition key:

[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;
}

Key Prefixes

Add prefixes to partition and sort keys for better organization, especially in single-table designs:

[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";
}

The source generator creates key builder methods that automatically apply prefixes:

// Generated UserKeys class
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"

Custom Separators

Change the default # separator to another character:

[PartitionKey(Prefix = "USER", Separator = "|")]
[DynamoDbAttribute("pk")]
public string UserId { get; set; } = string.Empty;

// UserKeys.Pk("user123") returns "USER|user123"

Property Mapping

The [DynamoDbAttribute] attribute maps C# property names 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: product_name
[DynamoDbAttribute("product_name")]
public string Name { get; set; } = string.Empty;

// Property name: Price → DynamoDB attribute: price
[DynamoDbAttribute("price")]
public decimal Price { get; set; }

// Property name: CreatedAt → DynamoDB attribute: created_at
[DynamoDbAttribute("created_at")]
public DateTime CreatedAt { get; set; }
}

The source generator creates a Fields class with constants for each attribute:

// Generated ProductFields class
public static class ProductFields
{
public const string ProductId = "pk";
public const string Name = "product_name";
public const string Price = "price";
public const string CreatedAt = "created_at";
}

Naming Conventions

Choose attribute names based on your table design:

  • Single-table design: Use short, generic names (pk, sk, gsi1pk) since multiple entity types share the same attributes
  • Dedicated tables: Use descriptive names (userId, email, orderDate) for clarity
// Single-table design - generic attribute names
[DynamoDbTable("entities")]
public partial class User
{
[PartitionKey]
[DynamoDbAttribute("pk")] // Generic
public string UserId { get; set; } = string.Empty;

[SortKey]
[DynamoDbAttribute("sk")] // Generic
public string SortKey { get; set; } = string.Empty;
}

// Dedicated table - descriptive attribute names
[DynamoDbTable("users")]
public partial class User
{
[PartitionKey]
[DynamoDbAttribute("userId")] // Descriptive
public string UserId { get; set; } = string.Empty;

[DynamoDbAttribute("email")] // Descriptive
public string Email { get; set; } = string.Empty;
}

Computed Keys

Use the [Computed] attribute to create keys from multiple properties or with custom formatting. This ensures consistent key formats across your application.

Simple Computed Keys

Create a key from a single property with a format string:

[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;
}

The source generator creates a key builder that applies the format:

// Generated ProductKeys class
public static class ProductKeys
{
public static string Pk(string productId)
{
return $"PRODUCT#{productId}";
}
}

// Usage
ProductKeys.Pk("prod123") // Returns "PRODUCT#prod123"

Multi-Property Computed Keys

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 EventKeys class
public static class EventKeys
{
public static string Pk(string tenantId, string eventType)
{
return $"TENANT#{tenantId}#EVENT#{eventType}";
}
}

// Usage
EventKeys.Pk("tenant123", "LOGIN") // Returns "TENANT#tenant123#EVENT#LOGIN"

DateTime Format Strings

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:

FormatExample OutputUse Case
:o2024-03-15T10:30:00.000ZISO 8601, sortable
:yyyy-MM-dd2024-03-15Date only
:yyyy-MM2024-03Year-month grouping
:yyyyMMddHHmmss20240315103000Compact timestamp

Numeric Format Strings

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:

FormatExample OutputUse Case
:D3001, 010, 100Zero-padded integers
:F210.50Fixed-point decimals
:N01,000Thousands separator

Default Separator (No Format)

Without a format string, properties are joined with the default # 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;
}

// CompositeEntityKeys.Pk("tenant1", "user1") returns "tenant1#user1"

You can customize the separator:

[PartitionKey]
[Computed(nameof(TenantId), nameof(UserId), Separator = "|")]
[DynamoDbAttribute("pk")]
public string PartitionKey { get; set; } = string.Empty;

// CompositeEntityKeys.Pk("tenant1", "user1") returns "tenant1|user1"

Best Practices

Use Computed Keys for Consistency

Computed keys ensure consistent formatting across your application:

// ✅ 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 is error-prone
[PartitionKey]
[DynamoDbAttribute("pk")]
public string PartitionKey { get; set; } = string.Empty;

// Manual construction elsewhere:
user.PartitionKey = $"USER#{user.UserId}"; // Easy to make mistakes

Use Sortable DateTime Formats

For sort keys with timestamps, use formats that sort correctly as strings:

// ✅ Good - ISO 8601 sorts correctly
[SortKey]
[Computed(nameof(Timestamp), Format = "{0:yyyy-MM-ddTHH:mm:ss.fffZ}")]
[DynamoDbAttribute("sk")]
public string TimestampKey { get; set; } = string.Empty;

// ❌ Avoid - MM/dd/yyyy doesn't sort chronologically
[SortKey]
[Computed(nameof(Timestamp), Format = "{0:MM/dd/yyyy}")]
[DynamoDbAttribute("sk")]
public string TimestampKey { get; set; } = string.Empty;

Keep Key Formats Simple

Simple key formats are easier to understand and debug:

// ✅ 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")]

Next Steps

Now that you understand entities, learn how they generate Tables with type-safe operations.