NovaMapper 0.1.9

dotnet add package NovaMapper --version 0.1.9
                    
NuGet\Install-Package NovaMapper -Version 0.1.9
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="NovaMapper" Version="0.1.9" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="NovaMapper" Version="0.1.9" />
                    
Directory.Packages.props
<PackageReference Include="NovaMapper" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add NovaMapper --version 0.1.9
                    
#r "nuget: NovaMapper, 0.1.9"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package NovaMapper@0.1.9
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=NovaMapper&version=0.1.9
                    
Install as a Cake Addin
#tool nuget:?package=NovaMapper&version=0.1.9
                    
Install as a Cake Tool

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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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.

Version Downloads Last Updated
0.1.9 115 7/18/2026
0.1.8 114 7/17/2026
0.1.7 106 7/16/2026
0.1.6 111 7/16/2026
0.1.5 125 3/21/2026