A certain David McSorley on twitter told me had written a better dapper. One that could load collections which is something dapper doesn’t do.

The project is called Gallus and is on github. But not yet on nuget.

So I set out and tried it.

Being that it is not on nuget and thus it has no compiled version of it and I work with VB.Net I had to create a second project, namely a C# class library. And then I just copy-pasted the code in this class to that project. And I referenced that project in my VB.Net project. So now that we got it setup we can start playing around with it.

Since the difference with dapper is rather small I just tried the collection mapping for now.

Here is a piece of code I used to setup the database, which is sqlserver.

Imports Dapper
Imports System.Data.SqlClient

Public Module SetupDatabase

    Function Setup(withTestData As Boolean) As IDbConnection
        Using db1 = New SqlConnection("Data Source=localhost;Initial Catalog=master;Integrated Security=True")
            db1.Open()
            CreateDatabase(db1)
        End Using
        Dim db = New SqlConnection("Data Source=localhost;Initial Catalog=test;Integrated Security=True")
        db.Open()
        CreateTables(db)
        If withTestData Then InsertTestData(db)
        Return db
    End Function

    Private Sub InsertTestData(ByVal db As IDbConnection)
        Using tran = db.BeginTransaction
            db.Execute("Insert into address (street, housenumber) values (@street,@housenumber)", New With {.street = "street1", .housenumber = "1"}, tran)
            db.Execute("Insert into address (street, housenumber) values (@street,@housenumber)", New With {.street = "street2", .housenumber = "2"}, tran)
            db.Execute("Insert into person (lastname, firstname, addressid) values (@lastname,@firstname,@addressid)", New With {.lastname = "lastname1", .firstname = "firstname1", .addressid = 1}, tran)
            db.Execute("Insert into person (lastname, firstname, addressid) values (@lastname,@firstname,@addressid)", New With {.lastname = "lastname1", .firstname = "firstname2", .addressid = 1}, tran)
            db.Execute("Insert into person (lastname, firstname, addressid) values (@lastname,@firstname,@addressid)", New With {.lastname = "lastname2", .firstname = "firstname3", .addressid = 2}, tran)
            db.Execute("Insert into BadHabit (BadHabit) values (@BadHabit)", New With {.BadHabit = "Drinks"}, transaction:=tran)
            db.Execute("Insert into BadHabit (BadHabit) values (@BadHabit)", New With {.BadHabit = "Smokes"}, transaction:=tran)
            db.Execute("Insert into BadHabit (BadHabit) values (@BadHabit)", New With {.BadHabit = "Eats to much"}, transaction:=tran)
            db.Execute("Insert into BadHabitPerson (BadHabitId, PersonId) values (@BadHabitid, @PersonId)", New With {.BadHabitId = 1, .PersonId = 1}, tran)
            db.Execute("Insert into BadHabitPerson (BadHabitId, PersonId) values (@BadHabitid, @PersonId)", New With {.BadHabitId = 2, .PersonId = 1}, tran)
            db.Execute("Insert into BadHabitPerson (BadHabitId, PersonId) values (@BadHabitid, @PersonId)", New With {.BadHabitId = 3, .PersonId = 2}, tran)
            db.Execute("Insert into BadHabitPerson (BadHabitId, PersonId) values (@BadHabitid, @PersonId)", New With {.BadHabitId = 3, .PersonId = 1}, tran)
            db.Execute("Insert into BadHabitPerson (BadHabitId, PersonId) values (@BadHabitid, @PersonId)", New With {.BadHabitId = 3, .PersonId = 3}, tran)
            tran.Commit()
        End Using
    End Sub

    Private Sub CreateDatabase(db As IDbConnection)
        db.Execute("IF EXISTS(select * from sys.databases where name='test') DROP DATABASE test")
        db.Execute("CREATE DATABASE test")
    End Sub

    Private Sub CreateTables(db As IDbConnection)
        Using tran = db.BeginTransaction
            db.Execute("CREATE TABLE Address (Id int IDENTITY(1,1) PRIMARY KEY, Street nvarchar (40) NOT NULL, HouseNumber nvarchar (10))", transaction:=tran)
            db.Execute("CREATE TABLE Person (Id int IDENTITY(1,1) PRIMARY KEY, LastName nvarchar (40) NOT NULL, FirstName nvarchar (40), AddressId int NOT NULL)", transaction:=tran)
            db.Execute("ALTER TABLE Person ADD CONSTRAINT FK_Person_Address FOREIGN KEY (AddressId) REFERENCES Address(Id)", transaction:=tran)
            db.Execute("CREATE TABLE BadHabit (Id int IDENTITY(1,1) PRIMARY KEY, BadHabit nvarchar (40) NOT NULL)", transaction:=tran)
            db.Execute("CREATE TABLE BadHabitPerson (BadHabitId int NOT NULL, PersonId int NOT NULL)", transaction:=tran)
            db.Execute("ALTER TABLE BadHabitPerson ADD PRIMARY KEY(BadHabitId, PersonId)", transaction:=tran)
            db.Execute("ALTER TABLE BadHabitPerson ADD CONSTRAINT FK_BadHabitPerson_Person FOREIGN KEY (PersonId) REFERENCES Person(Id)", transaction:=tran)
            db.Execute("ALTER TABLE BadHabitPerson ADD CONSTRAINT FK_BadHabitPerson_BadHabit FOREIGN KEY (BadHabitId) REFERENCES BadHabit(Id)", transaction:=tran)
            tran.Commit()
        End Using
    End Sub

End Module

The above is there because I like you all, remember that.

And here is the model.

Public Class Person
        Public Property LastName As String
        Public Property FirstName As String
        Public Property Address As Address
        Public Property BadHabits As IList(Of BadHabit)
    End Class

    Public Class BadHabit
        Public BadHabit As String
    End Class

    Public Class Address
        Public Property Street As String
        Public Property HouseNumber As String
    End Class

And now for the interesting part.

This is how I would fill those objects in dapper.

Imports Dapper

Module Module1

    Sub Main()
        Dim db = SetupDatabase.Setup(True)
        Dim sql1 = "select * from person p left join address a on p.addressid = a.id where p.id = @personid" & ControlChars.CrLf
        sql1 &= "select * from badhabit b join badhabitperson bp on b.id = bp.badhabitid where bp.personid = @personid"
        Dim multi = db.QueryMultiple(sql1, New With {.personid = 1})
        Dim person = multi.Read(Of Person, Address, Person)(Function(p, a)
                                                                p.Address = a
                                                                Return p
                                                            End Function).SingleOrDefault
        person.BadHabits = multi.Read(Of BadHabit).ToList
        Console.WriteLine(Person.LastName)
        Console.WriteLine(Person.FirstName)
        Console.WriteLine(Person.Address.Street)
        Console.WriteLine(Person.Address.HouseNumber)
        Console.WriteLine(Person.BadHabits.Count)
        Console.ReadLine()
    End Sub
End Module

You should notice I am using the QueryMultiple and the mapping for address and then the second read to get the list of badhabits. That is one transaction and one connection to the database. Not counting the setup of the testdata of course.

Let me cut that down for you to make it less cluttered.

Dim sql1 = "select * from person p left join address a on p.addressid = a.id where p.id = @personid" & ControlChars.CrLf
        sql1 &= "select * from badhabit b join badhabitperson bp on b.id = bp.badhabitid where bp.personid = @personid"
        Dim multi = db.QueryMultiple(sql1, New With {.personid = 1})
        Dim person = multi.Read(Of Person, Address, Person)(Function(p, a)
                                                                p.Address = a
                                                                Return p
                                                            End Function).SingleOrDefault
        person.BadHabits = multi.Read(Of BadHabit).ToList

Now let’s compare that with Gallus.

Dim sql2 = "select * from person p left join address a on p.addressid = a.id join badhabitperson bp on p.id = bp.personid join badhabit b on b.id = bp.badhabitid where p.id = @personid"
        Dim person = db.Query(Of Person, Address, IList(Of BadHabit))(sql3, Sub(p, a, b)
                                                                                p.Address = a
                                                                                p.BadHabits = b
                                                                            End Sub, New With {.personid = 1}).SingleOrDefault

That’s a lot shorter. Just one query not 2. And the lambda is an action so it needs one less generic parameter than dapper needs.

So maybe something to look out for if that is one of your problems with dapper.

Is this enough reason for me to switch? Not just yet, since that is not one of the features I use in dapper on a daily basis.