Repository Pattern Guide
The repository pattern provides typed entity storage with schema validation, automatic timestamps, and versioning.
Overview
Section titled “Overview”Repositories offer:
- Type Safety: Work with strongly-typed entities
- Schema Validation: Ensure data consistency
- Auto-Generated IDs: Unique identifiers for each entity
- Automatic Timestamps:
createdAtandupdatedAtfields - Versioning: Built-in optimistic concurrency
When to Use Repositories
Section titled “When to Use Repositories”| Use Case | Repository | Raw Items |
|---|---|---|
| Domain entities with defined structure | Yes | - |
| Schema validation required | Yes | - |
| Dynamic/flexible data | - | Yes |
| Single-table design patterns | - | Yes |
| Type-safe SDKs | Yes | - |
Defining Entities
Section titled “Defining Entities”Step 1: Create Entity Class
Section titled “Step 1: Create Entity Class”Extend EntityBase for your entities:
using TerraScale.Database.Client.Abstractions;
public record Customer : EntityBase{ public required string Name { get; init; } public required string Email { get; init; } public string? Phone { get; init; } public CustomerTier Tier { get; init; } = CustomerTier.Standard; public List<string> Tags { get; init; } = new();}
public enum CustomerTier{ Standard, Premium, Enterprise}Step 2: Entity Base Properties
Section titled “Step 2: Entity Base Properties”EntityBase provides these properties automatically:
public abstract record EntityBase : IEntity{ public required string Id { get; init; } public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow; public DateTimeOffset? UpdatedAt { get; init; } public long Version { get; init; } = 1;
public virtual string GetPartitionKey() => Id; public virtual string? GetSortKey() => null;}Step 3: Custom Keys (Optional)
Section titled “Step 3: Custom Keys (Optional)”Override key methods for custom partitioning:
public record Order : EntityBase{ public required string CustomerId { get; init; } public required string OrderNumber { get; init; } public decimal Total { get; init; }
// Use customer as partition key public override string GetPartitionKey() => $"customer#{CustomerId}";
// Use order number as sort key public override string? GetSortKey() => $"order#{OrderNumber}";}Repository Operations
Section titled “Repository Operations”Get Repository Reference
Section titled “Get Repository Reference”var customers = client.GetRepository<Customer>("repo_customers");Create Entity
Section titled “Create Entity”var customer = new Customer{ Id = Guid.NewGuid().ToString(), Name = "Acme Corp", Email = "contact@acme.com", Tier = CustomerTier.Premium};
var result = await customers.CreateAsync(customer);
if (result.IsSuccess){ Console.WriteLine($"Created customer with ID: {result.Value.Id}");}Get Entity
Section titled “Get Entity”var result = await customers.GetAsync(customerId);
if (result.IsSuccess){ var customer = result.Value; Console.WriteLine($"Customer: {customer.Name}");}Update Entity
Section titled “Update Entity”// Get existing entityvar getResult = await customers.GetAsync(customerId);var customer = getResult.Value;
// Create updated versionvar updated = customer with{ Tier = CustomerTier.Enterprise, Tags = customer.Tags.Append("upgraded").ToList()};
// Save changesvar updateResult = await customers.UpdateAsync(updated);Delete Entity
Section titled “Delete Entity”var result = await customers.DeleteAsync(customerId);
if (result.IsSuccess){ Console.WriteLine("Customer deleted");}List Entities
Section titled “List Entities”var result = await customers.ListAsync(new PaginationOptions{ Limit = 50});
if (result.IsSuccess){ foreach (var customer in result.Value.Items) { Console.WriteLine($"{customer.Name} ({customer.Tier})"); }
// Handle pagination if (result.Value.HasMore) { var nextPage = await customers.ListAsync(new PaginationOptions { Limit = 50, NextToken = result.Value.NextToken }); }}Query Entities
Section titled “Query Entities”var filter = new QueryFilter{ PartitionKey = $"customer#{customerId}", SortKeyCondition = SortKeyCondition.BeginsWith("order#")};
var result = await customers.QueryAsync(filter);Check Existence
Section titled “Check Existence”var result = await customers.ExistsAsync(customerId);
if (result.IsSuccess && result.Value){ Console.WriteLine("Customer exists");}Complete Example
Section titled “Complete Example”using TerraScale.Database.Client;using TerraScale.Database.Client.Abstractions;using TerraScale.Database.Client.Configuration;
// Define entitypublic record Product : EntityBase{ public required string Name { get; init; } public required string Category { get; init; } public decimal Price { get; init; } public bool InStock { get; init; } = true; public List<string> Tags { get; init; } = new();}
// Create clientvar client = new TerraScaleDatabase(new TerraScaleDatabaseOptions{ ApiKey = "ts_live_your_api_key", Endpoint = "https://api.terrascale.io", DefaultDatabase = "my-database"});
// Get repositoryvar products = client.GetRepository<Product>("repo_products");
// Create productvar product = new Product{ Id = "prod_001", Name = "Wireless Mouse", Category = "Electronics", Price = 29.99m, Tags = new() { "computer", "accessories" }};
await products.CreateAsync(product);
// Update pricevar updated = product with { Price = 24.99m };await products.UpdateAsync(updated);
// List all productsvar allProducts = await products.ListAsync(new PaginationOptions { Limit = 100 });
foreach (var p in allProducts.Value.Items){ Console.WriteLine($"{p.Name}: ${p.Price}");}
// Clean upawait client.DisposeAsync();Schema Validation
Section titled “Schema Validation”When creating a repository via the Management API, define a schema:
{ "name": "products", "entityName": "Product", "schema": { "name": { "type": "string", "required": true, "minLength": 1 }, "category": { "type": "string", "required": true }, "price": { "type": "number", "required": true, "min": 0 }, "inStock": { "type": "boolean", "required": false }, "tags": { "type": "array", "required": false, "items": { "type": "string" } } }}Entities that don’t match the schema will be rejected.
Best Practices
Section titled “Best Practices”Use Immutable Records
Section titled “Use Immutable Records”C# records with init properties ensure immutability:
public record Customer : EntityBase{ public required string Name { get; init; } // Immutable after creation}
// Update by creating new instancevar updated = customer with { Name = "New Name" };Handle Optimistic Concurrency
Section titled “Handle Optimistic Concurrency”Use the Version property to detect conflicts:
try{ await products.UpdateAsync(product);}catch (ConcurrencyException){ // Another process updated the entity var fresh = await products.GetAsync(product.Id); // Merge changes and retry}Design for Access Patterns
Section titled “Design for Access Patterns”Override key methods to optimize queries:
// If you often query by customerpublic override string GetPartitionKey() => $"customer#{CustomerId}";Next Steps
Section titled “Next Steps”- Repository Operations API - API reference
- Repository Management - Create repositories
- C# SDK - Full SDK documentation