- Prerequisites and Notes
- Execution
- Single row mappings
- Single value
- Multiple value mappings
- ARRAY types (PostgreSQL only)
- Named tuples
- Multiple named tuples
- Named Tuples with ARRAY types (PostgreSQL only)
- Class instance properties
- Class instance properties mapping with different naming styles (different case, snake case)
- Ignored class instance properties
- Multiple class instances
- Multiple class instances with the same name
- Class instances and map ARRAY types (PostgreSQL only)
- Record instances
- Multiple values
- Batching multiple commands
- Database command Parameters
- Using formattable strings to supply parameters
- Using formattable strings to supply native database parameters
- Simple parameters by position
- Simple parameters by position and native database parameters
- Passing class instance and mapping values to parameters
- Mixing class instance, database native, and simple parameters
- Mapping parameters by specific name
- Mapping parameters by specific name and specific database type
- Mapping parameters by specific name and specific custom database type
- Command object parameters
-
All Norm read operations are returning
IEnumerable<?>
orIAsyncEnumerable<?>
for asynchronous operations. -
Values are yielded to the resulting enumerator as they appear on your database connection.
-
To transform values from those resulting enumeration (without manual
foreach
-es), use theLinq
extensions:
using System.Linq;
using Norm;
-
Using
Linq
extensions to transforms your values does not add extra iteration over your values, since they are yielded. -
To be able to use
Linq
extensions forIAsyncEnumerable<?>
types and transform values from asynchronous operations you will need to addSystem.Linq.Async
package to your project.
dotnet add package System.Linq.Async
-
This applies only to frameworks before .NET 6.0, which apparently, has this package embedded with the framework itself.
-
System.Linq.Async shares the same namespace with the
System.Linq
, so no newusing
is needed.
- Methods
Execute
andExecuteAsync
do not return values.
connection.Execute("sql statements to execute");
await connection.ExecuteAsync("sql statements to execute");
connection
.Execute("begin tran")
.Execute("create temp table test (t text)")
.Execute("insert into test values ('foo')")
.Execute("rollback");
-
Use any of the
Linq
extensions available to get a single value from the enumeration, such asFirst
,Single
,FirstOrDefault
orSingleOrDefault
. -
Single row mappings demonstrate different mapping techniques like values, names tuples, class instances, etc
var value1 = connection.Read<int>("select 1").FirstOrDefault();
var value1 = await connection.ReadAsync<int>("select 1").FirstOrDefaultAsync();
- Up to 12 values the most.
- Mapped by position (name is not present).
var (number1, str1, date1) = connection.Read<int, string, DateTime>("select 1, 'str', '2021-18-10'").FirstOrDefault();
var (v1, v2, v3, v4) = connection.Read<int, int, int, int>("select 1, 2, 3, 4").FirstOrDefault();
var (number1, str1, date1) = await connection.ReadAsync<int, string, DateTime>("select 1, 'str', '2021-18-10'").FirstOrDefaultAsync();
var (v1, v2, v3, v4) = await connection.ReadAsync<int, int, int, int>("select 1, 2, 3, 4").FirstOrDefaultAsync();
var arr = connection.Read<int[]>("select array[1, 2]").FirstOrDefault();
var (arr1, arr2) = connection.Read<int[], int[]>("select array[1, 2], array[1, 2, 3, 4]").FirstOrDefault();
Console.WriteLine("{0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}", arr[0], arr[1], arr1[0], arr1[1], arr2[0], arr2[1], arr2[2], arr2[3]);
- If you use
Read<T1, T2>
, the result will be an unnamed tuple that you can map to single values:var (t1, t2) = conn.Read<T1, T2>(sql);
- If you use
Read<(T1, T2)>
, the result will be named tuple which will have IntelliSense autocomplete enabled from your IDE. - Named tuples are also mapped by position, not by name.
var myTuple = connection.Read<(int Value1, int Value2, int Value3)>("select 1, 2, 3").FirstOrDefault();
// intellisense and autocomplete available at this point
Console.WriteLine("{0}, {1}, {2}", myTuple.Value1, myTuple.Value2, myTuple.Value3);
- You can map multiple named tuples at the same time, up to 12 tuples the most:
var (myTuple1, myTuple2) = connection.Read<(int Value1, int Value2), (int Value1, int Value2, int Value3)>("select 1, 2, 3, 4, 5, 6").FirstOrDefault();
// intellisense and autocomplete available at this point
Console.WriteLine("{0}, {1}, {2}, {3}, {4}", myTuple1.Value1, myTuple1.Value2, myTuple2.Value1, myTuple2.Value2, myTuple2.Value3);
var tuple = connection.Read<(int[] Arr1, int[] Arr2)>("select array[1, 2], array[1, 2, 3, 4]").FirstOrDefault();
Console.WriteLine("{0}, {1}, {2}, {3}, {4}, {5}, {6}", tuple.Arr1[0], tuple.Arr1[1], tuple.Arr2[0], tuple.Arr2[1], tuple.Arr2[2], tuple.Arr2[3]);
-
Unlimited number of class instance properties (limited only by .NET internal number of class properties)of any type.
-
Mapped by name (instead of position):
public class MyClass
{
public int Value1 { get; set; }
public int Value2 { get; set; }
}
var instance = connection.Read<MyClass>("select 1 as Value1, 2 as Value2").FirstOrDefault();
var instance = await connection.ReadAsync<MyClass>("select 1 as Value1, 2 as Value2").FirstOrDefaultAsync();
Console.WriteLine("{0}, {1}", instance.Value1, instance.Value2);
- Mapping by name is case insensitive.
- Mapping by name works with snake case naming out of the box.
using System.Linq;
public class MyClass
{
public int MyValue1 { get; set; }
public int MyValue2 { get; set; }
}
var instance = connection.Read<MyClass>("@select
1 as myvalue1,
2 as my_value_2,
3 as my_value_3").FirstOrDefault();
//
// myvalue1 is mapped MyValue1 with different name casing
// my_value_2 (snake cased) is mapped to MyValue2
// my_value_3 is ignored since it is not found
//
Console.WriteLine("{0}, {1}", instance.MyValue1, instance.MyValue2);
- Only properties with the public setter are mapped
public class MyClass
{
public int Value1 { get; set; } // mapped
public int Value2 { get; init; } // mapped
public int Value3; // not mapped
public int Value4 { get; } // not mapped
public int Value5 { get; private set; } // not mapped
public int Value6 { get; protected set; } // not mapped
public int Value6 { get; internal set; } // not mapped
}
var instance = connection.Read<MyClass>(@"select
1 as value1,
2 as value2,
3 as value3,
4 as value4,
5 as value5,
6 as value6,
7 as value7").FirstOrDefault();
- Map up to 12 class instances at the same time.
- Class instance properties are always mapped by name.
public class MyClass1
{
public int Value1 { get; set; }
public int Value2 { get; set; }
}
public class MyClass2
{
public int Value3 { get; set; }
public int Value4 { get; set; }
}
var (instance1, instance2) = connection.Read<MyClass1, MyClass2>(@"select 1 as value1, 2 as value2, 3 as value3, 4 as value4").FirstOrDefault();
Console.WriteLine("{0}, {1}, {2}, {3}", instance1.Value1, instance1.Value2, instance2.Value3, instance2.Value4);
// outputs 1, 2, 3, 4
- When mapping multiple class instances, properties with the same name are mapped by the position they appear.
- From left to right:
MyClass1
maps the first appearances ofvalue1
andvalue2
as they appear in the query from left to right.
public class MyClass1
{
public int Value1 { get; set; }
public int Value2 { get; set; }
}
public class MyClass2
{
public int Value1 { get; set; }
public int Value2 { get; set; }
}
var (instance1, instance2) = connection.Read<MyClass1, MyClass2>(@"select 1 as value1, 2 as value2, 3 as value1, 4 as value2").FirstOrDefault();
Console.WriteLine("{0}, {1}, {2}, {3}", instance1.Value1, instance1.Value2, instance2.Value1, instance2.Value2);
// outputs 1, 2, 3, 4
- Array types are mapped normally
public class MyClass
{
public int[] Array1 { get; set; }
public int[] Array2 { get; set; }
}
var instance = connection.Read<MyClass>(@"select array[1,2] as array1, array[3,4] as array2").FirstOrDefault();
Console.WriteLine("{0}, {1}, {2}, {3}", instance.Array1[0], instance.Array1[1], instance.Array2[0], instance.Array2[1]);
- Everything true for class instance mapping is also true for Record instance mapping:
- Mapping by name (case insensitive, snake case, etc)
- Multiple mappings
- Array mappings
- etc
public record MyRecord(int Value1, int Value2);
var instance = connection.Read<MyRecord>("select 1 as value1, 2 as value2").FirstOrDefault();
var instance = await connection.ReadAsync<MyRecord>("select 1 as value1, 2 as value2").FirstOrDefaultAsync();
Console.WriteLine("{0}, {1}", instance.Value1, instance.Value2);
- Everything in regards to mapping is also true when returning multiple records from your query
- Mapped by position, no field name is required in a query:
var query = "select * from (values (1, 'a', 'x'), (2, 'b', 'y'), (3, 'c', 'z')) t";
foreach (var (id, str1, str2) in connection.Read<int, string, string>(query))
{
Console.WriteLine("{0}, {1}, {2}", id, str1, str2);
}
await foreach(var (id, str1, str2) in connection.ReadAsync<int, string, string>(query))
{
Console.WriteLine("{0}, {1}, {2}", id, str1, str2);
}
foreach(var tuple in connection.Read<(int Id, string Str1, string Str2)>(query))
{
Console.WriteLine("{0}, {1}, {2}", tuple.Id, tuple.Str1, tuple.Str2);
}
await foreach(var tuple in connection.ReadAsync<(int Id, string Str1, string Str2)>(query))
{
Console.WriteLine("{0}, {1}, {2}", tuple.Id, tuple.Str1, tuple.Str2);
}
- Mapped by name, the field name is required in a query:
var query = "select * from (values (1, 'a', 'x'), (2, 'b', 'y'), (3, 'c', 'z')) t (id, str1, str2)";
public class MyClass
{
public int Id { get; set; }
public string Str1 { get; set; }
public string Str2 { get; set; }
}
foreach (var instance in connection.Read<MyClass>(query))
{
Console.WriteLine("{0}, {1}, {2}", instance.Id, instance.Str1, instance.Str2);
}
await foreach (var instance in connection.ReadAsync<MyClass>(query))
{
Console.WriteLine("{0}, {1}, {2}", instance.Id, instance.Str1, instance.Str2);
}
public record MyRecord(int Id, string Str1, string Str2);
foreach (var instance in connection.Read<MyRecord>(query))
{
Console.WriteLine("{0}, {1}, {2}", instance.Id, instance.Str1, instance.Str2);
}
await foreach (var instance in connection.ReadAsync<MyRecord>(query))
{
Console.WriteLine("{0}, {1}, {2}", instance.Id, instance.Str1, instance.Str2);
}
- Using
Linq
to map to any other data structure will do, such asToHashset
for example
var list = connection.Read<T>(query).ToList();
var array = connection.Read<T>(query).ToArray();
var list = await connection.ReadAsync<T>(query).ToListAsync();
var array = await connection.ReadAsync<T>(query).ToArrayAsync();
- Reading generic values yields unnamed tuples, so, to map to dictionary generic names
Item1
andItem2
must be used.
var dict = connection.Read<int, string>("select id, value from my_table").ToDictionary(t => t.Item1, t => t.Item2);
var dict = await connection.ReadAsync<int, string>("select id, value from my_table").ToDictionaryAsync(t => t.Item1, t => t.Item2);
var dict = connection.Read<(int Key, string Value)>("select id, value from my_table").ToDictionary(t => t.Key, t => t.Value);
var dict = await connection.ReadAsync<(int Key, string Value)>("select id, value from my_table").ToDictionaryAsync(t => t.Key, t => t.Value);
- Note: any other
Linq
extension that transforms enumeration will do.
public class MyClass
{
public int Key { get; set; };
// other properties
}
var grouped = connection.Read<MyClass>(sql).GroupBy(g => g.Key);
- Batches are executed by using
Multiple(Async)
extension that returns a disposable object that can doExecute(Async)
and/orRead(Async)
. - SQL Commands (and potential parameters) are supplied to
Multiple(Async)
extension, subsequentExecute(Async)
and/orRead(Async)
calls are parameterless.
using var multiple = connection.Multiple(@"
select 1, 2, 3;
begin tran;
create temp table test (t text);
insert into test values ('foo');
select * from values;
rollback;");
var (one, two, three) = multiple.Read<int, int, int>();
multiple.Execute();
multiple.Execute();
multiple.Execute();
var valuefromTran = multiple.Read<string>().Single();
multiple.Execute();
- Parameters can be supplied to following extensions:
Execute
andExecuteAsync
Read
andReadAsync
Multiple
andMultipleAsync
ExecuteFormat
andExecuteFormatAsync
ReadFormat
andReadFormatAsync
MultipleFormat
andMultipleFormatAsync
- You can supply parameters using formattable string commands by using extension versions with
Format
suffix:
connection.ReadFormat<T>($"select {1}, {2}, {3}");
connection.ReadFormat<T>($"select * from table where id = {1}");
var p = "xyz";
connection.ReadFormat<T>($"select * from table where id = {p}");
- Native parameters allow you to set more precise parameter types.
- Note: Native parameters may require parameter names, which is in this case meaningless.
- Note2: you can mix simple and native parameters in formattable strings
connection.ReadFormat<T>($"select {new SqlParameter("p1", 1)}, {new SqlParameter("p2", 1)}, {new SqlParameter("p3", 3)}");
connection.ReadFormat<T>($"select * from table where id = {1}");
var p = "xyz";
connection.ReadFormat<T>($"select * from table where id = {new SqlParameter("p1", 1)}");
- Note: parameter names are meaningless but required since they are mapped by position.
connection.Read<T>("select @p1, @p2, @p3", 1, 2, 3);
connection.Read<T>("select * from table where id = @p", 1);
var p = "xyz";
connection.Read<T>("select * from table where id = @p", p);
- If the supplied parameter is a native database parameter, it will be interpreted as such
- In this case, parameter name matters
connection.Read<T>("select @p1, @p2, @p3", new SqlParameter("p1", 1), new SqlParameter("p2", 1), {new SqlParameter("p3", 3));
connection.Read<T>("select * from table where id = @p", new SqlParameter("p1", 1));
var p = new SqlParameter("p", 1)
connection.Read<T>("select * from table where id = @p", p);
- In this case, parameter name matters
public class MyClass
{
public string Param1 { get; set; }
public string Param2 { get; set; }
public string Param3 { get; set; }
}
var instance = new MyClass{ Param1 = "value1", Param2 = "value2", Param3 = "value3" };
connection.Read<T>("select @Param1, @Param2, @Param3", instance);
- Instance parameters and native database parameters are mapped by name first and what remains is mapped by position.
public class MyClass
{
public string Param1 { get; set; }
public string Param2 { get; set; }
public string Param3 { get; set; }
}
var instance = new MyClass{ Param1 = "value1", Param2 = "value2", Param3 = "value3" };
connection.Read<T>("select @X, @Param1, @Param2, @Param3, @Y", instance, new SqlParameter("X", 1), "Y");
// outputs 1, "value1", "value2", "value3", "Y"
connection.Read<T>("select @p1, @p2, @p3", ("p1", 1), ("p2", 2), ("p3", 1));
connection.Read<T>("select * from table where id = @p", ("p", 1));
var p = new SqlParameter("p", 1)
connection.Read<T>("select * from table where id = @p", ("p", p));
connection.Read<T>("select @p1, @p2, @p3", ("p1", 1, DbType.Int32), ("p2", 2, DbType.Int32), ("p3", 1, DbType.Int32));
connection.Read<T>("select * from table where id = @p", ("p", 1, DbType.Int32));
var p = new SqlParameter("p", 1)
connection.Read<T>("select * from table where id = @p", ("p", p, DbType.Int32));
connection.Read<T>("select @p1, @p2, @p3", ("p1", 1, NpgsqlDbType.Integer), ("p2", 2, NpgsqlDbType.Integer), ("p3", 1, NpgsqlDbType.Integer));
connection.Read<T>("select * from table where id = @p", ("p", 1, NpgsqlDbType.Integer));
var p = new SqlParameter("p", 1)
connection.Read<T>("select * from table where id = @p", ("p", p, NpgsqlDbType.Integer));
AsProcedure
sets all command types to the procedure for this connection instance.
connection.AsProcedure().Read<T>(name, parameters);
As
sets command type to specific type for all commands in this connection instance.- Command types can be
StoredProcedure
,TableDirect
orText
.
using System.Data;
connection.As(CommandType.TableDirect).Read<T>("my_table");
- Sets all database commands for this connection to wait time in seconds given by timeout parameter.
var timeoutSec = 100; // 100 seconds timeout for this connection
connection.Timeout(timeoutSec).Read<T>(sql, parameters);
- Sets the token to monitor for cancellation requests for all async database commands for this connection.
var timeoutSec = 100; // 100 seconds timeout for this connection
connection.WithCancellationToken(cancellationToken).Execute(sql, parameters);
- Sets all database commands for this connection to prepared mode.
connection.Prepared().Execute(sql, parameters);