Basic Concepts
Edit this page on GitHubExtensions And Fluid Syntax
Norm
is implemented as a set of extensions over theSystem.Data.Common.DbConnection
object.Some extensions may return the new
Norm
object instance.Norm
object instances have methods with the same signature as theSystem.Data.Common.DbConnection
extensions.This allows the use of the fluid syntax over the connection object. Example:
//
// Execute a stored procedure with command timoout 60 seconds
//
connection
.AsProcedure()
.WithTimeout(60)
.Execute("delete_inactive_customers");
//
// Execute a stored procedure with command timoout 60 seconds
//
connection
.AsProcedure()
.WithTimeout(60)
.Execute("delete_inactive_customers");
- Since
WithTimeout
exists as an extension and as an instance method, the example from above could have been written like this:
//
// Execute a stored procedure with command timoout 60 seconds
//
connection
.WithTimeout(60)
.AsProcedure()
.Execute("delete_inactive_customers");
//
// Execute a stored procedure with command timoout 60 seconds
//
connection
.WithTimeout(60)
.AsProcedure()
.Execute("delete_inactive_customers");
Database Commands
- There are three groups of methods used to execute database commands
- Execution
Executes the SQL command without returning a value.
These are:
Execute
ExecuteFormat
ExecuteAsync
ExecuteFormatAsync
- Read Mappings
These are:
Read
ReadFormat
ReadAsync
ReadFormatAsync
- Multiple Mappings
These are:
Multiple
MultipleFormat
MultipleAsync
MultipleFormatAsync
Note: All of these extensions and methods have the same three parameters:
public static Norm ExecuteAsync(this DbConnection connection,
string command,
object parameters = null,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0);
public static Norm ExecuteAsync(this DbConnection connection,
string command,
object parameters = null,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0);
- The last three parameters with the
Caller
attribute (memberName
,sourceFilePath
andsourceLineNumber
) - should not be supplied by the user - they are intended to be used in diagnostics and logging and are supplied automatically by the compiler.
Read Iterators
The
Read
extension method and all the overload versions of that method - will always return the iterator of theEnumerable<T>
interface type.Values are yielded to the iterator as they are returned from the database.
For example, the following statements:
using Norm;
// doesn't do anything with the database
var result1 = connection.Read<int>("select count(*) from actor");
// doesn't do anything with the database
var result2 = connection.Read<string>("select title from film");
using Norm;
// doesn't do anything with the database
var result1 = connection.Read<int>("select count(*) from actor");
// doesn't do anything with the database
var result2 = connection.Read<string>("select title from film");
These two lines will NOT send or execute any commands to the database.
Instead, they will create an iterator over the database connection that will be executed when the actual iteration starts:
using System.Linq;
using Norm;
// doesn't do anything with the database, return iterator
var result1 = connection.Read<int>("select count(*) from actor");
// doesn't do anything with the database, return iterator
var result2 = connection.Read<string>("select title from film");
// executes select count(*) from actor and retuns int
var count = result1.Single();
// executes select title from film and builds a list of film titles
var list = result2.Tolist();
using System.Linq;
using Norm;
// doesn't do anything with the database, return iterator
var result1 = connection.Read<int>("select count(*) from actor");
// doesn't do anything with the database, return iterator
var result2 = connection.Read<string>("select title from film");
// executes select count(*) from actor and retuns int
var count = result1.Single();
// executes select title from film and builds a list of film titles
var list = result2.Tolist();
- Or, simply combine
Linq
statements with the iterator returned from theRead
method:
using System.Linq;
using Norm;
// executes select count(*) from actor and retuns int
var count = connection.Read<int>("select count(*) from actor").Single();
// executes select title from film and builds a list of film titles
var list = connection.Read<string>("select title from film").Tolist();
using System.Linq;
using Norm;
// executes select count(*) from actor and retuns int
var count = connection.Read<int>("select count(*) from actor").Single();
// executes select title from film and builds a list of film titles
var list = connection.Read<string>("select title from film").Tolist();
- Or, for example, a standard
foreach
iteration withoutLinq
:
using Norm;
// executes select title from film and iterate titles
foreach(var title in connection.Read<string>("select title from film"))
{
// ...
}
using Norm;
// executes select title from film and iterate titles
foreach(var title in connection.Read<string>("select title from film"))
{
// ...
}
As we can see, the actual execution is delayed until the first iteration starts.
In simple pseudo-code, this is what the
Read
method does:
IEnumerable<TResult> Read()
{
foreach(var record in reader)
{
yield return record;
}
}
IEnumerable<TResult> Read()
{
foreach(var record in reader)
{
yield return record;
}
}
- This approach allows us to build
Linq
expressions over the iterator - which are then executed only once per iteration, and there is no need for additional buffering.
Async Read Iterators
The asynchronous version
ReadAsync
extension method and all the overload versions of that method - will always return the iterator of theIAsyncEnumerable<T>
interface type.The asynchronous version
ReadAsync
- behaves exactly the same as the synchronousRead
method - execution is delayed until the first iteration starts.IAsyncEnumerable<T>
enumerator interface that provides asynchronous iteration over values:
using System.Linq;
using Norm;
// doesn't do anything with the database, return iterator
var result1 = connection.ReadAsync<int>("select count(*) from actor");
// doesn't do anything with the database, return iterator
var result2 = connection.ReadAsync<string>("select title from film");
// executes select count(*) from actor and retuns int
var count = await result1.SingleAsync();
// executes select title from film and builds a list of film titles
var list = await result2.ToListAsync();
using System.Linq;
using Norm;
// doesn't do anything with the database, return iterator
var result1 = connection.ReadAsync<int>("select count(*) from actor");
// doesn't do anything with the database, return iterator
var result2 = connection.ReadAsync<string>("select title from film");
// executes select count(*) from actor and retuns int
var count = await result1.SingleAsync();
// executes select title from film and builds a list of film titles
var list = await result2.ToListAsync();
The example above uses the
System.Linq.Async
package.System.Linq.Async
is an official .NET Foundation implementation of asynchronous LINQ extension methods. It provides an asynchronous version of every LINQ extension method over theIAsyncEnumerable<T>
interface.
Working With Tuples
Both
Read
andReadAsync
methods implement a complex mapping system - but they both return a single generic value (IEnumerable<T>
andIAsyncEnumerable<T>
, respectively).When those iterators should return multiple values, that generic value is a tuple type. For example:
IEnumerable<(T1, T2)>
- two value tuple(T1, T2)
for two values.IEnumerable<(T1, T2, T3)>
- three value tuple(T1, T2, T3)
for three values.etc.
The same goes for the
IAsyncEnumerable<T>
type.This allows for a convenient tuple deconstruction, for example:
using Norm;
foreach(var (title, description, year) in
connection.Read<string, string, int>("select title, description, release_year from film"))
{
Console.WriteLine("Title: {0}, Description: {1}, Year: {2}", title, description, year)
}
using Norm;
foreach(var (title, description, year) in
connection.Read<string, string, int>("select title, description, release_year from film"))
{
Console.WriteLine("Title: {0}, Description: {1}, Year: {2}", title, description, year)
}
- Tuples can also be named tuples:
using Norm;
foreach(var tuple in
connection.Read<(string title, string description, int year)>("select title, description, release_year from film"))
{
Console.WriteLine("Title: {0}, Description: {1}, Year: {2}", tuple.title, tuple.description, tuple.year)
}
using Norm;
foreach(var tuple in
connection.Read<(string title, string description, int year)>("select title, description, release_year from film"))
{
Console.WriteLine("Title: {0}, Description: {1}, Year: {2}", tuple.title, tuple.description, tuple.year)
}
- We can also return multiple named tuples. In that case, each named tuple is a new tuple:
using Norm;
// deconstruction of named tuples
foreach (var (actor, film) in connection.Read<(int id, string name), (int id, string name)>(@"
select
actor_id, first_name || ' ' || last_name,
film_id, title
from
actor
join film_actor using (actor_id)
join film using (film_id)
limit 3"))
{
WriteLine("Actor: {0}-{1}, Film: {2}-{3}", actor.id, actor.name, film.id, film.name);
}
using Norm;
// deconstruction of named tuples
foreach (var (actor, film) in connection.Read<(int id, string name), (int id, string name)>(@"
select
actor_id, first_name || ' ' || last_name,
film_id, title
from
actor
join film_actor using (actor_id)
join film using (film_id)
limit 3"))
{
WriteLine("Actor: {0}-{1}, Film: {2}-{3}", actor.id, actor.name, film.id, film.name);
}
- That same approach applies when mapping to multiple class instances. Multiple values always yield a tuple:
using Norm;
// deconstruction of class instances
foreach (var (actor, film) in connection.Read<Actor, Film>(@"
select
actor_id, first_name || ' ' || last_name,
film_id, title
from
actor
join film_actor using (actor_id)
join film using (film_id)
limit 3"))
{
WriteLine("Actor: {0}-{1}, Film: {1}-{2}", actor.Id, actor.Name, film.Id, film.Name);
}
using Norm;
// deconstruction of class instances
foreach (var (actor, film) in connection.Read<Actor, Film>(@"
select
actor_id, first_name || ' ' || last_name,
film_id, title
from
actor
join film_actor using (actor_id)
join film using (film_id)
limit 3"))
{
WriteLine("Actor: {0}-{1}, Film: {1}-{2}", actor.Id, actor.Name, film.Id, film.Name);
}
Global Settings
- It is possible to modify
Norm
's default behavior for the entire application by setting the global settings. For example:
using Norm;
NormOptions.Configure(options =>
{
options.CommandTimeout = 60;
});
using Norm;
NormOptions.Configure(options =>
{
options.CommandTimeout = 60;
});
- This example sets a global option for all commands executed by
Norm
to have a timeout of 60 seconds.
Important note:
NormOptions.Configure
call is not thread-safe.
NormOptions.Configure
call is intended to be executed only once from the application's startup code, typicallyStartup.cs
orProgram.cs
, before any database command.