Architecture
This page is for contributors. It explains how a call flows through MicroORM and where each responsibility lives.
Source layout
Every type has its own file, named after the type. Folders group files by responsibility, and
each folder is its own namespace (MicroORM.Sessions, MicroORM.Querying, MicroORM.Sql, ...).
Inside the library, Sql names the MicroORM.Sql namespace, so code refers to the type as
Sql.Sql (or through a using SQL = MicroORM.Sql.Sql; alias).
src/MicroORM/
├── Sql/ Sql (interpolated string handler, composition, rendering), RenderedSql,
│ SqlParameterValue, and the internal part records
├── Sessions/ MicroSession (+ .Sync, .Procedures partials), MicroDb, MicroTransaction,
│ ResultSets, MicroEngine, PreparedCommand, ConnectionLease
├── Configuration/ MicroOptions, CommandLog
├── Dialects/ SqlDialect and one class per database; ColumnValue
├── Mapping/ EntityMap, ColumnMap, ValueGeneration, MappingRegistry, EntityBuilder,
│ EntityConfiguration, NamingConvention and its implementations
├── Materialization/ Materializer, ValueConverter, ScalarTypes, Row, RowSchema
├── Querying/ Query, Table, Projection, GroupedQuery, UpdateBuilder,
│ ExpressionTranslator, EntityFinder, GroupKey and the query source interfaces
├── TypeHandlers/ TypeHandler<T>, ITypeHandler, JsonTypeHandler, DelegateTypeHandler,
│ TypeHandlerRegistry
├── Procedures/ ProcedureParameters, ProcedureParameter, DbTypes
└── Exceptions/ MicroConcurrencyException
Sync and async from one implementation
Each operation is written once, as ...Core(bool async, ...) returning a ValueTask. The public
...Async method passes true. The synchronous method passes false and unwraps the result
with MicroSession.Sync, which asserts that the task already completed. With async: false, a
core only calls synchronous ADO.NET methods (ExecuteReader, Read, Open), so no
asynchronous work is ever blocked on. Npgsql and Microsoft.Data.SqlClient use the same pattern
internally.
Type handlers
MicroEngine.Prepare runs after rendering. For each parameter, it applies the registered
handler for the value's type (or for its declared type, when the value is null), then turns
enums into numbers. CreateCommand lets the handler configure the provider parameter. When
reading, the materializer compiles a call to the handler for handled member types.
Flow of a typed query
db.Table<User>().Where(u => u.Age > min).ToListAsync()
│
▼
MappingRegistry.GetMap(User) cached EntityMap: table, columns, key
│
▼
ExpressionTranslator.Predicate u.Age > min → Sql: ("age" > {param 30})
│
▼
Query<T>.ToSql() SELECT <columns> FROM <table> WHERE ... ORDER BY ... <paging>
│
▼
Sql.Render(dialect) text with @p0..@pn + parameter list; identifiers quoted
│
▼
MicroSession.StreamAsync lease connection → DbCommand → reader → Log
│
▼
Materializer.For<T>(reader) compiled Func<DbDataReader, T>, cached per column layout
Design principles
Sqlis the only way SQL enters MicroORM. Every API takesSql, andSqlis built from interpolation,Raw,Identifieror composition. Rendering is deferred until the dialect is known, so fragments are dialect-neutral and parameters are numbered once, globally.- Fail loudly. The translator throws on unsupported shapes rather than evaluating client-side.
Materialization throws
InvalidCastExceptionnaming the value and target type. - No provider references. Anything provider-specific goes through
SqlDialect, includingConfigureParameterfor type hints. - One session model.
MicroDbandMicroTransactiondiffer only inLeaseAsync, the method that hands out a connection (and transaction). Every operation is written once, inMicroSession. - Cache what reflection builds. Entity maps are cached per
MicroDb. Row readers are cached per (type, column names). Property getters are compiled once per column.
Adding a translation
- Add a case in
ExpressionTranslator.VisitCall,VisitMemberorVisitBinary. - If the SQL differs by database, add a virtual member to
SqlDialectand override it inDialects.cs. - Add a SQL-shape test in
QueryTranslationTestsand a behavior test inIntegration/ProviderTestsso it runs against all four databases in CI. - Document it in the table in querying.md.