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

Automatic Prefix Application on Put​

Recommended

In v1.1.0+, Put operations automatically apply key prefixes during serialization. You can assign raw values to key properties and the prefix is applied for you.

// v1.1.0 (Recommended) — Auto mode applies prefixes automatically on Put
var user = new User
{
UserId = "user123", // Automatically becomes "USER#user123" in DynamoDB
ProfileType = "MAIN" // Automatically becomes "PROFILE#MAIN" in DynamoDB
};
await table.Users.Put(user).PutAsync();
// Pre-v1.1.0 — Manual prefix construction required before Put
var user = new User
{
UserId = User.Keys.Pk("user123"), // "USER#user123"
ProfileType = User.Keys.Sk("MAIN") // "PROFILE#MAIN"
};
await table.Users.Put(user).PutAsync();

Auto mode detects whether the prefix is already present and only applies it if missing, so existing code using Entity.Keys.Pk(value) continues to work unchanged.

Generated Keys Class​

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"

KeyInputMode​

KeyInputMode controls how key prefixes are applied when passing values to CRUD operations. You can configure a global default or override per-operation.

Modes​

ModeBehavior
Auto (default)Detects if the prefix is already present. Applies the prefix if missing; passes through if already prefixed.
ValueAlways prepends the configured prefix, regardless of the current value.
RawNever prepends a prefix. You must provide the full key value yourself.

Global Configuration​

Set the default mode for all operations on a table:

var options = new FluentDynamoDbOptions()
.UseKeyInputMode(KeyInputMode.Auto); // Auto is the default

var table = new MyTable(dynamoDbClient, "TableName", options);

Per-Operation Override​

Override the global mode for a specific operation:

// Force Value mode — always prepends the prefix
var order = await table.Orders.Get("12345")
.WithKeyMode(KeyInputMode.Value)
.GetItemAsync();

// Force Raw mode — pass the full key value yourself
var order = await table.Orders.Get("ORDER#12345")
.WithKeyMode(KeyInputMode.Raw)
.GetItemAsync();

Mode Examples​

// With entity: [PartitionKey(Prefix = "ORDER")]

// Auto mode (default) — detects prefix, applies if missing
await table.Orders.GetAsync("12345"); // Looks for "ORDER#12345"
await table.Orders.GetAsync("ORDER#12345"); // Looks for "ORDER#12345" (no double-prefix)

// Value mode — always prepends
await table.Orders.Get("12345")
.WithKeyMode(KeyInputMode.Value)
.GetItemAsync(); // Looks for "ORDER#12345"

// Raw mode — no prefix applied
await table.Orders.Get("ORDER#12345")
.WithKeyMode(KeyInputMode.Raw)
.GetItemAsync(); // Looks for "ORDER#12345" (passed as-is)
info

The Auto mode is designed to be backwards-compatible. Existing code that already uses Entity.Keys.Pk(value) to build prefixed keys continues to work without changes — Auto detects the prefix is already present and passes the value through.

Non-String Key Types​

Entities can use non-string types as partition keys and sort keys when they have no prefix and are not computed. The source generator handles type-appropriate DynamoDB serialization automatically.

Supported non-string key types include: enum, int, long, Guid, DateTime, DateOnly, TimeOnly, and their nullable equivalents.

[DynamoDbTable("sensors")]
public partial class SensorReading
{
[PartitionKey]
[DynamoDbAttribute("sensorId")]
public Guid SensorId { get; set; }

[SortKey]
[DynamoDbAttribute("timestamp")]
public DateTime Timestamp { get; set; }

[DynamoDbAttribute("value")]
public double Value { get; set; }
}

// Generated accessors accept the typed parameters directly
var reading = await table.SensorReadings.GetAsync(sensorId, timestamp);
await table.SensorReadings.DeleteAsync(sensorId, timestamp);
[DynamoDbTable("tasks")]
public partial class TaskItem
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public int TaskId { get; set; }

[SortKey]
[DynamoDbAttribute("sk")]
public TaskStatus Status { get; set; }

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

public enum TaskStatus { Pending, InProgress, Completed }

The source generator produces correctly typed AttributeValue construction — numeric types serialize as N, while string/enum/Guid/date types serialize as S.

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
Format Specifier Fallback

If a computed format placeholder has no explicit specifier (e.g., {0} instead of {0:yyyy-MM-dd}), the source generator falls back to the Format property on the source property's [DynamoDbAttribute]. This lets you define a default format once on the property and reuse it across computed keys.

// The source property defines a default format
[DynamoDbAttribute("eventDate", Format = "yyyy-MM-dd")]
public DateOnly EventDate { get; set; }

// Computed key with no specifier → falls back to "yyyy-MM-dd"
[SortKey]
[Computed("EventDate", "Category", Format = "{0}#{1}")]
[DynamoDbAttribute("sk")]
public string Sk { get; set; } = string.Empty;
// Produces: "2024-03-15#electronics"

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

All format specifiers use CultureInfo.InvariantCulture to ensure consistent output regardless of the machine's locale settings.

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"

Typed Convenience Overloads for Computed Keys​

Recommended

Entities with [Computed] keys automatically generate typed convenience overloads for Get, Update, and Delete operations. The overload parameters match the types of the [Extracted] source properties, so you don't need to build the key string manually.

[DynamoDbTable("events")]
public partial class Event
{
[PartitionKey]
[DynamoDbAttribute("pk")]
[Computed("Year", "Month", "Day", Separator = "#")]
public string Pk { get; set; } = string.Empty;

[Extracted("Pk", 0)]
public int Year { get; set; }

[Extracted("Pk", 1)]
public int Month { get; set; }

[Extracted("Pk", 2)]
public int Day { get; set; }
}

// Generated typed overloads accept the source property types directly:
var evt = await table.Events.GetAsync(2024, 12, 25);
await table.Events.DeleteAsync(2024, 12, 25);
await table.Events.Update(2024, 12, 25)
.Set(x => new EventUpdateModel { Title = "Christmas" })
.UpdateAsync();

Typed overloads are generated for non-string source property types including enum, int, Guid, DateTime, DateOnly, and TimeOnly. When all source properties are strings, the existing string-based accessors are used instead.

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.