Attributes
Overview
Jaunty provides several attributes for customizing entity mapping behavior. These attributes offer fine-grained control over how entities and their properties are mapped to database tables and columns.
Table Attribute
[Table(string name, string? schema = null)]
Maps an entity class to a specific database table and schema.
Usage:
[Table("products", "inventory")]
public partial class Product
{
public int ProductId { get; set; }
public string ProductName { get; set; } = string.Empty;
}
Constructor Parameters:
name: The name of the database tableschema: Optional schema name (defaults to null)
Properties:
Name: Gets the table nameSchema: Gets the optional schema name
Notes:
- Takes precedence over
JauntyConfig.TableNameResolver - If schema is not specified, the table name is emitted unqualified and the database resolves it:
the login's default schema on SQL Server,
search_pathon PostgreSQL,mainon SQLite, the connection's database on MySQL. Jaunty never substitutes a default of its own — seeschemas.md - Attribute takes highest priority in naming resolution
schemais positional, not a named argument.[Table("products", Schema = "inventory")]does not compile:Schemais a read-only property, so the compiler reports CS0617.
Schemas on SQLite. SQLite has schemas too, and Jaunty qualifies with them the same way it does
on SQL Server, PostgreSQL and MySQL. They are main, temp, and the name given to each
ATTACH DATABASE ... AS name:
[Table("widgets", "archive")]
public partial class ArchivedWidget { /* ... */ }
SELECT id, name FROM archive.widgets
The alias is whatever you choose: archive, master, dbo. The connection has to have attached
that database first (ATTACH DATABASE 'archive.db' AS archive), because the schema set is a
property of the connection, not of the file. Ask for a schema the connection has not attached and
SQLite says so, for example SQLite Error 1: 'no such table: archive.widgets' from
Microsoft.Data.Sqlite. Full detail in schemas.md.
Before 2026-09-02 the SQLite dialect discarded the schema instead, on the belief that SQLite had no schemas. Nothing failed when it did: the statement went out unqualified, SQLite resolved it against
main, and reads returned another table's rows while writes landed in another file. If you pinned an earlier version and mapped an entity to a schema, check which database it has been writing to.
Column Attribute
[Column(string name)]
Maps a property to a specific database column name.
Usage:
public partial class Product
{
[Column("product_id")]
public int ProductId { get; set; }
[Column("product_name")]
public string ProductName { get; set; } = string.Empty;
}
Constructor Parameters:
name: The name of the database column
Properties:
Name: Gets the column name
Notes:
- Takes precedence over
JauntyConfig.ColumnNameResolver - Attribute takes highest priority in naming resolution
- Only applies to properties of entity classes
Ignore Attribute
[Ignore]
Excludes a property from database mapping operations.
Usage:
public partial class Product
{
public int ProductId { get; set; }
public string ProductName { get; set; } = string.Empty;
[Ignore]
public string? ComputedProperty { get; set; } // Won't be mapped from database
}
Notes:
- Properties marked with
[Ignore]are not included in mapping operations - Useful for computed properties or properties that don't exist in the database
- Takes precedence over all other mapping configurations
Key Attribute
[Key]
Marks a property as a primary key for update and delete operations.
Usage:
public partial class Product
{
[Key]
public int ProductId { get; set; }
public string ProductName { get; set; } = string.Empty;
}
Notes:
- Used by update and delete operations to identify the record to modify
- Multiple properties can be marked as
[Key]for composite primary keys - Required for proper functioning of Update and Delete operations
- If no property is marked with
[Key], Jaunty will try to infer the primary key based on naming conventions
DatabaseGenerated Attribute
[DatabaseGenerated(DatabaseGeneratedOption option)]
Specifies how a property's value is generated by the database.
Usage:
public partial class Product
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ProductId { get; set; }
public string ProductName { get; set; } = string.Empty;
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public DateTime DateModified { get; set; }
}
Constructor Parameters:
option: The database generation option
DatabaseGeneratedOption Enum Values:
None: The database does not generate valuesIdentity: The database provides an identity value (auto-increment)Computed: The database computes the value (e.g., timestamp, computed column)
Notes:
- Used primarily for insert operations to handle identity columns
- Identity values are returned by Insert operations and can be automatically populated
- Computed columns are typically read-only in insert/update operations
Write it explicitly — the two mapping paths infer differently without it.
If a key property is an int or long and carries no [DatabaseGenerated], the two mapping
paths disagree about whether the database generates its value, and therefore about whether the
column appears in the generated INSERT:
| Mapping path | Single int/long key, no [DatabaseGenerated] |
|---|---|
Jaunty.SourceGenerator |
Treated as an identity column — omitted from the INSERT |
Jaunty.Extensions.Reflection |
Not an identity column — included in the INSERT |
The source-generated mapper is preferred whenever one exists, so adding or removing the
Jaunty.SourceGenerator package reference changes the SQL for such an entity — dropping a
client-assigned key on one side, or overriding a real sequence on the other.
Adding [DatabaseGenerated(...)] removes the ambiguity: both paths then honour exactly what you
wrote. Do that for every int/long key, whichever way it should behave:
[Table("orders")]
public partial class Order
{
// Auto-increment / IDENTITY / SERIAL column: excluded from INSERT on both paths.
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
}
[Table("tenants")]
public partial class Tenant
{
// Client-assigned key: included in INSERT on both paths.
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
}
Composite keys are not affected: neither path infers identity for an entity with more than one key
column, since no database has two identity columns. An explicit [DatabaseGenerated] on one part
of a composite key is still honoured.
A property whose setter is init-only or inaccessible is mapped by reflection only.
The generated mapper assigns properties after construction (entity.Name = ...), so it cannot
write an init-only setter, nor a setter declared on a base class that the entity itself cannot
reach (private set on a base, or an internal set across an assembly boundary). The reflection
mapper writes both without difficulty — PropertyInfo.SetValue is not bound by either rule.
| Setter | Jaunty.SourceGenerator |
Jaunty.Extensions.Reflection |
|---|---|---|
set |
Mapped | Mapped |
init |
Not mapped | Mapped |
Inaccessible from the entity (e.g. base-class private set) |
Not mapped | Mapped |
| No setter at all | Not mapped | Not mapped |
The build reports JAUNTYGEN005 for each such property, naming the entity, the property and the
reason. Give the property a plain accessible setter to map it on both paths, or mark it [Ignore]
to record that the exclusion is intended and silence the warning.
EnumStorage Attribute
[EnumStorage(EnumStorage storage)]
Overrides how a single enum-typed property is stored in the database, taking precedence over the global JauntyConfig.DefaultEnumStorage setting (see Configuration).
Usage:
public partial class Order
{
[Key]
public int Id { get; set; }
// Uses the global default (JauntyConfig.DefaultEnumStorage), numeric unless changed
public OrderStatus Status { get; set; }
// Stored as the string name, overriding the global default
[EnumStorage(EnumStorage.String)]
public OrderPriority Priority { get; set; }
}
public enum OrderStatus { Pending = 0, Completed = 1 }
public enum OrderPriority { Low = 0, High = 1 }
Constructor Parameters:
storage: The enum storage strategy (EnumStorage.NumericorEnumStorage.String)
Properties:
Storage: Gets the configured enum storage strategy
Notes:
- Only applicable to enum-typed properties; has no effect otherwise
- Takes precedence over
JauntyConfig.DefaultEnumStorage - Applies to both the read path (mapping a database value into the enum) and the write path (parameter binding for insert/update)
Attribute Priority
Jaunty uses the following priority order for mapping configuration:
- Attributes (
[Table],[Column],[Ignore],[Key],[DatabaseGenerated]) - Highest priority - JauntyConfig resolvers (
SchemaNameResolver,TableNameResolver,ColumnNameResolver) - Medium priority - Default behavior (property/type names) - Lowest priority
Examples
Complete Entity with Multiple Attributes
[Table("products", "inventory")]
public partial class Product
{
[Key]
[Column("product_id")]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ProductId { get; set; }
[Column("product_name")]
public string ProductName { get; set; } = string.Empty;
[Column("category_id")]
public int CategoryId { get; set; }
[Column("unit_price")]
public decimal UnitPrice { get; set; }
[Ignore]
public string? ComputedDisplayName { get; set; }
public string? Description { get; set; } // Will map to "Description" column
}
Composite Primary Key Example
[Table("product_category_mapping")]
public partial class ProductCategoryMapping
{
[Key]
public int ProductId { get; set; }
[Key]
public int CategoryId { get; set; }
public DateTime CreatedAt { get; set; }
}
Using Attributes with Configuration
// Global configuration
JauntyConfig.ColumnNameResolver = ToSnakeCase; // a helper you write; Jaunty ships none
// Entity with attribute override
public partial class Product
{
[Column("product_id")] // This takes precedence over the global resolver
public int ProductId { get; set; }
public string ProductName { get; set; } // This will use snake_case: "product_name"
}
Best Practices
1. Use Attributes for Specific Overrides
Use attributes when you need specific column or table names that differ from conventions:
[Table("tbl_products")] // Specific table name
public partial class Product
{
[Column("pk_product_id")] // Specific column name
[Key] // Mark as primary key
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] // Identity column
public int ProductId { get; set; }
}
2. Combine with Global Configuration
Use attributes for exceptions and global configuration for general conventions:
// Global configuration for snake_case
JauntyConfig.ColumnNameResolver = ToSnakeCase; // a helper you write; Jaunty ships none
// Specific override for this property
public partial class Product
{
[Column("legacy_product_id")] // Override the global convention
public int ProductId { get; set; }
}
3. Mark Computed Properties as Ignored
Always mark properties that are computed or don't exist in the database as [Ignore]:
public partial class Product
{
public int ProductId { get; set; }
public decimal UnitPrice { get; set; }
public int Quantity { get; set; }
[Ignore] // Computed property, not in database
public decimal TotalValue => UnitPrice * Quantity;
}
4. Properly Mark Primary Keys
Ensure primary key properties are properly marked for update/delete operations:
public partial class Order
{
[Key] // Required for Update/Delete operations
public int OrderId { get; set; }
public DateTime OrderDate { get; set; }
public decimal TotalAmount { get; set; }
}
Important Notes
- Attribute Precedence: Attributes take precedence over global configuration
- Primary Key Importance: Update and Delete operations require properly marked primary keys
- Identity Handling: Identity columns should be marked with
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] - Composite Keys: Multiple properties can be marked as
[Key]for composite primary keys - Performance: Attributes are processed once during metadata caching and have minimal runtime impact
- Compatibility: All attributes work with both sync and async operations
- Mapping Modes: Attributes work with both strict and partial mapping modes
- Fluent API: Attributes are respected by the Fluent API as well as direct method calls