160

I'm currently using Entity Framework for my db access but want to have a look at Dapper. I have classes like this:

public class Course{
   public string Title{get;set;}
   public IList<Location> Locations {get;set;}
   ...
}

public class Location{
   public string Name {get;set;}
   ...
}

So one course can be taught at several locations. Entity Framework does the mapping for me so my Course object is populated with a list of locations. How would I go about this with Dapper, is it even possible or do I have to do it in several query steps?

Kiquenet
  • 14,494
  • 35
  • 148
  • 243
b3n
  • 3,805
  • 5
  • 31
  • 46

8 Answers8

213

Alternatively, you can use one query with a lookup:

var lookup = new Dictionary<int, Course>();
conn.Query<Course, Location, Course>(@"
    SELECT c.*, l.*
    FROM Course c
    INNER JOIN Location l ON c.LocationId = l.Id                    
    ", (c, l) => {
        Course course;
        if (!lookup.TryGetValue(c.Id, out course))
            lookup.Add(c.Id, course = c);
        if (course.Locations == null) 
            course.Locations = new List<Location>();
        course.Locations.Add(l); /* Add locations to course */
        return course;
     }).AsQueryable();
var resultList = lookup.Values;

See here https://www.tritac.com/blog/dappernet-by-example/

MarredCheese
  • 17,541
  • 8
  • 92
  • 91
Jeroen K
  • 10,258
  • 5
  • 41
  • 40
  • 15
    This saved me a ton of time. One modification I needed that others may need is to include the splitOn: argument since I wasn't using the default "Id". – Bill Sambrone Jan 07 '15 at 16:52
  • 2
    For LEFT JOIN you will get a null item in the location list. Remove them by var items = lookup.Values; items.ForEach(x => x.Locations.RemoveAll(y => y == null)); – Choco Smith Jan 23 '15 at 13:08
  • 1
    I can't compile this unless I have a semicolon at the end of line 1 and remove the comma before the 'AsQueryable()'. I would edit the answer but 62 upvoters before me seemed to think its okay, maybe I'm missing something... – bitcoder Jul 14 '16 at 21:31
  • 1
    @BillSambrone Hi Bill, I have a list of `string` instead of a list of `Location`, what should I split on? – Quentin Aug 26 '16 at 21:28
  • 1
    @BillSambrone Nvm. I figured out. Your comment was helpful. Thanks! – Quentin Aug 26 '16 at 21:46
  • 2
    For LEFT JOIN: Don't need to do another Foreach on it. Just check before adding it: if(l != null) course.Locations.Add(l). – jpgrassi Jul 13 '17 at 14:17
  • 2
    Since you are using a dictionary. Would this be faster if you used QueryMultiple and queried course and location separately then used the same dictionary to assign location to course? It'sessentially the same thing minus the inner join which means sql wont transfer as many bytes? – MIKE May 31 '18 at 17:10
64

Dapper is not a full blown ORM it does not handle magic generation of queries and such.

For your particular example the following would probably work:

Grab the courses:

var courses = cnn.Query<Course>("select * from Courses where Category = 1 Order by CreationDate");

Grab the relevant mapping:

var mappings = cnn.Query<CourseLocation>(
   "select * from CourseLocations where CourseId in @Ids", 
    new {Ids = courses.Select(c => c.Id).Distinct()});

Grab the relevant locations

var locations = cnn.Query<Location>(
   "select * from Locations where Id in @Ids",
   new {Ids = mappings.Select(m => m.LocationId).Distinct()}
);

Map it all up

Leaving this to the reader, you create a few maps and iterate through your courses populating with the locations.

Caveat the in trick will work if you have less than 2100 lookups (Sql Server), if you have more you probably want to amend the query to select * from CourseLocations where CourseId in (select Id from Courses ... ) if that is the case you may as well yank all the results in one go using QueryMultiple

Community
  • 1
  • 1
Sam Saffron
  • 128,308
  • 78
  • 326
  • 506
  • 1
    Thanks for the clarification Sam. Like you described above I'm just running a second query fetching the Locations and manually assigning them to the course. I just wanted to make sure I didn't miss something that would allow me to do it with one query. – b3n Sep 23 '11 at 04:43
  • 3
    Sam, in a ~large application where collections are regularly exposed on domain objects as in the example, *where would you recommend this code be physically located*? (Assuming you would want to consume a similarly fully constructed [Course] entity from *numerous* different places in your code) In the constructor? In a class factory? Somewhere else? – tbone Aug 27 '15 at 17:27
  • I was confused about `CourseLocations`, a table/construct never mentioned in the question. OP lists only 2 entities - is this just assuming there might be an intermediate table? – PandaWood Sep 27 '22 at 07:27
46

No need for lookup Dictionary

var coursesWithLocations = 
    conn.Query<Course, Location, Course>(@"
        SELECT c.*, l.*
        FROM Course c
        INNER JOIN Location l ON c.LocationId = l.Id                    
        ", (course, location) => {
            course.Locations = course.Locations ?? new List<Location>();
            course.Locations.Add(location); 
            return course;
        }).AsQueryable();
MarredCheese
  • 17,541
  • 8
  • 92
  • 91
tchelidze
  • 8,050
  • 1
  • 29
  • 49
  • 4
    This is excellent - this in my opinion should be the selected answer. People doing this, though, watch out for doing * since that can impact performance. – cr1pto Dec 01 '17 at 19:40
  • 1
    @randomus1r, I'm not sure what you mean. My remark is cautioning users from doing SELECT * in general, not saying he is not using SELECT * correctly. – cr1pto May 08 '18 at 21:16
  • 6
    Only problem with this is that you'll be duplicating the header on every Location record. If there are many locations per course, it could be a significant amount of data duplication going across the wire that will increase bandwidth, take longer to parse/map, and use more memory to read all of that. – Daniel Lorenz Aug 20 '18 at 15:03
  • @DanielLorenz - what would the alternative be? Multiple queries? Would you run the query to get locations for each course? Or do you run some operation to build a list of course ID's and pass it into the location query for a join? I much prefer the solution above to load a flat object and nest it as required. This is how Entity Framework also does it's object nesting. – TheCrimsonSpace Nov 28 '18 at 10:58
  • @TheCrimsonSpace QueryMultiple where the second query has 1 more field from the parent to map it. Then we duplicate 1 column instead of all of then. Then when we map it, we use a dictionary to get n log n. I hate how EF duplicates all that data. it has caused me nothing but grief with the amount of data I work with and now actively avoid doing any get queries via EF at all. – Daniel Lorenz Nov 28 '18 at 12:39
  • 21
    i'm not sure this is working as i expected. i have 1 parent object with 3 related objects. the query i use gets three rows back. the first columns describing the parent which are duplicated for each row; the split on id would identify each unique child. my results are 3 duplicate parents with 3 children.... should be one parent with 3 children. – topwik Dec 10 '18 at 21:22
  • 5
    @topwik is right. it doesn't work as expected for me either. – Maciej Pszczolinski Mar 04 '19 at 19:30
  • 6
    I actually ended up with 3 parents, 1 child in each with this code. Not sure why my result is different than @topwik, but still, it does not work. – th3morg Mar 15 '19 at 16:42
  • 1
    now in C# 8.0 you can simplify the assignment to: `course.Locations ??= new List();` – Majid Nov 14 '19 at 18:59
  • 6
    This answer is wrong because returns with one course and 3 locations in the database, would return 3 courses each with one location. – Delphi.Boy May 08 '20 at 18:04
40

I know I'm really late to this, but there is another option. You can use QueryMultiple here. Something like this:

var results = cnn.QueryMultiple(@"
    SELECT * 
      FROM Courses 
     WHERE Category = 1 
  ORDER BY CreationDate
          ; 
    SELECT A.*
          ,B.CourseId 
      FROM Locations A 
INNER JOIN CourseLocations B 
        ON A.LocationId = B.LocationId 
INNER JOIN Course C 
        ON B.CourseId = B.CourseId 
       AND C.Category = 1
");

var courses = results.Read<Course>();
var locations = results.Read<Location>(); //(Location will have that extra CourseId on it for the next part)
foreach (var course in courses) {
   course.Locations = locations.Where(a => a.CourseId == course.CourseId).ToList();
}
Emwat
  • 116
  • 1
  • 15
Daniel Lorenz
  • 4,178
  • 1
  • 32
  • 39
  • 7
    One thing to note. If there are a lot of locations/courses, you should loop through the locations once and put them in a dictionary lookup so you have N log N instead of N^2 speed. Makes a big difference in larger datasets. – Daniel Lorenz Dec 15 '17 at 19:27
10

Sorry to be late to the party (like always). For me, it's easier to use a Dictionary, like Jeroen K did, in terms of performance and readability. Also, to avoid header multiplication across locations, I use Distinct() to remove potential dups:

string query = @"SELECT c.*, l.*
    FROM Course c
    INNER JOIN Location l ON c.LocationId = l.Id";
using (SqlConnection conn = DB.getConnection())
{
    conn.Open();
    var courseDictionary = new Dictionary<Guid, Course>();
    var list = conn.Query<Course, Location, Course>(
        query,
        (course, location) =>
        {
            if (!courseDictionary.TryGetValue(course.Id, out Course courseEntry))
            {
                courseEntry = course;
                courseEntry.Locations = courseEntry.Locations ?? new List<Location>();
                courseDictionary.Add(courseEntry.Id, courseEntry);
            }

            courseEntry.Locations.Add(location);
            return courseEntry;
        },
        splitOn: "Id")
    .Distinct()
    .ToList();

    return list;
}
MarredCheese
  • 17,541
  • 8
  • 92
  • 91
Tena
  • 600
  • 4
  • 14
6

Something is missing. If you do not specify each field from Locations in the SQL query, the object Location cannot be filled. Take a look:

var lookup = new Dictionary<int, Course>()
conn.Query<Course, Location, Course>(@"
    SELECT c.*, l.Name, l.otherField, l.secondField
    FROM Course c
    INNER JOIN Location l ON c.LocationId = l.Id                    
    ", (c, l) => {
        Course course;
        if (!lookup.TryGetValue(c.Id, out course)) {
            lookup.Add(c.Id, course = c);
        }
        if (course.Locations == null) 
            course.Locations = new List<Location>();
        course.Locations.Add(a);
        return course;
     },
     ).AsQueryable();
var resultList = lookup.Values;

Using l.* in the query, I had the list of locations but without data.

MarredCheese
  • 17,541
  • 8
  • 92
  • 91
Eduardo Pires
  • 91
  • 1
  • 7
1

Not sure if anybody needs it, but I have dynamic version of it without Model for quick & flexible coding.

var lookup = new Dictionary<int, dynamic>();
conn.Query<dynamic, dynamic, dynamic>(@"
    SELECT A.*, B.*
    FROM Client A
    INNER JOIN Instance B ON A.ClientID = B.ClientID                
    ", (A, B) => {
        // If dict has no key, allocate new obj
        // with another level of array
        if (!lookup.ContainsKey(A.ClientID)) {
            lookup[A.ClientID] = new {
                ClientID = A.ClientID,
                ClientName = A.Name,                                        
                Instances = new List<dynamic>()
            };
        }

        // Add each instance                                
        lookup[A.ClientID].Instances.Add(new {
            InstanceName = B.Name,
            BaseURL = B.BaseURL,
            WebAppPath = B.WebAppPath
        });

        return lookup[A.ClientID];
    }, splitOn: "ClientID,InstanceID").AsQueryable();

var resultList = lookup.Values;
return resultList;
MarredCheese
  • 17,541
  • 8
  • 92
  • 91
Kiichi
  • 121
  • 2
  • 3
0

There is another approach using the JSON result. Even though the accepted answer and others are well explained, I just thought about an another approach to get the result.

Create a stored procedure or a select qry to return the result in json format. then Deserialize the the result object to required class format. please go through the sample code.

using (var db = connection.OpenConnection())
{                
  var results = await db.QueryAsync("your_sp_name",..);
  var result = results.FirstOrDefault();    
                    
  string Json = result?.your_result_json_row;
                   
  if (!string.IsNullOrEmpty(Json))
  {
     List<Course> Courses= JsonConvert.DeserializeObject<List<Course>>(Json);
  }
    
  //map to your custom class and dto then return the result        
}

This is an another thought process. Please review the same.

AcAnanth
  • 765
  • 3
  • 19
  • 53
  • What about the performance of such approach, compared to multi-mapping and/or using a dictionary for lookup? Let's say I need to fetch a 1000 records from DB, each containing 8 properties and two sub-lists of 2 objects with 3 properties each. Isn't it true that using FOR JSON PATH is rather costly on the DB server (depending ofcourse on the object structure it needs to construct)? – RollerMobster Oct 19 '22 at 19:54