Entity Mapping
The LINQ package automatically maps database query results to your strongly-typed C# classes.
Defining Entities
Basic Entity
public class User
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}
With Nullable Properties
public class User
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Email { get; set; } // Nullable
public string? PhoneNumber { get; set; } // Nullable
public DateTime? LastLoginAt { get; set; } // Nullable DateTime
}
Complex Types
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public ProductStatus Status { get; set; } // Enum
public bool IsActive { get; set; } // Boolean
public DateTime CreatedAt { get; set; } // DateTime
public Guid TrackingId { get; set; } // Guid
public int? CategoryId { get; set; } // Nullable int
}
public enum ProductStatus
{
Draft = 0,
Published = 1,
Archived = 2
}
Column Name Mapping
Snake_case to PascalCase
The default mapper automatically converts snake_case database columns to PascalCase properties:
| Database Column | C# Property |
|---|---|
user_id | UserId |
email_address | EmailAddress |
created_at | CreatedAt |
is_active | IsActive |
first_name | FirstName |
// Database: user_id, full_name, email_address, created_at
public class User
{
public int UserId { get; set; }
public string FullName { get; set; } = string.Empty;
public string EmailAddress { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
}
Exact Matching
If property names match column names exactly, mapping works automatically:
// Database: id, name, email
public class User
{
public int id { get; set; }
public string name { get; set; } = string.Empty;
public string email { get; set; } = string.Empty;
}
Type Conversions
Supported Types
The default mapper handles automatic conversion for:
Primitives
int,long,short,bytedecimal,float,doubleboolstring
Special Types
DateTime- Parsed from strings or Unix timestampsGuid- Parsed from string representationsEnum- Mapped from integers or strings
Nullable Types
All of the above with ? suffix:
int?,decimal?,DateTime?, etc.
SQLite Boolean Handling
SQLite stores booleans as 0 or 1. The mapper automatically converts:
// SQLite: 0 → false, 1 → true
public class User
{
public bool IsActive { get; set; }
public bool IsVerified { get; set; }
public bool? IsSubscribed { get; set; } // Nullable
}
DateTime Handling
DateTime values are parsed from multiple formats:
public class Event
{
// Handles: ISO 8601, RFC 3339, Unix timestamps
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
}
Enum Handling
Enums can be stored as integers or strings:
public enum OrderStatus
{
Pending = 0,
Confirmed = 1,
Shipped = 2,
Delivered = 3
}
public class Order
{
public OrderStatus Status { get; set; } // Maps from int or string
}
Custom Mappers
Implementing IEntityMapper
Create custom mapping logic by implementing IEntityMapper:
using CloudflareD1.NET.Linq.Mapping;
public class CustomUserMapper : IEntityMapper
{
public T Map<T>(Dictionary<string, object?> row)
{
if (typeof(T) == typeof(User))
{
var user = new User
{
// Custom column names
Id = Convert.ToInt32(row["user_id"]),
FullName = row["full_name"]?.ToString() ?? "",
Email = row["email_address"]?.ToString() ?? "",
// Custom logic
IsActive = row["status"]?.ToString() == "active",
// Transform data
DisplayName = row["full_name"]?.ToString()?.ToUpper() ?? ""
};
return (T)(object)user;
}
throw new NotSupportedException($"Type {typeof(T)} not supported");
}
public IEnumerable<T> MapMany<T>(IEnumerable<Dictionary<string, object?>> rows)
{
return rows.Select(Map<T>);
}
}
Using Custom Mappers
With Direct Queries
var mapper = new CustomUserMapper();
var users = await client.QueryAsync<User>(
"SELECT * FROM users",
parameters: null,
mapper: mapper
);
With Query Builder
var mapper = new CustomUserMapper();
var users = await client.Query<User>("users", mapper)
.Where("is_active = ?", true)
.ToListAsync();
Real-World Examples
E-Commerce Product
public class Product
{
// Database: product_id
public int ProductId { get; set; }
// Database: product_name
public string ProductName { get; set; } = string.Empty;
// Database: sku
public string Sku { get; set; } = string.Empty;
// Database: price (decimal)
public decimal Price { get; set; }
// Database: quantity_in_stock
public int QuantityInStock { get; set; }
// Database: is_available (0 or 1)
public bool IsAvailable { get; set; }
// Database: category_id (nullable)
public int? CategoryId { get; set; }
// Database: created_at (ISO string)
public DateTime CreatedAt { get; set; }
// Database: updated_at (ISO string, nullable)
public DateTime? UpdatedAt { get; set; }
}
// Query
var products = await client.QueryAsync<Product>(@"
SELECT product_id, product_name, sku, price,
quantity_in_stock, is_available, category_id,
created_at, updated_at
FROM products
WHERE is_available = 1
");
User with Profile
public class UserProfile
{
public int UserId { get; set; }
public string Username { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string? ProfilePictureUrl { get; set; }
public string? Bio { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? LastLoginAt { get; set; }
public bool IsEmailVerified { get; set; }
public bool IsPremium { get; set; }
public UserRole Role { get; set; }
}
public enum UserRole
{
User = 0,
Moderator = 1,
Admin = 2
}
var profile = await client.QueryFirstOrDefaultAsync<UserProfile>(
"SELECT * FROM users WHERE user_id = @id",
new { id = 123 }
);
Audit Log Entry
public class AuditLog
{
public long Id { get; set; }
public int UserId { get; set; }
public string Action { get; set; } = string.Empty;
public string? Details { get; set; }
public string IpAddress { get; set; } = string.Empty;
public DateTime Timestamp { get; set; }
public Guid TraceId { get; set; }
}
var recentLogs = await client.Query<AuditLog>("audit_logs")
.Where("timestamp > ?", DateTime.UtcNow.AddHours(-24))
.OrderByDescending("timestamp")
.Take(100)
.ToListAsync();
Performance
Reflection Caching
The default mapper uses ConcurrentDictionary to cache:
- Property information for each type
- Column-to-property mappings
This means:
- First query for a type: ~10-20ms overhead
- Subsequent queries: Less than 1ms overhead
- Mapping 1000 rows: Less than 1ms total
Best Practices
- Reuse mapper instances: Create once, use many times
- Keep entities simple: Avoid complex computed properties
- Use nullable types appropriately: Match database schema
- Name properties clearly: Follow PascalCase conventions
Troubleshooting
Property Not Mapped
Problem: Database column not mapping to property
Solutions:
- Check property name matches column (with snake_case conversion)
- Ensure property has a public setter
- Verify column name in database
- Use custom mapper for non-standard mappings
Type Conversion Error
Problem: Cannot convert database value to property type
Solutions:
- Check database column type matches C# property type
- Use nullable types for nullable columns
- Implement custom mapper with explicit conversions
Missing Properties
Problem: Some properties are null after mapping
Solutions:
- Verify column is in SELECT query results
- Check for typos in column or property names
- Ensure database has data in those columns
What's Next?
- Query Builder - Build fluent queries