0

I am trying to build a dynamic query string and apply it on dapper (somehow giving a similar "feel" of the Entity Framework optional ".include(x=>x.XXX)" functionality):

Build query string

var query = new StringBuilder();
query.Append(" select * from test_post p ");
if (validIncludesToPerform[0]) 
              query.Append(" left join sys_user u on u.Id = p.CreatorId ");
if (validIncludesToPerform[1]) 
              query.Append(" left join test_poststatus s on s.Id = p.StatusId ");
query.Append(" where p.Id = @Id; ");

var isQueryMultiple = false;
if (validIncludesToPerform[2])
{
    isQueryMultiple = true;
    query.Append(" select * from test_postnote n where n.PostId = @Id; ");
}
if (validIncludesToPerform[3])
{
    isQueryMultiple = true;
    query.Append(@" select * 
                   from test_post_tag pt 
                   left join test_tag t on t.Id = pt.TagId 
                   where pt.PostId = @Id; ");
}

Process query

Post pst;
if (isQueryMultiple)
{
    using (var multi = Connection.QueryMultiple(query.ToString(), new { Id = id }))
    {
        pst = multi.Read<Post, User, PostStatus, Post>((post, user, status) =>
        {
            if (post == null) return null;
            if (user != null) post.Creator = user;
            if (status != null) post.Status = status;
            return post;
        }).FirstOrDefault();

        if (pst != null && validIncludesToPerform[2])
            pst.Notes = multi.Read<PostNote>().ToList();

        if (pst != null && validIncludesToPerform[3])
            pst.PostTags = multi.Read<PostTag, Tag, PostTag>((pTag, tag) =>
                                    {
                                        if (pTag == null) return null;
                                        if (tag != null) pTag.Tag = tag;
                                        return pTag;
                                    }).ToList();
    }
}
else
{
    pst = Connection.Query<Post, User, PostStatus, Post>
                        (query.ToString(), (post, user, status) =>
                        {
                            if (post == null) return null;
                            if (user != null) post.Creator = user;
                            if (status != null) post.Status = status;
                            return post;
                        }, new { Id = id }).FirstOrDefault();
}

Important note: Dapper assumes your Id columns are named "Id" or "id", if your primary key is different or you would like to split the wide row at point other than "Id", use the optional 'splitOn' parameter. This means that in my case I do NOT have to specify "Id,Id" because this part is already implicit.

  • The part that is processing the PostNotes or the PostTags is working fine.
  • The Post when is "including" the User and the Status is working fine as well.
  • If FAILS when I try to load the User "empty" or just with User OR Status...

Error message: "When using the multi-mapping APIs ensure you set the splitOn param if you have keys other than Id\r\nParameter name: splitOn"

Reason: The use of the generics implies that the query is going to receive "3 parts" but it is just receiving a lower number....

Ugly solutions: I know that this would work if I simply would separate the Status and User into each independent query:

if (validIncludesToPerform[0])
   query.Append(@" select u.* from sys_user u right join test_post p on u.Id = p.CreatorId 
                   where p.Id = @Id;");
if (validIncludesToPerform[1])
   query.Append(@" select ps.* from test_poststatus ps right join test_post p on ps.Id = p.StatusId 
                   where p.Id = @Id;");

but since this information is like a 1 to 1 relation, I believe that a single query is better. So, I am trying to avoid using this solution.

I also do NOT want to have a list of all possible combinations of the generics signature like:

  • Connection.Query<Post, User, PostStatus, Post>
  • Connection.Query<Post, User, Post>
  • Connection.Query<Post, PostStatus, Post>
  • Connection.Query<Post>

This approach is simply not friendly as soon as I start adding more 1to1 relations to this class.

How can I fix this?

Dryadwoods
  • 2,875
  • 5
  • 42
  • 72
  • You could have just post the final sql query possibly with the class and sql table definitions. –  Apr 09 '16 at 20:27
  • @yildizm85 which omitted information is lacking from my question that would improve it by providing the full classes/sql tables? If this really would give you extra insights on how you could provide me an answer. Fell free to ask again and I will do it. – Dryadwoods Apr 09 '16 at 20:53
  • At least the section for building the query string is not about the problem. What I'm simply saying is your question should be minimal. See: http://stackoverflow.com/help/mcve –  Apr 09 '16 at 21:13

2 Answers2

0

I was able to find a "somehow" less bad approach:

        var query = new StringBuilder();
        query.Append(" select * from test_post p ");

        query.Append(validIncludesToPerform[0]
            ? " left join sys_user u on u.Id = p.CreatorId "
            : " left join sys_user u on 1 = 0 ");

        query.Append(validIncludesToPerform[1]
            ? " left join test_poststatus s on s.Id = p.StatusId "
            : " left join test_poststatus s on 1 = 0 ");

        query.Append(" where p.Id = @Id; ");

I will leave this answer for now while I wait for better alternatives....

Dryadwoods
  • 2,875
  • 5
  • 42
  • 72
-1

You need to tell dapper where to split on - where the boundary between the objects is in your select statement.That's what your error statement says. So modify your call to something like:

pst = multi.Read<Post, User, PostStatus, Post>((post, user, status) =>
{
   if (post == null) return null;
   if (user != null) post.Creator = user;
   if (status != null) post.Status = status;
   return post;
}, splitOn: "Id,Id").FirstOrDefault();

The split on tell Dapper where to stop mapping into one object and start mapping into the other. Since you have 2 joins you need to have two fields in split on. Make sure those two are placed in a single string separated by comma and that there is no space characters inside the string...

For better explanation see this post:

Correct use of Multimapping in Dapper

Community
  • 1
  • 1
Marko
  • 12,543
  • 10
  • 48
  • 58
  • I appreciate your help, unfortunately your solution does not work and also does not change anything in my code, because your splitOn with "id, Id" is EXACLTY the same as my code when I do not write it, because the official description on Dapper is: Dapper assumes your Id columns are named "Id" or "id", if your primary key is different or you would like to split the wide row at point other than "Id", use the optional 'splitOn' parameter. – Dryadwoods Apr 09 '16 at 18:24
  • I'm not sure if you read my description, but when at least one of the validIncludesToPerform[0] validIncludesToPerform[1] are FALSE, then I have the error message (same message described on my question text. When I try to load all the Post with Creator and with Status, I have NO error and that just makes my point on the use of the splitter (that is redundant in my case). – Dryadwoods Apr 09 '16 at 18:24
  • @Dryadwoods I've read your post and I read your error message which clearly states that there is an issue with split on. Anyway my guess is that you have a arbitrary number of join statements and yet you have a set number of objects being mapped to (3 exactly) so 2 parameters are expected for split on yet number of joins varies based on your conditions. So when number of joins is NOT exactly 2 you have a problem... – Marko Apr 09 '16 at 20:00
  • Exactly, and that reason was also already stated in my question. I know that I could overcome this problem by having multiple conditions and have different "generic signatures: Query OR .Query OR Query OR Query... but this simply do NOT scale well when I have more 1to1 relations. – Dryadwoods Apr 09 '16 at 20:06
  • Why do you have conditional joins why not just have joins or if you are not sure that you have an associated object why not have an outer join? Are you really that concerned with performance? – Marko Apr 09 '16 at 20:09
  • Using Entity Framework as example: there are many reasons on why to use .Include(x=>x.Status) or NOT include. And the idea of this "method" is to offer a single "select" method and pass as parameters which "properties" to "load" as well. – Dryadwoods Apr 09 '16 at 20:12