NovaMapper 0.1.9
dotnet add package NovaMapper --version 0.1.9
NuGet\Install-Package NovaMapper -Version 0.1.9
<PackageReference Include="NovaMapper" Version="0.1.9" />
<PackageVersion Include="NovaMapper" Version="0.1.9" />
<PackageReference Include="NovaMapper" />
paket add NovaMapper --version 0.1.9
#r "nuget: NovaMapper, 0.1.9"
#:package NovaMapper@0.1.9
#addin nuget:?package=NovaMapper&version=0.1.9
#tool nuget:?package=NovaMapper&version=0.1.9
NovaMapper
Source-generated, AOT-compatible SQL-object mapper for C# with support for nullable reference types and custom mappers.
SQL-object mapper
Automatically converts rows returned from SQL queries into C# objects:
public record Person(string Name, int Age);
IEnumerable<Person> people = connection.Query<Person>(
"SELECT Name, Age FROM People WHERE Age >= @age", [("@age", 25)]);
Conversion is performed by matching column names to constructor parameters with the same name. The most specific constructor is chosen based on the number of parameters.
Source-generated, AOT-compatible
NovaMapper uses C# source generators to generate the mapping code at compile time, resulting in high performance and compatibility with Ahead-of-Time (AOT) compilation. No usage of reflection or runtime code generation needed.
Support for nullable reference types
NovaMapper fully supports C# 8.0 nullable reference types, allowing you to define your data models with proper nullability annotations:
public record Person(string Name, int Age, string? Email);
IEnumerable<Person> people = connection.Query<Person>("SELECT Name, Age, Email FROM People");
Here people collection guaranteed not to contain null values. Each Person object is guaranteed to have a not-null
Name and Age properties. In case of NULL value in Name or Age column, an exception will be thrown. Email property can be null if the corresponding column value is NULL.
Custom mappers
NovaMapper allows you to define custom mappers that convert between C# types and database types. Column mappers handle DB-to-C# conversion, parameter mappers handle C#-to-DB. Register them via the Mapper class:
var mapper = new Mapper(
columnMappers: [new UnixMillisecondsMapper()],
parameterMappers: [new UnixMillisecondsMapper()]);
// Pass to a single call:
var people = connection.Query<Person>("SELECT ...", mapper: mapper);
// Or set as the global default:
Mapper.Default = mapper;
There is a built-in set of SQLite-related mappers in NovaMapper.CustomMappers.SqliteMappers:
Mapper.Default = new Mapper(SqliteMappers.ColumnMappers, SqliteMappers.ParameterMappers);
Other built-in mappers: Iso8601Mapper, UnixMillisecondsMapper. See Custom mappers (detailed) for how to write your own.
Performance
NovaMapper is designed for high performance, with minimal overhead compared to manual mapping. Benchmarks:
BenchmarkDotNet v0.15.8, macOS Sequoia 15.7.4 (24G517) [Darwin 24.6.0]
Apple M1 Pro, 1 CPU, 10 logical and 10 physical cores
.NET SDK 10.0.103
[Host] : .NET 10.0.3 (10.0.3, 10.0.326.7603), Arm64 RyuJIT armv8.0-a
DefaultJob : .NET 10.0.3 (10.0.3, 10.0.326.7603), Arm64 RyuJIT armv8.0-a
| Method | Mean | Error | StdDev | Ratio | Gen0 | Allocated | Alloc Ratio |
|------------------ |---------:|--------:|--------:|------:|--------:|----------:|------------:|
| NovaMapper_Record | 660.8 us | 8.00 us | 7.48 us | 1.07 | 23.4375 | 149.41 KB | 3.79 |
| NovaMapper_Struct | 648.4 us | 1.79 us | 1.40 us | 1.05 | 5.8594 | 40.06 KB | 1.02 |
| Dapper_Record | 673.3 us | 1.44 us | 1.28 us | 1.09 | 31.2500 | 195.84 KB | 4.97 |
| Dapper_Struct | 668.8 us | 1.35 us | 1.06 us | 1.09 | 31.2500 | 195.88 KB | 4.97 |
| Manual_Struct | 615.4 us | 4.52 us | 4.01 us | 1.00 | 5.8594 | 39.44 KB | 1.00 |
| Manual_Record | 645.8 us | 1.58 us | 1.47 us | 1.05 | 23.4375 | 148.78 KB | 3.77 |
// * Hints *
Outliers
SqliteBenchmarks.NovaMapper_Struct: Default -> 3 outliers were removed (655.88 us..659.94 us)
SqliteBenchmarks.Dapper_Record: Default -> 1 outlier was removed (681.40 us)
SqliteBenchmarks.Dapper_Struct: Default -> 3 outliers were removed, 4 outliers were detected (666.42 us, 672.47 us..687.66 us)
SqliteBenchmarks.Manual_Struct: Default -> 1 outlier was removed (628.53 us)
One key feature is that value types (structs) are never boxed during mapping, which significantly reduces memory allocations and improves performance.
API
Extension methods on IDbConnection:
Query<T>
Maps query results to objects of type T. T must be a type registered for mapping (see How it works).
IEnumerable<T> Query<T>(
string query,
IEnumerable<(string Name, object? Value)>? parameters = null,
Mapper? mapper = null)
IEnumerable<Person> people = connection.Query<Person>(
"SELECT Name, Age FROM People WHERE Age >= @age",
[("@age", 25)]);
QueryScalar<T>
Maps a single-column result set to Option<T> values. Each row yields Option<T>.Some(value) or Option<T>.None for NULL.
IEnumerable<Option<T>> QueryScalar<T>(
string query,
IEnumerable<(string Name, object? Value)>? parameters = null,
Mapper? mapper = null)
IEnumerable<Option<string>> emails = connection.QueryScalar<string>("SELECT Email FROM People");
// Unwrap with various strategies:
IEnumerable<string> nonNull = emails.UnwrapOrThrow(); // throws on NULL
IEnumerable<string?> nullable = emails.UnwrapOrNull(); // NULL becomes null
IEnumerable<string> withDefault = emails.UnwrapOrDefault("N/A");
IEnumerable<string> skipped = emails.UnwrapOrSkip(); // skip NULL rows
QueryScalarValue<T>
Shorthand for a query expected to return exactly one non-null scalar value. Throws if the result is NULL or the query doesn't return exactly one row.
T QueryScalarValue<T>(
string query,
IEnumerable<(string Name, object? Value)>? parameters = null,
Mapper? mapper = null)
int count = connection.QueryScalarValue<int>("SELECT COUNT(*) FROM People");
Execute
Executes a non-query command and returns the number of rows affected.
int Execute(
string query,
IEnumerable<(string Name, object? Value)>? parameters = null,
Mapper? mapper = null)
int affected = connection.Execute(
"UPDATE People SET Age = @age WHERE Name = @name",
[("@age", 30), ("@name", "Alice")]);
Common parameters
| Parameter | Description |
|---|---|
query |
SQL query string. |
parameters |
Optional list of (string Name, object? Value) tuples. Parameter values are run through parameter mappers before being assigned. |
mapper |
Optional Mapper instance. Defaults to Mapper.Default. |
Mapper
The Mapper class orchestrates column mapping, parameter mapping, and object construction. All IDbConnection extension methods accept an optional Mapper parameter. When omitted, Mapper.Default is used.
var mapper = new Mapper(
columnMappers: [new UnixMillisecondsMapper()],
parameterMappers: [new UnixMillisecondsMapper()]);
// Pass explicitly to a single call:
var people = connection.Query<Person>("SELECT ...", mapper: mapper);
// Or set as the global default so all calls use it:
Mapper.Default = mapper;
The Mapper class also exposes Map<T> and MapScalar<T> methods that work directly with an IDataReader, useful when you need more control over command execution:
using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT Name, Age FROM People";
using var reader = cmd.ExecuteReader();
IEnumerable<Person> people = mapper.Map<Person>(reader);
Exceptions
All exceptions inherit from NovaMapperException, which includes an optional Row number in the message when applicable.
| Exception | When thrown |
|---|---|
TypeCantBeMappedException |
The type T passed to Query<T> is not registered for mapping. |
NoMatchingConstructorException |
No constructor of T has parameters matching the columns returned by the query. |
ZeroColumnsException |
The query returned zero columns. |
SingleColumnExpectedException |
QueryScalar<T> was called, but the query returned more than one column. |
ColumnBindingException |
A column can't be bound to any constructor parameter. |
ColumnMappingException |
A column value couldn't be mapped to the target type — either a custom mapper returned an error, or a NULL was encountered for a non-nullable parameter. Includes the row number. |
ParameterMappingException |
A parameter mapper returned an error when converting a query parameter value. |
Option & Result
Lightweight value types are used across the API to represent optional values and operation outcomes without exceptions.
Option<T>
Represents a value that may or may not be present. Used by QueryScalar<T> to represent nullable column values.
Option<string> some = Option.Some<string>("hello");
Option<string> none = Option.None<string>();
// Pattern matching:
switch (some)
{
case (Option.IsSome, var value): Console.WriteLine(value); break;
case (Option.IsNone, _): Console.WriteLine("no value"); break;
}
// TryGetValue:
if (some.TryGetValue(out var v))
Console.WriteLine(v);
Extension methods on Option<T> and IEnumerable<Option<T>>:
| Method | Description |
|---|---|
UnwrapOrThrow() |
Returns the value or throws InvalidOperationException. |
UnwrapOrNull() |
Returns the value or null (reference types). |
UnwrapOrNullStruct() |
Returns the value or null (value types via T?). |
UnwrapOrDefault(T defaultValue) |
Returns the value or the provided default. |
UnwrapOrSkip() |
Filters out None values (IEnumerable only). |
Result<TOk, TErr>
Represents a success or failure outcome. Used by custom mappers to return mapping results.
Result<int, string> ok = Result.Ok<int, string>(42);
Result<int, string> err = Result.Err<int, string>("something went wrong");
// Pattern matching:
switch (ok)
{
case (IsOk, var v, _): Console.WriteLine(v); break;
case (IsErr, _, var e): Console.WriteLine(e); break;
}
Custom mappers
Parameter mappers
Parameter mappers convert C# values into types that the database provider understands. Implement IParameterMapper<TFrom, TTo>:
public interface IParameterMapper<in TFrom, TTo> : IParameterMapper
where TFrom : notnull where TTo : notnull
{
Result<TTo, string> Map(TFrom from);
}
Example — mapping DateTimeOffset to a Unix milliseconds long:
public class UnixMillisecondsMapper : IParameterMapper<DateTimeOffset, long>
{
Result<long, string> IParameterMapper<DateTimeOffset, long>.Map(DateTimeOffset from) =>
Ok<long, string>(from.ToUnixTimeMilliseconds());
}
Register parameter mappers via the Mapper constructor:
var mapper = new Mapper(parameterMappers: [new UnixMillisecondsMapper()]);
Column mappers
Column mappers convert database column values into C# types. Implement IColumnMapper<TColumn, TTarget>:
public interface IColumnMapper<in TColumn, TTarget> : IColumnMapper
where TColumn : notnull where TTarget : notnull
{
Result<TTarget, string> Map(TColumn value);
}
Example — mapping a long column to DateTimeOffset:
public class UnixMillisecondsMapper : IColumnMapper<long, DateTimeOffset>
{
private static readonly long MinValue = DateTimeOffset.MinValue.ToUnixTimeMilliseconds();
private static readonly long MaxValue = DateTimeOffset.MaxValue.ToUnixTimeMilliseconds();
Result<DateTimeOffset, string> IColumnMapper<long, DateTimeOffset>.Map(long value) =>
value >= MinValue && value <= MaxValue
? Ok<DateTimeOffset, string>(DateTimeOffset.FromUnixTimeMilliseconds(value))
: Err<DateTimeOffset, string>($"Value {value} is out of range for {nameof(DateTimeOffset)}.");
}
Register column mappers via the Mapper constructor:
var mapper = new Mapper(columnMappers: [new UnixMillisecondsMapper()]);
A single class can implement both IParameterMapper and IColumnMapper to handle conversions in both directions.
Null handling
Custom mappers are never called with NULL values. NovaMapper checks for NULL before invoking any mapper, so Map always receives a non-null input. Null handling is determined by the nullability of the target constructor parameter:
| Constructor parameter | NULL column value |
|---|---|
string, int, etc. (non-nullable) |
Throws ColumnMappingException |
string? (nullable reference type) |
Maps to null |
int? (nullable value type) |
Maps to null |
The same applies to parameter mappers — null parameter values are passed directly to the database command without going through any mapper.
How it works
NovaMapper uses a C# source generator to generate mapping code at compile time. It scans object-mapping calls such as Query<T> and Mapper.Map<T>, and automatically generates an IObjectMapper implementation capable of mapping all found types. (Scalar helpers like QueryScalar<T> are not scanned — they use column mapping and need no generated object mapper.)
In cases where the source generator can't detect usage (e.g., when mapping is invoked through a generic method), you can explicitly register a type:
Mapper.RegisterForMapping<Person>();
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.