Auto Mapping - lobodava/artisan-orm GitHub Wiki
The ReadAs* family (ReadAs<T>, ReadAsList<T>, ReadAsArray<T>, ReadAsDictionary, etc.) reads result sets without requiring a hand-written mapper. Properties on T are matched to result-set columns by name (exact, case-sensitive matching) using a delegate built once via expression trees and then cached.
The cache details below describe the source being prepared for 4.1.0, not a statement that this package version has been published.
-
On the first call for a given
(T, column-shape)pair, the auto-mapper:- inspects
T's public, settable, simple-typed properties via reflection, - matches each one to a result-set column by name,
- builds an expression tree that allocates a
Tand assigns each property from the correspondingSqlDataReader.Get*call, - compiles the tree to a
Func<SqlDataReader, T>delegate, - caches it by target type and result shape (ordered column names and field types), not SQL command text.
- inspects
-
Later calls with the same type and shape reuse the compiled delegate. The lookup still inspects reader metadata to identify the shape; it does not rebuild the expression tree. Measure the actual workload when comparing it with a hand-written
[MapperFor].
In 4.1.0 the target key is the actual runtime Type, not only Type.FullName; same-named types from different assemblies stay separate. Shape components are encoded unambiguously, so punctuation inside a column alias cannot collide with a different column list. Changing column order or field types selects a different mapping entry. This protects cache identity; it does not validate the result against a database schema.
- Application queries with a suitable property-to-column contract, including one-off queries where a separate
CreateObjectadds little value. - Result sets whose column names happen to match property names exactly.
- DTOs that already exist for serialization, where you don't want a parallel mapper class.
- You need a custom mapping (a column has a different name from the property, or you want to compose a navigation property from several columns).
- You need to avoid expression-tree compilation. Registered mappers still use reflection for discovery; validate trimming/Native AOT compatibility separately rather than assuming either path guarantees it.
- You want to control exactly how columns are converted or how SQL NULL is represented. Auto-mapping can target different DTOs, provided each has a compatible shape.
- You also need to save the type — a
[MapperFor]mapper can carryCreateDataTable/CreateDataRowfor table-valued parameters, whileAsDataTablealso supports auto-mapping for writes.
public class User
{
public int Id { get; set; }
public string Login { get; set; } = null!;
public string Name { get; set; } = null!;
public string Email { get; set; } = null!;
}
// No [MapperFor] attribute — columns and properties are matched by name.
public IList<User> GetAllUsers()
{
return ReadAsList<User>("select Id, Login, Name, Email from dbo.Users");
}The generated mapper uses typed getters and numeric conversions according to the SQL and property types; these conversions are not a substitute for a consistent SELECT contract. Nullable value types and strings receive an IsDBNull check, so SQL NULL becomes null for both string and string?, independently of nullable-reference annotations. Non-nullable properties require non-null compatible SQL values.
In 4.1.0, nullable and non-nullable enum properties both participate in automatic mapping through their underlying numeric types. The conversion does not establish that a numeric value names an enum member or matches a live SQL schema. Reader mapping selects public instance setters and skips indexers; automatic DataTable mapping selects public instance getters and also skips indexers and private getters.
Unmatched columns are ignored, and properties without matching columns retain their defaults. If no columns match writable simple properties, mapping fails with ArtisanMappingException. The target must be constructible by the generated parameterless constructor call.
Source: SqlDataReaderExtensions_CreateObject.cs.
There is also an AsDataTable<T> extension that uses the same reflection / expression-tree machinery in the opposite direction — building a DataTable from IEnumerable<T> for use as a TVP. See cmd.AddTableParam for the auto-mapping overloads of TVP creation.
Its 4.1.0 cache separates the actual target Type, requested table name, and ordered column selection. The table name and selected columns have distinct roles in the cache, including when names contain punctuation.
These rules describe automatic mapping. A manual mapper may deliberately rename columns, combine values, or create a TVP shape different from the C# model. Match its output to the actual SQL contract; do not impose automatic property matching on a hand-written mapper. Regression coverage is in MappingCacheTest.cs.
See also: