Blazer is a tiny ADO.NET object mapper
Clone or download
Latest commit 96f015e Aug 27, 2017

README.md

Blazer

Blazer is a tiny, high-performance object mapper that serves as an extension to ADO.NET.

You can get it on NuGet.

Blazer's API consists of a couple of useful extension methods on System.Data's provider-agnostic IDbConnection interface. These take care of a lot of the boilerplate code that's normally involved when using ADO.NET directly.

So, instead of doing stuff like this:

using (var cmd = m_conn.CreateCommand())
{
    cmd.CommandText = "SELECT * FROM [Production].[TransactionHistory] WHERE [TransactionID] = @Id";

    var dbParam = cmd.CreateParameter();
    dbParam.Direction = ParameterDirection.Input;
    dbParam.DbType = DbType.Int32;
    dbParam.ParameterName = "@Id";
    dbParam.Value = 42;
    cmd.Parameters.Add(dbParam);

    using (var reader = cmd.ExecuteReader())
    {
        if (reader.Read())
        {
            var transaction = new TransactionHistory();

            transaction.TransactionID = reader.GetInt32(0);
            transaction.ProductID = reader.GetInt32(1);
            transaction.ReferenceOrderID = reader.GetInt32(2);
            transaction.ReferenceOrderLineID = reader.GetInt32(3);
            transaction.TransactionDate = reader.GetDateTime(4);
            transaction.TransactionType = reader.GetString(5);
            transaction.Quantity = reader.GetInt32(6);
            transaction.ActualCost = reader.GetDecimal(7);
            transaction.ModifiedDate = reader.GetDateTime(8);

            return transaction;
        }
        else
        {
            return null;
        }
    }
}

...Blazer lets you do this:

return m_conn.QuerySingle<TransactionHistory>(
      "SELECT * FROM [Production].[TransactionHistory] WHERE [TransactionID] = @Id",
      new { Id = 42 }
    );

Or, if your project is >= .Net 4.6 or >= .Net Standard 1.3, you can use string interpolation to do this:

return m_conn.QuerySingle<TransactionHistory>(
      $"SELECT * FROM [Production].[TransactionHistory] WHERE [TransactionID] = {42}"
    );

Design Goals

Blazer's primary features are:

  1. Speed. Blazer's performance is on level with that of hand-crafted ADO.NET. (also see Performance section)
  2. Simplicity. Blazer has a minimal API and does exactly what you would expect. Nothing more, nothing less. (also see API Functions section)
  3. Lightweight. One assembly. One namespace. No external dependencies.
  4. Provider-agnostic. There is no separate code for SQL Server, SQLite, MySql, etc... As long as there is an ADO.NET provider for it, Blazer just works.
  5. Configuration-less. Blazer requires zero configuration and does not depend on any pre-generated code or models.

There are also a lot of things that Blazer explicitly does not do:

  1. Blazer will not write your SQL for you.
  2. Blazer will not create, migrate, or otherwise manage your database.
  3. Blazer will not generate code from your database schema, or vice versa.
  4. Blazer will not perform change tracking on loaded entities.
  5. Blazer will not load anything that's not a flat object (i.e. no child entities, collections, relations, etc).
  6. Blazer will never threaten to stab you and, in fact, cannot speak.

In short, Blazer is not an ORM. It lacks a lot of fancy features that come standard with libraries like Entity Framework. But the things it does do, it aims to do really well.

And while it's not a fully-fledged Object Relational Mapper, Blazer does perform convention-based object mapping when loading query results. See the Mapping conventions section for more on this.

Quickstart

  1. Reference Blazer.dll (get it on NuGet).
  2. Import the Blazer namespace.
  3. Open an IDbConnection.
  4. Use the Blazer extension methods on IDbConnection.

Minimal code example: (uses the Adventure Works 2014 database)

namespace Quickstart
{
    using System.Data;
    using System.Data.SqlClient;
    using Blazer;
    
    class Program
    {
        static void Main(string[] args)
        {
            dynamic product;
            using (IDbConnection conn = new SqlConnection(@"Server=.\LOCALSQL;Database=AdventureWorks;Integrated Security=True"))
            {
                conn.Open();
                product = conn.QuerySingle("SELECT * FROM [Production].[Product] WHERE [ProductID] = @Id", new { Id = 328 });
                conn.Close();
            }
            Console.WriteLine(product.Name);
        }
    }
}

API Functions

Here's all the different things Blazer can do for you.

Querying

For typed query results, use:

IEnumerable<T> IDbConnection.Query<T>(string command, object parameters = null, CommandConfiguration config = null) where T : new()

For dynamic query results, use:

IEnumerable<dynamic> IDbConnection.Query(string command, object parameters = null, CommandConfiguration config = null)

For retrieving single results, use:

T IDbConnection.QuerySingle<T>(string command, object parameters = null, CommandConfiguration config = null) where T : new()

and:

dynamic IDbConnection.QuerySingle(string command, object parameters = null, CommandConfiguration config = null)

Scalar queries

To execute a scalar query, use:

T IDbConnection.Scalar<T>(string command, object parameters = null, CommandConfiguration config = null)

Note that the singular result of the scalar query is casted directly to T.

Commands / Non-queries

To execute a command (or non-query), use:

int IDbConnection.Command(string command, object parameters = null, CommandConfiguration config = null)

Stored procedures

A stored procedure can be executed in two different ways:

  1. As a regular query using the CommandType.StoredProcedure option.
  2. As a non-query using input/output parameters.

For the second option, use:

void IDbConnection.StoredProcedure(string command, SpParameters parameters = null, CommandConfiguration config = null)

The SpParameters object contains all input-, output-, and return parameters.

To add an input parameter, use:

void SpParameters.AddInput(string name, object value, DbType? dbType = null, int? size = null, byte? precision = null, byte? scale = null)

To add an output parameter, use:

void SpParameters.AddOutput(string name, DbType dbType, int? size = null, byte? precision = null, byte? scale = null)

To add an input/output parameter, use:

void SpParameters.AddInputOutput(string name, object value, DbType? dbType = null, int? size = null, byte? precision = null, byte? scale = null)

To set the return parameter, use:

void SpParameters.SetReturn(string name, DbType dbType, int? size = null, byte? precision = null, byte? scale = null)

After executing the stored procedure using IDbConnection.StoredProcedure, the output- and return values are available from the SpParameters object.

To get the value of an output parameter, use:

T SpParameters.GetOutputValue<T>(string name)

To get the return value, use:

T SpParameters.GetReturnValue<T>()

Note that values of the output- and return parameters are casted directly to T.

Command parameters and configuration

With the exception of StoredProcedure, all Blazer IDbConnection functions have arguments of shape (string command, object parameters = null, CommandConfiguration config = null).

command is pretty self-explanatory: it contains the command text.

(optional) parameters contains the command parameters. You'll most likely just want to use an anonymous type for this, although this is not a requirement. Any type can be used, though only Public Instance properties of the type can be used as parameter values. For the conventions Blazer uses to map properties of the parameter object to IDataParameters on a command, see the Mapping conventions section.

(optional) config contains some configuration options to use for the command. It can contain things like the command timeout or IDbTransaction to use. If null is passed here, Blazer falls back to its default configuration options. The defaults can be accessed through CommandConfiguration.Default. When a config value is provided, not all options on it are necessarily set. A second option, CommandConfiguration.OnUnsetConfigurationOption, controls how Blazer deals with this situation.

async/await

For any target platform that supports async/await, Blazer's various functions contain a matching ...Async() version to support asynchonous programming patterns.

String interpolation

Starting with .Net 4.6 and .Net Standard 1.3, Blazer supports string interpolation as an alternative way of passing parameters to queries. For this, Blazer's IDbConnection functions contain an overload of shape (FormattableString commandString, CommandConfiguration config = null), combining the command string and the parameters in one argument.

So, instead of doing something like this:

someConnection.Query("SELECT * FROM Foo WHERE Bar = @Bar", new { Bar = 42 });

...you can just do this:

someConnection.Query($"SELECT * FROM Foo WHERE Bar = {42}");

Blazer will intercept the interpolated string and its argument values, and uses them to produce a parameterized SQL query (as it would for regular string queries).

In the SQL profiler, this first query produces the following:

exec sp_executesql N'SELECT * FROM Foo WHERE Bar = @Bar',N'@Bar int',@Bar=42

...while the second query produces this:

exec sp_executesql N'SELECT * FROM Foo WHERE Bar = @p__blazer__0',N'@p__blazer__0 int',@p__blazer__0=42

Performance

Blazer is blazingly fast. Here are some performance benchmarks comparing Blazer to other popular data access libraries.

Selecting many records (typed results)

In this test, the top N records are selected in a single query and are mapped to a strongly-typed POCO.
Results are averaged over 10 runs. The values are formatted average (stdev).

Provider N = 500 N = 5.000 N = 50.000
Hand-Crafted ADO.NET 0,5ms (0,4ms) 7,2ms (0,4ms) 81,2ms (2,96ms)
Blazer v0.1.0 0,8ms (0,4ms) 8,3ms (0,46ms) 93,1ms (2,91ms)
Linq2SQL .NET 4.6.1 1,1ms (0,3ms) 11,9ms (0,54ms) 120,4ms (5,31ms)
Linq2SQL .NET 4.6.1 (change tracking) 1,4ms (0,66ms) 12,9ms (0,7ms) 132,3ms (2,57ms)
Entity Framework v6.1.3 1,2ms (0,6ms) 12ms (0,77ms) 139,6ms (8,26ms)
Entity Framework v6.1.3 (change tracking) 6,9ms (0,54ms) 90,4ms (6,81ms) 1009,2ms (38,21ms)
Dapper v1.42.0 1,1ms (0,13ms) 10ms (0,2ms) 118,1ms (2,02ms)
OrmLite v4.0.56 1ms (0ms) 9ms (0,7ms) 106,3ms (2,05ms)
PetaPoco v5.1.1.171 1,2ms (0,08ms) 9,9ms (0,3ms) 98,5ms (1,75ms)

Blazer takes the gold, but it's a photo finish. In fact, all tested providers perform great in this category, except for Entity Framework with change tracking enabled.

Selecting many records (dynamic results)

In this test, the top N records are selected in a single query and are provided as dynamic results.
Results are averaged over 10 runs. The values are formatted average (stdev).

Provider N = 500 N = 5.000 N = 50.000
Blazer v0.1.0 2,7ms (1,76ms) 10,6ms (0,66ms) 182,4ms (3,17ms)
Dapper v1.42.0 1,3ms (0,9ms) 10,4ms (0,49ms) 126,5ms (1,2ms)
PetaPoco v5.1.1.171 2,9ms (0,54ms) 22,3ms (0,46ms) 322,2ms (4,24ms)
Simple.Data v0.19.0 4,9ms (2,7ms) 34,9ms (2,47ms) 334,9ms (1,81ms)
Massive v2.0 9,8ms (1,47ms) 30,7ms (0,9ms) 248,5ms (1,8ms)

Dapper wins at this particular test, with Blazer at an undisputed second place. Not too shabby!

Selecting single records (typed results)

In this test, single, random records are selected N times and are mapped to a strongly-typed POCO.
Results are averaged over 10 runs. The values are formatted average (stdev).

Provider N = 500 N = 5.000 N = 50.000
Hand-Crafted ADO.NET 35,5ms (1,63ms) 349,8ms (2,14ms) 3512,9ms (14,46ms)
Blazer v0.1.0 37ms (4,49ms) 366,7ms (2,49ms) 3674,5ms (14,53ms)
Linq2SQL .NET 4.6.1 414,9ms (5,03ms) 4143,7ms (15,79ms) 41686,2ms (407,96ms)
Linq2SQL .NET 4.6.1 (change tracking) 421,5ms (15,09ms) 4194,2ms (62,36ms) 35444,9ms (411,42ms)
Entity Framework v6.1.3 218,1ms (7,62ms) 2248,7ms (50,05ms) 21956,8ms (112,96ms)
Entity Framework v6.1.3 (change tracking) 228,8ms (9,83ms) 2310ms (24,35ms) 22811,6ms (105,37ms)
Dapper v1.42.0 39,5ms (3,98ms) 379ms (2,68ms) 3783,3ms (10,61ms)
OrmLite v4.0.56 67,6ms (3,26ms) 661ms (2,76ms) 6587,3ms (21,18ms)
PetaPoco v5.1.1.171 47,1ms (1,92ms) 473,5ms (15,24ms) 4612,4ms (28,9ms)

Blazer wins at this one, with Dapper and PetaPoco closely behind. Microsoft's own data access tech falls a bit short here, and is about an order of magnitude slower.

Everything scales linearly over N, as expected.

Selecting single records (dynamic results)

In this test, single, random records are selected N times and provided as dynamic results.
Results are averaged over 10 runs. The values are formatted average (stdev).

Provider N = 500 N = 5.000 N = 50.000
Blazer v0.1.0 36,8ms (1,99ms) 340,4ms (21,06ms) 3480,7ms (284,05ms)
Dapper v1.42.0 45,1ms (5,43ms) 420,9ms (7,46ms) 4280,1ms (98,7ms)
PetaPoco v5.1.1.171 59,4ms (3,9ms) 541,3ms (41,77ms) 5022,7ms (176,1ms)
Simple.Data v0.19.0 134,5ms (6,34ms) 1470,6ms (16,49ms) 14851,8ms (221,19ms)
Massive v2.0 132,6ms (9,26ms) 1436,2ms (14,12ms) 14371,5ms (28,6ms)

Although Dapper won when selecting many dynamic results in a single query, Blazer is faster when selecting single records as dynamic.

Mapping conventions

Here's how Blazer does its mapping.

Input parameters

For input parameters, the name of the parameter property will become the name of the IDbDataParameter added to the command, prefixed with an "@".

So for example, take the following input parameter object:

{
  CategoryId = 42,
  YearStart = 2016
}

In this case two parameters named @CategoryId and @YearStart will be added to the command.

Blazer does not attempt to parse the command string to see if each parameter is actually used. All properties in the input parameter object will be blindly added as command parameters.

Note: when using Blazer's string interpolation functions, none of the above is applicable as Blazer just generates its own parameter names, and adds parameters automatically for each string interpolation argument. So in that case you don't have to worry about any of this :)

Output parameters

In the case of typed query results, Blazer performs mapping from the columns of the query result set to fields on the result type. To do this it attempts to find a matching field for each column of the result set, using a couple of simple matching rules that are attempted in sequence.

First, it looks for any public instance member (field or property) which has a System.ComponentModel.DataAnnotations.Schema.ColumnAttribute where the ColumnAttribute.Name value equals the name of the result set column.

Secondly, it looks for any member (again, public instance field or property) where the name (MemberInfo.Name property) equals the name of the result set column.

StringComparison.OrdinalIgnoreCase is used for all string matching.

The first member found is matched to the result set column. Columns not matched to any member are simply ignored.

License

See LICENSE.txt

Copyright

Blazer is copyright (c) 2017 Bart Wolff, www.bartwolff.com