| title | First Entity Guide | ||||||
|---|---|---|---|---|---|---|---|
| category | getting-started | ||||||
| order | 3 | ||||||
| keywords |
|
||||||
| related |
|
Documentation > Getting Started > First Entity
This guide provides a deep dive into creating your first DynamoDB entity with Oproto.FluentDynamoDb, explaining how source generation works and what code gets generated for you.
Your entity class must be marked as partial to enable source generation:
// ✅ Correct - partial keyword allows source generator to extend the class
public partial class User
{
// ...
}
// ❌ Wrong - source generator cannot extend non-partial classes
public class User
{
// ...
}Why partial? The source generator creates additional code in a separate file that extends your class. The partial keyword allows the compiler to merge both parts into a single class.
namespace MyApp.Models;
// Can be public, internal, or private
public partial class User
{
// ...
}The generated code will match your class's accessibility level and namespace.
Map C# properties to DynamoDB attribute names using the [DynamoDbAttribute] attribute:
using Oproto.FluentDynamoDb.Attributes;
[DynamoDbTable("users")]
public partial class User
{
// Maps to DynamoDB attribute "pk"
[DynamoDbAttribute("pk")]
public string UserId { get; set; } = string.Empty;
// Maps to DynamoDB attribute "email"
[DynamoDbAttribute("email")]
public string Email { get; set; } = string.Empty;
// Maps to DynamoDB attribute "full_name"
[DynamoDbAttribute("full_name")]
public string FullName { get; set; } = string.Empty;
}Recommended Patterns:
// Pattern 1: Short, generic names (recommended for single-table design)
[DynamoDbAttribute("pk")] // Partition key
[DynamoDbAttribute("sk")] // Sort key
[DynamoDbAttribute("gsi1pk")] // GSI partition key
// Pattern 2: Descriptive names (recommended for dedicated tables)
[DynamoDbAttribute("userId")]
[DynamoDbAttribute("email")]
[DynamoDbAttribute("createdAt")]
// Pattern 3: Snake case (common in some organizations)
[DynamoDbAttribute("user_id")]
[DynamoDbAttribute("created_at")]Best Practice: Choose a naming convention and stick with it across your project.
The source generator supports standard .NET types:
[DynamoDbTable("examples")]
public partial class TypeExamples
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string Id { get; set; } = string.Empty;
// Strings
[DynamoDbAttribute("name")]
public string Name { get; set; } = string.Empty;
// Numbers
[DynamoDbAttribute("age")]
public int Age { get; set; }
[DynamoDbAttribute("price")]
public decimal Price { get; set; }
[DynamoDbAttribute("score")]
public double Score { get; set; }
// Booleans
[DynamoDbAttribute("isActive")]
public bool IsActive { get; set; }
// DateTime
[DynamoDbAttribute("createdAt")]
public DateTime CreatedAt { get; set; }
// Nullable types
[DynamoDbAttribute("updatedAt")]
public DateTime? UpdatedAt { get; set; }
// Collections
[DynamoDbAttribute("tags")]
public List<string> Tags { get; set; } = new();
[DynamoDbAttribute("metadata")]
public Dictionary<string, string> Metadata { get; set; } = new();
}Every entity must have exactly one partition key:
[DynamoDbTable("users")]
public partial class User
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string UserId { get; set; } = string.Empty;
}The [PartitionKey] attribute tells the source generator which property is the partition key.
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;
[DynamoDbAttribute("total")]
public decimal Total { get; set; }
}Use Cases for Sort Keys:
- One-to-many relationships (customer → orders)
- Time-series data (sensor → readings by timestamp)
- Hierarchical data (folder → files)
- Multi-item entities (order → order items)
When you build your project, the source generator creates nested support classes within your entity:
// Generated: User.g.cs (nested within User class)
public partial class User
{
public static partial class Fields
{
public const string UserId = "pk";
public const string Email = "email";
public const string FullName = "full_name";
}
}Usage:
// Access through entity class
await table.Query<User>()
.Where($"{User.Fields.UserId} = {{0}}", "user123")
.WithFilter($"{User.Fields.Email} = {{0}}", "john@example.com")
.ToListAsync();Benefits:
- Compile-time safety (typos caught at build time)
- IntelliSense support
- Refactoring support (rename property → field name updates automatically)
- No namespace pollution - Fields class is nested within entity
Tip: You can also use string literals directly (e.g.,
"pk"instead ofUser.Fields.UserId). The generated constants provide compile-time validation but aren't required. Use whichever approach is cleaner for your use case.
// Generated: User.g.cs (nested within User class)
public partial class User
{
public static partial class Keys
{
public static string Pk(string userId)
{
return $"USER#{userId}";
}
}
}Usage:
// Build partition key value through entity class
var key = User.Keys.Pk("user123"); // Returns "USER#user123"
// Use in operations
await table.Get<User>()
.WithKey(User.Fields.UserId, User.Keys.Pk("user123"))
.GetItemAsync();Benefits:
- Consistent key formatting
- Prevents key format errors
- Supports composite key patterns
- Clear relationship between entity and its keys
Note: The
Keys.Pk()method formats keys based on your entity's[Computed]or[PartitionKey(Prefix = "...")]attributes. If no prefix is configured, it returns the value as-is. You can also pass raw values directly toWithKey()if you don't need key formatting.
// Generated: UserMapper.g.cs
public static class UserMapper
{
public static Dictionary<string, AttributeValue> ToAttributeMap(User entity)
{
// Converts User object to DynamoDB attribute map
}
public static User FromAttributeMap(Dictionary<string, AttributeValue> attributes)
{
// Converts DynamoDB attribute map to User object
}
}Usage:
// Typically used internally by the library
// You rarely need to call these directly
var attributeMap = UserMapper.ToAttributeMap(user);
var user = UserMapper.FromAttributeMap(attributeMap);Benefits:
- Zero-boilerplate serialization
- Type-safe conversions
- Optimized performance
[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;
}
// Usage
await table.Get<User>()
.WithKey(User.Fields.UserId, "user123")
.GetItemAsync();[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;
[DynamoDbAttribute("total")]
public decimal Total { get; set; }
[DynamoDbAttribute("status")]
public string Status { get; set; } = "pending";
}
// Usage - requires both partition and sort key
await table.Get<Order>()
.WithKey(Order.Fields.CustomerId, "customer123", Order.Fields.OrderId, "order456")
.GetItemAsync();Use the Prefix property on [PartitionKey] and [SortKey] for simple key formatting:
[DynamoDbTable("orders")]
public partial class Order
{
// Generates keys like "ORDER#order123"
[PartitionKey(Prefix = "ORDER")]
[DynamoDbAttribute("pk")]
public string OrderId { get; set; } = string.Empty;
// Generates keys like "LINE#item456"
[SortKey(Prefix = "LINE")]
[DynamoDbAttribute("sk")]
public string LineId { get; set; } = string.Empty;
[DynamoDbAttribute("total")]
public decimal Total { get; set; }
}
// Generated key builder methods
// Order.Keys.Pk("order123") returns "ORDER#order123"
// Order.Keys.Sk("item456") returns "LINE#item456"Key Prefix Properties:
| Property | Default | Description |
|---|---|---|
Prefix |
null |
Optional prefix prepended to key values |
Separator |
"#" |
Separator between prefix and value |
Custom Separator Example:
// Generates keys like "USER_user123"
[PartitionKey(Prefix = "USER", Separator = "_")]
[DynamoDbAttribute("pk")]
public string UserId { get; set; } = string.Empty;Use [Computed] attribute for complex keys with dynamic formatting:
[DynamoDbTable("products")]
public partial class Product
{
[PartitionKey]
[Computed("PRODUCT#{ProductId}")]
[DynamoDbAttribute("pk")]
public string ProductId { get; set; } = string.Empty;
[SortKey]
[Computed("v{Version:D3}")] // Formats as v001, v002, etc.
[DynamoDbAttribute("sk")]
public int Version { get; set; }
[DynamoDbAttribute("name")]
public string Name { get; set; } = string.Empty;
}
// Generated key builder
// ProductKeys.Pk("abc123") returns "PRODUCT#abc123"
// ProductKeys.Sk(5) returns "v005"Format Specifiers:
{PropertyName}- Simple substitution{PropertyName:D3}- Numeric formatting (3 digits, zero-padded){PropertyName:o}- DateTime ISO 8601 format- See Expression Formatting for more options
Tip: For simple prefix patterns like
"ORDER#123", use[PartitionKey(Prefix = "ORDER")]instead of[Computed]. Reserve[Computed]for complex multi-property keys or custom formatting.
Use [Extracted] for keys derived from other properties:
[DynamoDbTable("events")]
public partial class Event
{
[PartitionKey]
[Extracted("Date", "yyyy-MM-dd")]
[DynamoDbAttribute("pk")]
public string DateKey { get; set; } = string.Empty;
[DynamoDbAttribute("date")]
public DateTime Date { get; set; }
[DynamoDbAttribute("eventName")]
public string EventName { get; set; } = string.Empty;
}
// The DateKey is automatically populated from Date property
// Date = 2024-03-15 → DateKey = "2024-03-15"- Right-click on your project in Solution Explorer
- Select Analyze and Code Cleanup → View Generated Files
- Expand Oproto.FluentDynamoDb.SourceGenerator
- View the generated
.g.csfiles
- In Solution Explorer, expand Dependencies
- Expand Analyzers
- Expand Oproto.FluentDynamoDb.SourceGenerator
- View the generated files
Generated files are in the obj/ directory:
# Find generated files
find obj -name "*.g.cs"Here's a complete entity with all common features:
using Oproto.FluentDynamoDb.Attributes;
[DynamoDbTable("users")]
public partial class User
{
// Partition key with computed format
[PartitionKey]
[Computed("USER#{UserId}")]
[DynamoDbAttribute("pk")]
public string UserId { get; set; } = string.Empty;
// Sort key (optional)
[SortKey]
[Computed("PROFILE")]
[DynamoDbAttribute("sk")]
public string RecordType { get; set; } = "PROFILE";
// Standard attributes
[DynamoDbAttribute("email")]
public string Email { get; set; } = string.Empty;
[DynamoDbAttribute("name")]
public string Name { get; set; } = string.Empty;
[DynamoDbAttribute("status")]
public string Status { get; set; } = "active";
// Timestamps
[DynamoDbAttribute("createdAt")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[DynamoDbAttribute("updatedAt")]
public DateTime? UpdatedAt { get; set; }
// Collections
[DynamoDbAttribute("roles")]
public List<string> Roles { get; set; } = new();
[DynamoDbAttribute("preferences")]
public Dictionary<string, string> Preferences { get; set; } = new();
}Generated Code Usage:
using Amazon.DynamoDBv2;
using Oproto.FluentDynamoDb.Storage;
var client = new AmazonDynamoDBClient();
var table = new UsersTable(client, "users");
// Create user
var user = new User
{
UserId = "user123",
Email = "john@example.com",
Name = "John Doe",
Roles = new List<string> { "admin", "user" },
Preferences = new Dictionary<string, string>
{
["theme"] = "dark",
["language"] = "en"
}
};
await table.Put<User>().WithItem(user).PutAsync();
// Retrieve user using generated fields
var response = await table.Get<User>()
.WithKey(User.Fields.UserId, "user123", User.Fields.RecordType, "PROFILE")
.GetItemAsync();
// Query with filter using generated fields
var activeUsers = await table.Query<User>()
.Where($"{User.Fields.UserId} = {{0}}", "user123")
.WithFilter($"{User.Fields.Status} = {{0}}", "active")
.ToListAsync();
// Update using generated fields
await table.Update<User>()
.WithKey(User.Fields.UserId, "user123", User.Fields.RecordType, "PROFILE")
.Set($"SET {User.Fields.Name} = {{0}}, {User.Fields.UpdatedAt} = {{1:o}}",
"Jane Doe",
DateTime.UtcNow)
.UpdateAsync();// ✅ Good - clear property names
public string Email { get; set; }
public DateTime CreatedAt { get; set; }
// ❌ Avoid - unclear abbreviations
public string Em { get; set; }
public DateTime Dt { get; set; }// ✅ Good - prevents null reference exceptions
public List<string> Tags { get; set; } = new();
// ❌ Risky - can be null
public List<string> Tags { get; set; }// ✅ Good - optional timestamp
public DateTime? UpdatedAt { get; set; }
// ✅ Good - required timestamp with default
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;// ✅ Good - consistent prefix pattern
[Computed("USER#{UserId}")]
[Computed("ORDER#{OrderId}")]
[Computed("PRODUCT#{ProductId}")]
// ❌ Inconsistent - hard to maintain
[Computed("USER#{UserId}")]
[Computed("{OrderId}-ORDER")]
[Computed("prod_{ProductId}")]- Entity Definition - Advanced entity patterns (GSIs, relationships)
- Basic Operations - CRUD operations with your entities
- Expression Formatting - Advanced format specifiers
- Attribute Reference - Complete attribute documentation
Symptoms: UserFields, UserKeys, or UserMapper not found
Solutions:
- Ensure class is marked as
partial - Rebuild project:
dotnet clean && dotnet build - Restart IDE
- Check that
[PartitionKey]attribute is present
Error: "Partial declarations must not specify different base classes"
Solution: Ensure all partial declarations of the same class don't specify base classes, or they all specify the same base class.
Error: "The type or namespace name 'DynamoDbTable' could not be found"
Solution: Add using statement:
using Oproto.FluentDynamoDb.Attributes;See Troubleshooting Guide for more help.
Previous: Installation | Next: Entity Definition
See Also: