93

I've written this code to project one to many relation but it's not working:

using (var connection = new SqlConnection(connectionString))
{
   connection.Open();

   IEnumerable<Store> stores = connection.Query<Store, IEnumerable<Employee>, Store>
                        (@"Select Stores.Id as StoreId, Stores.Name, 
                                  Employees.Id as EmployeeId, Employees.FirstName,
                                  Employees.LastName, Employees.StoreId 
                           from Store Stores 
                           INNER JOIN Employee Employees ON Stores.Id = Employees.StoreId",
                        (a, s) => { a.Employees = s; return a; }, 
                        splitOn: "EmployeeId");

   foreach (var store in stores)
   {
       Console.WriteLine(store.Name);
   }
}

Can anybody spot the mistake?

EDIT:

These are my entities:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Price { get; set; }
    public IList<Store> Stores { get; set; }

    public Product()
    {
        Stores = new List<Store>();
    }
}

public class Store
{
    public int Id { get; set; }
    public string Name { get; set; }
    public IEnumerable<Product> Products { get; set; }
    public IEnumerable<Employee> Employees { get; set; }

    public Store()
    {
        Products = new List<Product>();
        Employees = new List<Employee>();
    }
}

EDIT:

I change the query to:

IEnumerable<Store> stores = connection.Query<Store, List<Employee>, Store>
        (@"Select Stores.Id as StoreId ,Stores.Name,Employees.Id as EmployeeId,
           Employees.FirstName,Employees.LastName,Employees.StoreId 
           from Store Stores INNER JOIN Employee Employees 
           ON Stores.Id = Employees.StoreId",
         (a, s) => { a.Employees = s; return a; }, splitOn: "EmployeeId");

and I get rid of exceptions! However, Employees are not mapped at all. I am still not sure what problem it had with IEnumerable<Employee> in first query.

Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
TCM
  • 16,780
  • 43
  • 156
  • 254
  • 1
    What do your entities look like? – gideon Feb 19 '12 at 15:34
  • 2
    How is not working ? Are you getting an exception ? Unexpected results ? – driis Feb 19 '12 at 15:43
  • 1
    The error is not meaningful that's why I didn't bother to post it. I get: "{"Value cannot be null.\r\nParameter name: con"}". The line that throws error in SqlMapper is :" il.Emit(OpCodes.Newobj, type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null));" – TCM Feb 19 '12 at 15:47

6 Answers6

175

This post shows how to query a highly normalised SQL database, and map the result into a set of highly nested C# POCO objects.

Ingredients:

  • 8 lines of C#.
  • Some reasonably simple SQL that uses some joins.
  • Two awesome libraries.

The insight that allowed me to solve this problem is to separate the MicroORM from mapping the result back to the POCO Entities. Thus, we use two separate libraries:

Essentially, we use Dapper to query the database, then use Slapper.Automapper to map the result straight into our POCOs.

Advantages

  • Simplicity. Its less than 8 lines of code. I find this a lot easier to understand, debug, and change.
  • Less code. A few lines of code is all Slapper.Automapper needs to handle anything you throw at it, even if we have a complex nested POCO (i.e. POCO contains List<MyClass1> which in turn contains List<MySubClass2>, etc).
  • Speed. Both of these libraries have an extraordinary amount of optimization and caching to make them run almost as fast as hand tuned ADO.NET queries.
  • Separation of concerns. We can change the MicroORM for a different one, and the mapping still works, and vice-versa.
  • Flexibility. Slapper.Automapper handles arbitrarily nested hierarchies, it isn't limited to a couple of levels of nesting. We can easily make rapid changes, and everything will still work.
  • Debugging. We can first see that the SQL query is working properly, then we can check that the SQL query result is properly mapped back to the target POCO Entities.
  • Ease of development in SQL. I find that creating flattened queries with inner joins to return flat results is much easier than creating multiple select statements, with stitching on the client side.
  • Optimized queries in SQL. In a highly normalized database, creating a flat query allows the SQL engine to apply advanced optimizations to the whole which would not normally be possible if many small individual queries were constructed and run.
  • Trust. Dapper is the back end for StackOverflow, and, well, Randy Burden is a bit of a superstar. Need I say any more?
  • Speed of development. I was able to do some extraordinarily complex queries, with many levels of nesting, and the dev time was quite low.
  • Fewer bugs. I wrote it once, it just worked, and this technique is now helping to power a FTSE company. There was so little code that there was no unexpected behavior.

Disadvantages

  • Scaling beyond 1,000,000 rows returned. Works well when returning < 100,000 rows. However, if we are bringing back >1,000,000 rows, in order to reduce the traffic between us and SQL server, we should not flatten it out using inner join (which brings back duplicates), we should instead use multiple select statements and stitch everything back together on the client side (see the other answers on this page).
  • This technique is query oriented. I haven't used this technique to write to the database, but I'm sure that Dapper is more than capable of doing this with some more extra work, as StackOverflow itself uses Dapper as its Data Access Layer (DAL).

Performance Testing

In my tests, Slapper.Automapper added a small overhead to the results returned by Dapper, which meant that it was still 10x faster than Entity Framework, and the combination is still pretty darn close to the theoretical maximum speed SQL + C# is capable of.

In most practical cases, most of the overhead would be in a less-than-optimum SQL query, and not with some mapping of the results on the C# side.

Performance Testing Results

Total number of iterations: 1000

  • Dapper by itself: 1.889 milliseconds per query, using 3 lines of code to return the dynamic.
  • Dapper + Slapper.Automapper: 2.463 milliseconds per query, using an additional 3 lines of code for the query + mapping from dynamic to POCO Entities.

Worked Example

In this example, we have list of Contacts, and each Contact can have one or more phone numbers.

POCO Entities

public class TestContact
{
    public int ContactID { get; set; }
    public string ContactName { get; set; }
    public List<TestPhone> TestPhones { get; set; }
}

public class TestPhone
{
    public int PhoneId { get; set; }
    public int ContactID { get; set; } // foreign key
    public string Number { get; set; }
}

SQL Table TestContact

enter image description here

SQL Table TestPhone

Note that this table has a foreign key ContactID which refers to the TestContact table (this corresponds to the List<TestPhone> in the POCO above).

enter image description here

SQL Which Produces Flat Result

In our SQL query, we use as many JOIN statements as we need to get all of the data we need, in a flat, denormalized form. Yes, this might produce duplicates in the output, but these duplicates will be eliminated automatically when we use Slapper.Automapper to automatically map the result of this query straight into our POCO object map.

USE [MyDatabase];
    SELECT tc.[ContactID] as ContactID
          ,tc.[ContactName] as ContactName
          ,tp.[PhoneId] AS TestPhones_PhoneId
          ,tp.[ContactId] AS TestPhones_ContactId
          ,tp.[Number] AS TestPhones_Number
          FROM TestContact tc
    INNER JOIN TestPhone tp ON tc.ContactId = tp.ContactId

enter image description here

C# code

const string sql = @"SELECT tc.[ContactID] as ContactID
          ,tc.[ContactName] as ContactName
          ,tp.[PhoneId] AS TestPhones_PhoneId
          ,tp.[ContactId] AS TestPhones_ContactId
          ,tp.[Number] AS TestPhones_Number
          FROM TestContact tc
    INNER JOIN TestPhone tp ON tc.ContactId = tp.ContactId";

string connectionString = // -- Insert SQL connection string here.

using (var conn = new SqlConnection(connectionString))
{
    conn.Open();    
    // Can set default database here with conn.ChangeDatabase(...)
    {
        // Step 1: Use Dapper to return the  flat result as a Dynamic.
        dynamic test = conn.Query<dynamic>(sql);

        // Step 2: Use Slapper.Automapper for mapping to the POCO Entities.
        // - IMPORTANT: Let Slapper.Automapper know how to do the mapping;
        //   let it know the primary key for each POCO.
        // - Must also use underscore notation ("_") to name parameters in the SQL query;
        //   see Slapper.Automapper docs.
        Slapper.AutoMapper.Configuration.AddIdentifiers(typeof(TestContact), new List<string> { "ContactID" });
        Slapper.AutoMapper.Configuration.AddIdentifiers(typeof(TestPhone), new List<string> { "PhoneID" });

        var testContact = (Slapper.AutoMapper.MapDynamic<TestContact>(test) as IEnumerable<TestContact>).ToList();      

        foreach (var c in testContact)
        {                               
            foreach (var p in c.TestPhones)
            {
                Console.Write("ContactName: {0}: Phone: {1}\n", c.ContactName, p.Number);   
            }
        }
    }
}

Output

enter image description here

POCO Entity Hierarchy

Looking in Visual Studio, We can see that Slapper.Automapper has properly populated our POCO Entities, i.e. we have a List<TestContact>, and each TestContact has a List<TestPhone>.

enter image description here

Notes

Both Dapper and Slapper.Automapper cache everything internally for speed. If you run into memory issues (very unlikely), ensure that you occasionally clear the cache for both of them.

Ensure that you name the columns coming back, using the underscore (_) notation to give Slapper.Automapper clues on how to map the result into the POCO Entities.

Ensure that you give Slapper.Automapper clues on the primary key for each POCO Entity (see the lines Slapper.AutoMapper.Configuration.AddIdentifiers). You can also use Attributes on the POCO for this. If you skip this step, then it could go wrong (in theory), as Slapper.Automapper would not know how to do the mapping properly.

Update 2015-06-14

Successfully applied this technique to a huge production database with over 40 normalized tables. It worked perfectly to map an advanced SQL query with over 16 inner join and left join into the proper POCO hierarchy (with 4 levels of nesting). The queries are blindingly fast, almost as fast as hand coding it in ADO.NET (it was typically 52 milliseconds for the query, and 50 milliseconds for the mapping from the flat result into the POCO hierarchy). This is really nothing revolutionary, but it sure beats Entity Framework for speed and ease of use, especially if all we are doing is running queries.

Update 2016-02-19

Code has been running flawlessly in production for 9 months. The latest version of Slapper.Automapper has all of the changes that I applied to fix the issue related to nulls being returned in the SQL query.

Update 2017-02-20

Code has been running flawlessly in production for 21 months, and has handled continuous queries from hundreds of users in a FTSE 250 company.

Slapper.Automapper is also great for mapping a .csv file straight into a list of POCOs. Read the .csv file into a list of IDictionary, then map it straight into the target list of POCOs. The only trick is that you have to add a propery int Id {get; set}, and make sure it's unique for every row (or else the automapper won't be able to distinguish between the rows).

Update 2019-01-29

Minor update to add more code comments.

See: https://github.com/SlapperAutoMapper/Slapper.AutoMapper

Contango
  • 76,540
  • 58
  • 260
  • 305
  • Yes, this is working absolutely brilliantly at my workplace. Its blindingly fast, and very easy to update if we want any changes. – Contango Jun 11 '15 at 06:36
  • 1
    I reeeeally don't like the table name prefix convention in all your sql though, it doesn't support something like Dapper's "splitOn"? – tbone Jun 11 '15 at 06:55
  • 3
    This table name convention is required by Slapper.Automapper. Yes, Dapper does have support for mapping straight to POCOs, but I prefer use Slapper.Automapper as the code is so clean and maintainable. – Contango Jun 11 '15 at 07:57
  • 3
    I think I'd use Slapper if you didn't have to alias all the columns - instead, in your example, I'd like to be able to say: , splitOn: "PhoneId" - wouldn't that be quite a bit easier than having to alias everything? – tbone Jun 11 '15 at 12:53
  • 1
    I really like the look of slapper, just wondering if you have tried a left join when a person has no contact numbers? Do you have a good way of dealing with that? – undefined Jul 15 '15 at 22:27
  • It brings back null on a left join id there is nothing there, and its up to the consumer to filter those nulls out. This is a bit of a corner case with the current implementation (if it crashes for you, check the comments on the GitHub source that added which describe the patch). – Contango Jul 15 '15 at 22:54
  • 1
    @tbone splitOn doesnt contain any info about where in your object this element belongs which is why slapper uses a path like this – undefined Jul 15 '15 at 23:56
  • @Contango this issue? https://github.com/randyburden/Slapper.AutoMapper/issues/12 – undefined Jul 15 '15 at 23:59
  • @Luke McGregor Yes, thats the issue. – Contango Jul 16 '15 at 06:53
  • This is the only patch that is needed to run this code in production, this solution has been battle proven for a while now. Someone should submit a push request. – Contango Jul 16 '15 at 06:55
  • Update: its been running under heavy load for 8 months now, and has been working flawlessly. – Contango Nov 16 '15 at 22:38
  • 1
    How can I map IList with Slapper.AutoMapper? – Adam Szabo Jan 17 '16 at 15:22
  • @Adam Szabo Recommend not using interfaces such as IList, instead, use concrete classes such as List. The problem with interfaces is that one interface can be implemented by many concrete classes, so the mapper has no idea which concrete class to match with which interface. WCF has the same issue, before you can serialize any class with an interface in it, you have to add an attribute which points the interface at the concrete class. In general, avoid headaches by using concrete classes for anything we are going to serialize, transfer data in/out of our application, or map. – Contango Jan 18 '16 at 17:40
  • 1
    Good work! I've been using Dapper for awhile and have loved it. However, I have recently needed to do some more complex queries and have been worried about the performance of multiple queries. This post resolved those worries. I should also add that the code I wrote (based on the examples here and on the Slapper website) worked on the 1st try! – Don Rolling Feb 19 '16 at 16:40
  • 1
    Excellent. It is working fine for a regular .net project, but I'm having issues to use Slapper.AutoMapper in a DNX project - it is not prepared for it yet. – Fabricio Mar 01 '16 at 20:19
  • 1
    @The Fabricio If you want to port Slapper.Automapper to .NET Core there is a wizard which can help, see http://dotnet.github.io/porting/. – Contango Mar 01 '16 at 21:43
  • @Contango thx a lot for the reference. I'll take a look on it. – Fabricio Apr 07 '16 at 14:04
  • This doesn't appear to work with Guid IDs - is there any way to make that work with Slapper - or some other technique? – niico Oct 12 '16 at 02:02
  • It definitely works with IDs that are longs or ints. Perhaps this would require an upgrade to Slapper? They do accept pull requests. – Contango Oct 12 '16 at 20:55
  • Can you not achieve this as easily with regular old AutoMapper? – Tobias Mar 25 '17 at 19:50
  • @Tobias No, not easily as far as I know. I've used Automapper a lot in the past, and its not really designed to map a flat table to a nested hierarchy of classes. Be prepared to write a lot of gnarly glue code. If it is possible, let me know - perhaps link to some sample code on GitHub. – Contango Mar 26 '17 at 11:02
  • Not of any use of you are targeting .Net core 2.00 – WernerVA Nov 17 '17 at 12:51
  • I get the following error {"An item with the same key has already been added."} – delwasaf ewrew Feb 10 '18 at 18:00
  • @delwasaf ewrew Try the example given above, then keep adding SQL data until something goes wrong. Im guessing its something to do with a repeated value in one of the key columns. – Contango Feb 11 '18 at 19:02
24

I wanted to keep it as simple as possible, my solution:

public List<ForumMessage> GetForumMessagesByParentId(int parentId)
{
    var sql = @"
    select d.id_data as Id, d.cd_group As GroupId, d.cd_user as UserId, d.tx_login As Login, 
        d.tx_title As Title, d.tx_message As [Message], d.tx_signature As [Signature], d.nm_views As Views, d.nm_replies As Replies, 
        d.dt_created As CreatedDate, d.dt_lastreply As LastReplyDate, d.dt_edited As EditedDate, d.tx_key As [Key]
    from 
        t_data d
    where d.cd_data = @DataId order by id_data asc;

    select d.id_data As DataId, di.id_data_image As DataImageId, di.cd_image As ImageId, i.fl_local As IsLocal
    from 
        t_data d
        inner join T_data_image di on d.id_data = di.cd_data
        inner join T_image i on di.cd_image = i.id_image 
    where d.id_data = @DataId and di.fl_deleted = 0 order by d.id_data asc;";

    var mapper = _conn.QueryMultiple(sql, new { DataId = parentId });
    var messages = mapper.Read<ForumMessage>().ToDictionary(k => k.Id, v => v);
    var images = mapper.Read<ForumMessageImage>().ToList();

    foreach(var imageGroup in images.GroupBy(g => g.DataId))
    {
        messages[imageGroup.Key].Images = imageGroup.ToList();
    }

    return messages.Values.ToList();
}

I still do one call to the database, and while i now execute 2 queries instead of one, the second query is using a INNER join instead of a less optimal LEFT join.

Davy
  • 6,295
  • 5
  • 27
  • 38
  • 6
    I like this approach. Pure dapper and IMHO more understandable mapping. – Avner Nov 19 '18 at 03:43
  • 1
    Seems like this would be easy to put into an extension method that takes a couple albmdas, one for the key selector and one for the child selector. Similar to `.Join(` but produces an object graph instead of flattened result. – AaronLS Jul 16 '19 at 18:55
10

A slight modification of Andrew's answer that utilizes a Func to select the parent key instead of GetHashCode.

public static IEnumerable<TParent> QueryParentChild<TParent, TChild, TParentKey>(
    this IDbConnection connection,
    string sql,
    Func<TParent, TParentKey> parentKeySelector,
    Func<TParent, IList<TChild>> childSelector,
    dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null)
{
    Dictionary<TParentKey, TParent> cache = new Dictionary<TParentKey, TParent>();

    connection.Query<TParent, TChild, TParent>(
        sql,
        (parent, child) =>
            {
                if (!cache.ContainsKey(parentKeySelector(parent)))
                {
                    cache.Add(parentKeySelector(parent), parent);
                }

                TParent cachedParent = cache[parentKeySelector(parent)];
                IList<TChild> children = childSelector(cachedParent);
                children.Add(child);
                return cachedParent;
            },
        param as object, transaction, buffered, splitOn, commandTimeout, commandType);

    return cache.Values;
}

Example usage

conn.QueryParentChild<Product, Store, int>("sql here", prod => prod.Id, prod => prod.Stores)
Clay
  • 10,885
  • 5
  • 47
  • 44
  • One thing to note with this solution, your parent class is responsible for instantiating the child property. `class Parent { public List Children { get; set; } public Parent() { this.Children = new List(); } }` – Clay Mar 14 '14 at 21:00
  • 1
    This solution is excellent and worked for us. I did have to add in a check with the children.add to check for null in case there were no child rows returned. – tlbignerd Mar 07 '17 at 16:31
  • This is very interesting... if the parent class doesn't instantiate the `Children`, then `Children` will always be null, even if you instantiate it inside through the `children` variable. `children` should be a reference to `Parent.Children` and hence whatever change made to `children` should affect `Parent.Children` or am I missing something? – Jonas Stawski Mar 27 '21 at 04:16
8

According to this answer there is no one to many mapping support built into Dapper.Net. Queries will always return one object per database row. There is an alternative solution included, though.

Community
  • 1
  • 1
Damir Arh
  • 17,637
  • 2
  • 45
  • 83
  • I am sorry but I don't understand how do I use that in my query? It is trying to query the database 2 times without joins (and using a hardcoded 1 in example). The example only has 1 main entity being returned which in turn contains child entities. In my case I want to project join (list that internally contains list). How do I do that with the link you mentioned? In the link where the line says: `(contact, phones) => { contact.Phones = phones; } ` I would have to write a filter for phones whose contactid matches contact's contactid. This is pretty inefficient. – TCM Feb 19 '12 at 16:19
  • @Anthony Take a look at Mike's answer. He executes a single query with two result sets and joins them afterwards with the Map method. Of course, you don't need to hardcode the value in your case. I'll try to put together an example in a couple of hours. – Damir Arh Feb 19 '12 at 16:27
  • 1
    alright I got it working finally. Thanks! Don't know how this would impact performance of querying database 2 times what could be accomplished using a single join. – TCM Feb 19 '12 at 16:46
  • 2
    Also I don't understand what changes I would need to make if there had been 3 tables :p – TCM Feb 19 '12 at 16:51
  • 1
    this totally sucks.. why on earth avoid joins ? – GorillaApe Oct 28 '12 at 11:46
  • Because a join always implies that we are repeating data and there is redundant information coming back from the query. If we have an extremely fast LAN and we are running a Line of Business (LOB) app over the local network, then this is not a problem. We let SQL server do the join, and pipe the data back to the client. However, if we are running over the internet, where bandwidth is extremely precious, we dont want any duplicated data, so we do the joins on the client side. Dapper does not like joins on the database side, because it was writen for StackOverflow which is internet based. – Contango Jun 11 '15 at 22:16
6

Here is another method:

Order (one) - OrderDetail (many)

using (var connection = new SqlCeConnection(connectionString))
{           
    var orderDictionary = new Dictionary<int, Order>();

    var list = connection.Query<Order, OrderDetail, Order>(
        sql,
        (order, orderDetail) =>
        {
            Order orderEntry;

            if (!orderDictionary.TryGetValue(order.OrderID, out orderEntry))
            {
                orderEntry = order;
                orderEntry.OrderDetails = new List<OrderDetail>();
                orderDictionary.Add(orderEntry.OrderID, orderEntry);
            }

            orderEntry.OrderDetails.Add(orderDetail);
            return orderEntry;
        },
        splitOn: "OrderDetailID")
    .Distinct()
    .ToList();
}

Source: http://dapper-tutorial.net/result-multi-mapping#example---query-multi-mapping-one-to-many

Exocomp
  • 1,477
  • 2
  • 20
  • 29
3

Here is a crude workaround

    public static IEnumerable<TOne> Query<TOne, TMany>(this IDbConnection cnn, string sql, Func<TOne, IList<TMany>> property, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null)
    {
        var cache = new Dictionary<int, TOne>();
        cnn.Query<TOne, TMany, TOne>(sql, (one, many) =>
                                            {
                                                if (!cache.ContainsKey(one.GetHashCode()))
                                                    cache.Add(one.GetHashCode(), one);

                                                var localOne = cache[one.GetHashCode()];
                                                var list = property(localOne);
                                                list.Add(many);
                                                return localOne;
                                            }, param as object, transaction, buffered, splitOn, commandTimeout, commandType);
        return cache.Values;
    }

its by no means the most efficient way, but it will get you up and running. I'll try and optimise this when i get a chance.

use it like this:

conn.Query<Product, Store>("sql here", prod => prod.Stores);

bear in mind your objects need to implement GetHashCode, perhaps like this:

    public override int GetHashCode()
    {
        return this.Id.GetHashCode();
    }
Andrew Bullock
  • 36,616
  • 34
  • 155
  • 231
  • 12
    The cache implementation is flawed. Hash codes aren't unique - two objects can have the same hash code. This can lead to an objects list being filled with items that belong to another object.. – stmax Nov 13 '12 at 21:14