Griffin DAL Generator is a tool which can generate pure ADO.NET based repositories for you. This new version is a Visual Studio Extension. Right-click on any project in the Solution Explorer to start the generator wizard.
As you might already know if you’ve read my blog, I’m not a big fan of ORM libraries. They speed up the development process and seemingly make the code cleaner. However, as soon as you begin to wander off the main path, the pain starts. Same headache when the ORM library fails. The tedious process of figuring out if it’s your mapping, the library itself or the generated SQL query begins.
A code generator is much better. It does the same job for you, but is 100% transparent. The code that you get does not hide anything and it’s yours to modify as you please. You can optimize it where it matters, ignore fields when needed or simply use it as is. If you get an exception, you will in most cases know where the error is (as the data exception is not wrapped in a ORM exception). No need to understand anything but SQL and standard ADO.NET code.
We’ve worked hard to make the generated code beautiful and easy to read. Some parts might not be as elegant, but that’s due to a tradeoff between simplicity and performance. Regarding performance, which type of library do you think performs best? An ORM that does a lot for you, a data mapper that does mappings for you, or generated code that contain no extra calls at all?
Functional Walkthrough
You start the tool by right-clicking on the project that you want to add the repositories to and pick the “Add -> Griffin DAL Repo Generator” option in the context menu:

The extension selects the first connection string that it finds in your app/web.config(s). You can of course select another connection string. All tables that exist in the database are listed in the UI. Simply select the tables that you want to generate repositories for.

As you can see, there is a preview for every file being generated.

The generator currently supports the following repository types:
- Repository (ADO.NET) – An ADO.NET repository without any external dependencies.
- Async Repository (ADO.NET) – An ADO.NET repository using
async
/wait
. - Repository (
Griffin.Framework
) – A repository that uses the super awesome data mapper in Griffin.Framework
. - Async Repository (
Griffin.Framework
) – A repository that uses the async API in the super awesome data mapper in Griffin.Framework
.
Each repository generation creates the following classes/interfaces:
- Repository interface
- Repository
- Entity
As a bonus, you can also generate integration tests in your test project. These test each method in the repository class so that the SQL queries, mappings, etc. work as they should.
Once you are happy, click on the import button. When you do, the extension will download and include all required nuget packages, include some nice helper classes and add .NET assembly references if required.

Now we’ve done our job in delivering the classes. It’s your turn to nurture them and make them beautiful.
Nice Features
Here is a small subset of the functions that we’ve worked with:
- Primary key mapping
- Interprets “
Id
”, “ID
”, “id
”, “[tableName]_id
”, “[tableName]Id
” as you wanted to have a property named “Id
”.
- Naming conventions
- Removes spaces in table and column names
- Converts underscore convention to Pascal case
- Pluralization of namespaces
- Singularization of entity names
- Readable SQL queries
- Spans them over multiple lines
- Uses proper indentation
- Safe SQL queries
- Uses named parameters
- Handles transition from
DBNull
to primitives (i.e. NULL
is replaced with -1
and vice versa)
- Tests
- Centralizes the db connection creation to ease test maintenance
- Factory method to generate a working entity (where foreign key properties are skipped by design)
- Random values are generated for the created entity (upon code generation, i.e.,
static
when the test runs) - Tests for all repository methods.
- Uses
MSTest
/FluentAssertions
for the tests
Generated Code
Below is all code produced for a single entity. It’s provided exactly as the generator produced it.
Interface
namespace DemoApp.Suppliers
{
public interface ISupplierRepository
{
Task CreateAsync(Supplier entity);
Task DeleteAsync(int id);
Task DeleteAsync(Supplier entity);
Task<Supplier> GetAsync(int id);
Task UpdateAsync(Supplier entity);
}
}
Entity
namespace DemoApp.Suppliers
{
public class Supplier
{
public int Id { get; set; }
public string CompanyName { get; set; }
public string ContactName { get; set; }
public string ContactTitle { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string Region { get; set; }
public string PostalCode { get; set; }
public string Country { get; set; }
public string Phone { get; set; }
public string Fax { get; set; }
public string HomePage { get; set; }
}
}
Repository Class
Uses extension methods that are included by the generator.
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Threading.Tasks;
using DemoApp.Shared;
namespace DemoApp.Suppliers
{
public class SupplierRepository : ISupplierRepository
{
private readonly DbTransaction _transaction;
public SupplierRepository(IDbTransaction transaction)
{
if (transaction == null) throw new ArgumentNullException("transaction");
_transaction = transaction as DbTransaction;
if (_transaction == null) throw new NotSupportedException
("Async operations are not declared on the interfaces,
you need to use drivers that inherit the ADO.NET base classes.");
}
public async Task CreateAsync(Supplier entity)
{
if (entity == null) throw new ArgumentNullException("entity");
var cmd = _transaction.Connection.CreateCommand();
try
{
cmd.Transaction = _transaction;
cmd.CommandText = @"INSERT INTO Suppliers (CompanyName, ContactName,
ContactTitle, Address, City, Region, PostalCode, Country, Phone, Fax, HomePage)
VALUES(@CompanyName, @ContactName, @ContactTitle, @Address,
@City, @Region, @PostalCode, @Country, @Phone, @Fax, @HomePage);
SELECT CAST(SCOPE_IDENTITY() AS INT);";
cmd.AddParameter("CompanyName", entity.CompanyName);
cmd.AddParameter("ContactName", entity.ContactName);
cmd.AddParameter("ContactTitle", entity.ContactTitle);
cmd.AddParameter("Address", entity.Address);
cmd.AddParameter("City", entity.City);
cmd.AddParameter("Region", entity.Region);
cmd.AddParameter("PostalCode", entity.PostalCode);
cmd.AddParameter("Country", entity.Country);
cmd.AddParameter("Phone", entity.Phone);
cmd.AddParameter("Fax", entity.Fax);
cmd.AddParameter("HomePage", entity.HomePage);
var id = (System.Int32)await cmd.ExecuteScalarAsync();
entity.Id = id;
}
catch (Exception exception)
{
throw cmd.CreateDataException(exception);
}
finally
{
cmd.Dispose();
}
}
public async Task DeleteAsync(int id)
{
if (id == 0) throw new ArgumentOutOfRangeException
("id", id, "Must be 1 or larger.");
var cmd = _transaction.Connection.CreateCommand();
try
{
cmd.Transaction = _transaction;
cmd.CommandText = @"DELETE FROM Suppliers
WHERE SupplierID = @SupplierID";
cmd.AddParameter("SupplierID", id);
await cmd.ExecuteNonQueryAsync();
}
catch (Exception exception)
{
throw cmd.CreateDataException(exception);
}
finally
{
cmd.Dispose();
}
}
public async Task DeleteAsync(Supplier entity)
{
if (entity == null) throw new ArgumentNullException("entity");
var cmd = _transaction.Connection.CreateCommand();
try
{
cmd.Transaction = _transaction;
cmd.CommandText = @"DELETE FROM Suppliers
WHERE SupplierID = @SupplierID";
cmd.AddParameter("SupplierID", entity.Id);
await cmd.ExecuteNonQueryAsync();
}
catch (Exception exception)
{
throw cmd.CreateDataException(exception);
}
finally
{
cmd.Dispose();
}
}
public async Task<Supplier> GetAsync(int id)
{
if (id == 0) throw new ArgumentOutOfRangeException
("id", id, "Must be 1 or larger.");
var cmd = _transaction.Connection.CreateCommand();
try
{
cmd.Transaction = _transaction;
cmd.CommandText = @"SELECT SupplierID, CompanyName, ContactName,
ContactTitle, Address, City, Region, PostalCode, Country, Phone, Fax, HomePage
FROM Suppliers
WHERE SupplierID = @SupplierID";
cmd.AddParameter("SupplierID", id);
using (var reader = await cmd.ExecuteReaderAsync())
{
if (!await reader.ReadAsync())
throw new EntityNotFoundException(typeof(Supplier),
"Failed to find 'Supplier' with id '" + id + "'.");
var entity = new Supplier();
Map(reader, entity);
return entity;
}
}
catch (Exception exception)
{
throw cmd.CreateDataException(exception);
}
finally
{
cmd.Dispose();
}
}
public async Task UpdateAsync(Supplier entity)
{
var cmd = _transaction.Connection.CreateCommand();
try
{
cmd.Transaction = _transaction;
cmd.CommandText = @"UPDATE Suppliers SET
CompanyName = @CompanyName,
ContactName = @ContactName,
ContactTitle = @ContactTitle,
Address = @Address,
City = @City,
Region = @Region,
PostalCode = @PostalCode,
Country = @Country,
Phone = @Phone,
Fax = @Fax,
HomePage = @HomePage
WHERE SupplierID = @SupplierID";
cmd.AddParameter("SupplierID", entity.Id);
cmd.AddParameter("CompanyName", entity.CompanyName);
cmd.AddParameter("ContactName", entity.ContactName);
cmd.AddParameter("ContactTitle", entity.ContactTitle);
cmd.AddParameter("Address", entity.Address);
cmd.AddParameter("City", entity.City);
cmd.AddParameter("Region", entity.Region);
cmd.AddParameter("PostalCode", entity.PostalCode);
cmd.AddParameter("Country", entity.Country);
cmd.AddParameter("Phone", entity.Phone);
cmd.AddParameter("Fax", entity.Fax);
cmd.AddParameter("HomePage", entity.HomePage);
await cmd.ExecuteNonQueryAsync();
}
catch (Exception exception)
{
throw cmd.CreateDataException(exception);
}
finally
{
cmd.Dispose();
}
}
public async Task<IList<Supplier>> FindAllAsync(QueryOptions queryOptions = null)
{
var cmd = _transaction.Connection.CreateCommand();
try
{
cmd.Transaction = _transaction;
cmd.CommandText = @"SELECT SupplierID, CompanyName, ContactName,
ContactTitle, Address, City, Region, PostalCode, Country, Phone, Fax, HomePage
FROM Suppliers";
if (queryOptions != null)
{
if (!string.IsNullOrEmpty(queryOptions.SortColumn))
{
cmd.CommandText += " ORDER BY " + queryOptions.SortColumn;
if (queryOptions.SortOrder == QuerySortOrder.Descending)
cmd.CommandText += " DESC";
}
if (queryOptions.PageNumber > 0)
{
var startRow = (queryOptions.PageNumber - 1)*queryOptions.PageSize;
cmd.CommandText += string.Format(" OFFSET {0} ROWS FETCH NEXT {1} ROWS ONLY",
startRow,
queryOptions.PageSize);
}
}
return await MapCollectionAsync(cmd);
}
catch (Exception exception)
{
throw cmd.CreateDataException(exception);
}
finally
{
cmd.Dispose();
}
}
private static void Map(IDataRecord record, Supplier dto)
{
dto.Id = (int)record[0];
dto.CompanyName = (string)record[1];
dto.ContactName = record[2] is DBNull ? "" : (string)record[2];
dto.ContactTitle = record[3] is DBNull ? "" : (string)record[3];
dto.Address = record[4] is DBNull ? "" : (string)record[4];
dto.City = record[5] is DBNull ? "" : (string)record[5];
dto.Region = record[6] is DBNull ? "" : (string)record[6];
dto.PostalCode = record[7] is DBNull ? "" : (string)record[7];
dto.Country = record[8] is DBNull ? "" : (string)record[8];
dto.Phone = record[9] is DBNull ? "" : (string)record[9];
dto.Fax = record[10] is DBNull ? "" : (string)record[10];
dto.HomePage = record[11] is DBNull ? "" : (string)record[11];
}
private static async Task<List<Supplier>> MapCollectionAsync(DbCommand cmd)
{
using (var reader = await cmd.ExecuteReaderAsync())
{
var items = new List<Supplier>();
while (await reader.ReadAsync())
{
var item = new Supplier();
Map(reader, item);
items.Add(item);
}
return items;
}
}
}
}
Test Class
using System;
using System.Data;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using FluentAssertions;
using System.Threading.Tasks;
using DemoApp.Suppliers;
namespace DemoApp.Tests.Suppliers
{
[TestClass]
public class SupplierRepositoryTests
{
private IDbConnection _connection;
private IDbTransaction _transaction;
[TestMethod]
public async Task can_insert_and_fetch_a_Row()
{
var entity = CreateCorrectEntity();
var sut = new SupplierRepository(_transaction);
await sut.CreateAsync(entity);
var actual = await sut.GetAsync(entity.Id);
actual.Should().NotBeNull();
}
[TestMethod]
public async Task can_update_a_row()
{
var expected = CreateCorrectEntity();
var sut = new SupplierRepository(_transaction);
await sut.CreateAsync(expected);
expected.CompanyName = "31506ddc59b946af8ed36e351e4e59f325486a06";
await sut.UpdateAsync(expected);
var actual = await sut.GetAsync(expected.Id);
actual.CompanyName.Should().Be(expected.CompanyName);
}
[TestMethod]
public async Task can_delete_row_using_id()
{
var entity = CreateCorrectEntity();
var sut = new SupplierRepository(_transaction);
await sut.CreateAsync(entity);
await sut.DeleteAsync(entity);
Func<Task> actual = async () => await sut.GetAsync(entity.Id);
actual.ShouldThrow<DataException>("because deleted row cannot be found");
}
[TestMethod]
public async Task can_delete_row_using_entity()
{
var entity = CreateCorrectEntity();
var sut = new SupplierRepository(_transaction);
await sut.CreateAsync(entity);
await sut.DeleteAsync(entity);
Func<Task> actual = async () => await sut.GetAsync (entity.Id);
actual.ShouldThrow<DataException>("because deleted row cannot be found");
}
private Supplier CreateCorrectEntity()
{
return new Supplier
{
CompanyName = "Da Griffin Framework!",
ContactName = "Da Griffin Framework!",
ContactTitle = "Da Griffin Framework!",
Address = "Da Griffin Framework!",
City = "dcacbc33100b473",
Region = "9d005e4d58f946d",
PostalCode = "b3f7a5a56f",
Country = "1dafad266674495",
Phone = "Da Griffin Framework!",
Fax = "Da Griffin Framework!",
HomePage = "Da Griffin Framework!",
};
}
[TestInitialize]
public void Init()
{
_connection = ConnectionFactory.Create();
_transaction = _connection.BeginTransaction();
}
[TestCleanup]
public void Cleanup()
{
if (_transaction != null)
{
_transaction.Dispose();
_transaction = null;
}
if (_connection != null)
{
_connection.Dispose();
_connection = null;
}
}
}
}
Action to Take
I’ve created this extension together with Emilie Crocker. This is hopefully just our first collaboration.
Go download the extension. It’s alpha and free.
Do note that it’s currently just for Microsoft SQL Server.
All kind of feedback is most welcome. Just leave a comment on this article.