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.