Mapping
MicroORM maps a class to a table and its properties to columns. For each setting, the first of these that applies wins:
- Fluent configuration in
MicroOptions.Entity<T>(...) - Data annotation attributes from
System.ComponentModel.DataAnnotations, the same ones EF Core uses - Conventions
Conventions
| What | Convention |
|---|---|
| Table name | The class name, run through MicroOptions.Naming |
| Column name | The property name, run through MicroOptions.Naming |
| Mapped properties | Public, readable, non-indexer properties of scalar types: numbers, string, bool, char, Guid, DateTime, DateTimeOffset, DateOnly, TimeOnly, TimeSpan, byte[], enums, and their nullable forms. Collections and other classes are skipped, so navigation properties are ignored. |
| Key | A property named Id, or {ClassName}Id (case-insensitive) |
| Generated key | int, long and short keys are database-generated (identity, serial, auto-increment). Guid and string keys are supplied by the application. |
Naming conventions
| Convention | UserId becomes |
|---|---|
NamingConvention.AsIs (default) | UserId |
NamingConvention.SnakeCase | user_id |
NamingConvention.LowerCase | userid |
var db = new MicroDb(SqlDialect.PostgreSql, dataSource, new MicroOptions { Naming = NamingConvention.SnakeCase });PostgreSQL tip: MicroORM quotes identifiers, and PostgreSQL treats quoted names as case-sensitive. Tables created without quotes have lowercase names, so use
SnakeCaseorLowerCasewith PostgreSQL.
For other schemes, subclass NamingConvention and override Convert, or TableName and
ColumnName for finer control (for example, to pluralize table names).
Attributes
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
[Table("invoices", Schema = "billing")] // billing.invoices
public class Invoice
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)] // the application assigns the number
public long Number { get; set; }
[Column("customer_ref")]
public string Customer { get; set; } = "";
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public decimal Total { get; set; } // read, never written
[NotMapped]
public string Label => $"INV-{Number}";
}| Attribute | Effect |
|---|---|
[Table(name, Schema = ...)] | Table name and optional schema |
[Column(name)] | Column name |
[Key] | Primary key. Several [Key] properties make a composite key, ordered by [Column(Order = n)] |
[ConcurrencyCheck] | An int or long optimistic concurrency version (see Concurrency) |
[Timestamp] | A SQL Server rowversion: read, never written |
[DatabaseGenerated(Identity)] | Generated on insert; read back after InsertAsync |
[DatabaseGenerated(Computed)] | Never written by insert or update |
[DatabaseGenerated(None)] | Always written, even for an integer key |
[NotMapped] | Not a column |
Fluent configuration
Use fluent configuration when you can't or don't want to put attributes on the class:
var options = new MicroOptions()
.Entity<Invoice>(e => e
.ToTable("invoices", schema: "billing")
.HasKey(i => i.Number, ValueGeneration.None) // composite: .HasKey(i => new { i.Year, i.Number })
.IsVersion(i => i.Revision)
.Column(i => i.Customer, "customer_ref")
.Generated(i => i.Total, ValueGeneration.Computed)
.Ignore(i => i.Label));Configure entities before creating the MicroDb. Mappings are built on first use and cached for
the lifetime of the MicroDb.
Query filters
A query filter is a condition that every typed query of an entity applies, such as a soft delete or a tenant:
var options = new MicroOptions()
.Entity<Post>(e => e
.HasQueryFilter(p => !p.IsDeleted)
.HasQueryFilter(p => p.TenantId == tenants.CurrentId));
var posts = await db.Table<Post>().Where(p => p.Published).ToListAsync();
// WHERE (NOT (("is_deleted" = @p0))) AND ("tenant_id" = @p1) AND ("published" = @p2)
var trash = await db.Table<Post>().IgnoreQueryFilters().Where(p => p.IsDeleted).ToListAsync();- Filters apply to everything built from
Table<T>(): reads, aggregates,FindAsync,ExecuteUpdateAsync,ExecuteDeleteAsync, and subqueries over the entity. - They don't apply to raw SQL (
QueryAsync($"...")), or to the single-entity writesInsertAsync,UpdateAsync,DeleteAsync,DeleteByKeyAsyncandUpsertAsync, which act on the key they're given. Check ownership before you call those with a key from user input. - Captured values, such as
tenants.CurrentId, are read each time a statement is built, so a long-livedTable<T>always uses the current tenant. - Several filters are joined with
AND.IgnoreQueryFilters()skips all of them, but keeps the query's ownWherefilters.
Inspecting a mapping
EntityMap map = db.MapOf<Invoice>();
Console.WriteLine(map.Table); // billing.invoices
foreach (var c in map.Columns) Console.WriteLine($"{c.Property.Name} -> {c.Name} ({c.Generation})");Composite keys
Mark each key property with [Key] and order them with [Column(Order = n)], or configure them
fluently with HasKey(x => new { x.OrderId, x.LineNo }). Composite keys are never
database-generated by convention.
[Table("order_lines")]
public class OrderLine
{
[Key, Column(Order = 0)] public int OrderId { get; set; }
[Key, Column(Order = 1)] public int LineNo { get; set; }
public string Product { get; set; } = "";
}
var line = await db.Table<OrderLine>().FindAsync((orderId, 2)); // a tuple, in key order
await db.Table<OrderLine>().DeleteByKeyAsync((orderId, 2));UpdateAsync, DeleteAsync and UpsertAsync match on all key columns.
Custom types
Value objects, strongly typed IDs, JSON documents and enums stored as text are mapped through type handlers.
Limitations
- No relationships. MicroORM doesn't load navigation properties. Use a
JOINin raw SQL and materialize into a record shaped like the result.