Playing around with Dapper, I'm quite pleased with the results so far - intriguing!
But now, my next scenario would be to read data from two tables - a Student
and an Address
table.
Student
table has a primary key of StudentID (INT IDENTITY)
, Address
has an AddressID (INT IDENTITY)
. Student
also has an FK called AddressID
linking into the Address
table.
My idea was to create two classes, one for each table, with the properties I'm interested in. Additionally, I put an PrimaryAddress
property of type Address
onto my Student
class in C#.
I then tried to retrieve both student and address data in a single query - I mimick the sample that's given on the Github page:
var data = connection.Query<Post, User>(sql, (post, user) => { post.Owner = user; });
var post = data.First();
Here, a Post
and a User
are retrieved, and the owner of the post is set to the user - the type returned is a Post
- correct?
So in my code, I define two parameters to the generic Query
extension method - a Student
as the first which should be returned, and an Address
as the second, which will be stored onto the student instance:
var student = _conn.Query<Student, Address>
("SELECT s.*, a.* FROM dbo.Student s
INNER JOIN dbo.Address a ON s.AddressID = a.AddressID
WHERE s.StudentenID = @Id",
(stu, adr) => { stu.PrimaryAddress = adr; },
new { Id = 4711 });
Trouble is - I get an error in Visual Studio:
Using the generic method 'Dapper.SqlMapper.Query(System.Data.IDbConnection, string, System.Func, dynamic, System.Data.IDbTransaction, bool, string, int?, System.Data.CommandType?)' requires 6 type arguments
I don't really understand why Dapper insists on using this overload with 6 type arguments...
That would be cause I changed APIs and forgot to update the documentation, I corrected the error.
Be sure to have a look at Tests.cs for a full up-to-date spec.
In particular, the old API used to take in an Action<T,U>
to perform the mapping, the trouble was that it felt both arbitrary and inflexible. You could not fully control the return type. The new APIs take in a Func<T,U,V>
. So you can control the type you get back from the mapper and it does not need to be a mapped type.
I just tied up some additional flexibility around multi mapping, this test should make it clear:
class Person
{
public int PersonId { get; set; }
public string Name { get; set; }
}
class Address
{
public int AddressId { get; set; }
public string Name { get; set; }
public int PersonId { get; set; }
}
class Extra
{
public int Id { get; set; }
public string Name { get; set; }
}
public void TestFlexibleMultiMapping()
{
var sql =
@"select
1 as PersonId, 'bob' as Name,
2 as AddressId, 'abc street' as Name, 1 as PersonId,
3 as Id, 'fred' as Name
";
var personWithAddress = connection.Query<Person, Address, Extra, Tuple<Person, Address,Extra>>
(sql, (p,a,e) => Tuple.Create(p, a, e), splitOn: "AddressId,Id").First();
personWithAddress.Item1.PersonId.IsEqualTo(1);
personWithAddress.Item1.Name.IsEqualTo("bob");
personWithAddress.Item2.AddressId.IsEqualTo(2);
personWithAddress.Item2.Name.IsEqualTo("abc street");
personWithAddress.Item2.PersonId.IsEqualTo(1);
personWithAddress.Item3.Id.IsEqualTo(3);
personWithAddress.Item3.Name.IsEqualTo("fred");
}
Dapper pipes all the multi mapping APIs through a single method, so if something fails it will end up in the 6 param one. The other piece of the puzzle was that I did not allow for some super flexible splits, which I just added.
Note, the splitOn
param will default to Id
, meaning it will take a column called id
or Id
as the first object boundary. However if you need boundaries on multiple primary keys that have different names for say a "3 way" multi mapping, you can now pass in a comma separated list.
So if we were to fix the above, probably the following would work:
var student = _conn.Query<Student,Address,Student>
("SELECT s.*, a.* FROM dbo.Student s
INNER JOIN dbo.Address a ON s.AddressID = a.AddressID
WHERE s.StudentenID = @Id",
(stu, adr) => { stu.PrimaryAddress = adr; return stu;},
new { Id = 4711 }, splitOn: "AddressID").FirstOrDefault();