0

I am trying to convert from EntityFramework Core to Dapper for most queries. I have a sql table (Tree Structure) defined as:

[Table("bma_ec_categories")]
public class Category
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] 
    [Column("category_id")]
    public int CategoryId { get; set; }

    [Column("parent_category_id")]
    public int ParentId { get; set; }

    [Required]
    [Column("category_name")]
    [StringLength(50)]
    public string Name { get; set; }

    public ICollection<Category> Children { get; set; }

    [Column("title")] 
    [StringLength(150)] 
    public string Title { get; set; }

    [Column("description")]
    [StringLength(250)]
    public string Description { get; set; }
    
    [Column("keywords")]
    [StringLength(250)]
    public string Keywords { get; set; }

    [Column("page_content", TypeName = "text")]
    public string PageContent { get; set; }

    [Column("banner_group_id")] 
    public int? BannerGroupId { get; set; }
    
    [Column("inactive")] 
    public bool? Inactive { get; set; }
    
    [Column("issitecategory")] 
    public bool Issitecategory { get; set; }
    
    [Column("sort_order")] 
    public int? SortOrder { get; set; }
}

It is a tree. I also have a dto defined as:

public class CategoryDto
{
    [Key]
    public int CategoryId { get; set; }

    public int ParentId { get; set; }

    public string Name { get; set; }

    public string Title { get; set; }

    public string Description { get; set; }

    public string Keywords { get; set; }

    public string PageContent { get; set; }

    public int? BannerGroupId { get; set; }

    public bool? Inactive { get; set; }

    public bool Issitecategory { get; set; }

    public int? SortOrder { get; set; }

    public ICollection<CategoryDto> Children { get; set; }
}

My Item Service has this Dapper query that I copied for the most part from EF Core:

    public IEnumerable<CategoryDto> GetCategoriesWithDapperUnbuffered()
    {
        var plainResult =  _dapper.GetCategoriesUnbuffered();

        var categoryDtos = plainResult.ToList();

        var lookup = categoryDtos.Where(x => x.ParentId != 0).ToLookup(x => x.ParentId);

        foreach (var c in categoryDtos)
        {
            if (lookup.Contains(c.CategoryId))
            {
                c.Children = lookup[c.CategoryId]
                    .OrderBy(x => x.SortOrder)
                    .ThenBy(x => x.Name)
                    .ToList();
            }
        }

        var dto = categoryDtos
            .Where(x => x.ParentId == 0)
            .OrderBy(x => x.SortOrder)
            .ThenBy(x => x.Name)
            .ToList();

        return dto;
    }

... where GetCategoriesUnbuffered is:

    public IEnumerable<CategoryDto> GetCategoriesUnbuffered()
    {
        string sql = "SELECT * FROM [dbo].[bma_ec_categories]";

        using (DbConnection db = _connectionFactory.CreateConnection())
        {
            var categories = db.Query<CategoryDto>(sql, buffered: false).ToList();

            return categories;
        }
    }

I also have a Dapper Fluent mapping I can show. This query does work. But I am trying to optimize further by using Dapper's Multi-Mapping feature in the original query. Here is what I have so far:

    public IEnumerable<CategoryDto> GetCategoriesMultiMapping()
    {
        string sql = "SELECT * FROM [dbo].[bma_ec_categories] AS [A] INNER JOIN [dbo].[bma_ec_categories] AS [B] ON [A].[category_id] = [B].[parent_category_id];";

        using (DbConnection db = _connectionFactory.CreateConnection())
        {
            var categoryDictionary = new Dictionary<int, CategoryDto>();

            var list = db.Query<CategoryDto, CategoryDto, CategoryDto>(
                    sql,
                    (category, children) =>
                    {
                        if (!categoryDictionary.TryGetValue(category.ParentId, out var categoryEntry))
                        {
                            categoryEntry = category;
                            categoryEntry.Children = new List<CategoryDto>();
                            categoryDictionary.Add(categoryEntry.ParentId, categoryEntry);
                        }

                        categoryEntry.Children.Add(children);
                        return categoryEntry;
                    },
                    splitOn: "category_id,category_id", buffered: false)
                .Distinct()
                .ToList();

            return list;
        }
    }

This returns the first category with all the other categories as its child. I am running into trouble trying to splitOn the second instance of category_id. I tried to adapt the One to Many example from Dapper's Example - Query Multi-Mapping (One to Many). Any help would be greatly appreciated.

Randy
  • 1,137
  • 16
  • 49
  • 2
    I strongly suggest yiou specify only the columns you need, and do not duplicate columns in a Multi-map query – Charlieface Aug 16 '21 at 11:15
  • @Charlieface Do you mean in the `SELECT *` – Randy Aug 16 '21 at 11:27
  • 1
    Yes, specify columns explicitly, see https://sqlblog.org/2009/10/10/bad-habits-to-kick-using-select-omitting-the-column-list – Charlieface Aug 16 '21 at 11:38
  • @Charlieface Thanks, fixed in source. You also said `do not duplicate columns`, what do you suggest I do to avoid duplicating columns in a Tree? – Randy Aug 16 '21 at 11:43

2 Answers2

1

If I understand truly you want to read this table as a tree, one the way is using recursive queries on SQL server which I did.

I used a stored procedure in the same situation, you could use sp or function on SQL server, run command to run your sp.

I create a simple example for you;

 DECLARE @CatTree TABLE(category_id int,parent_category_id int,category_name varchar(50) )

 INSERT INTO @CatTree (category_id,parent_category_id,category_name) 
 VALUES (1,null,'shoes'),(2,1,'outdoor shoes'),(3,1,'winter shoes'),(4,3,'water proof shoes'),(5,3,'snow shoes')




 ;WITH TopCat AS (
                SELECT 
                    CAST(category_id AS VARCHAR(MAX))  ID_Path,
                    parent_category_id,
                    category_id,
                    category_name
                
                FROM @CatTree where parent_category_id is null

            UNION ALL

             SELECT  
                r.ID_Path + ' > ' + CAST(t.category_id AS VARCHAR(MAX)),
                t.parent_category_id,
                t.category_id,
                t.category_name
            
            FROM @CatTree t 
            INNER JOIN TopCat r ON  t.parent_category_id = r.category_id
            ) 
            
  select * from TopCat

enter image description here

Second solition; If you want to create your tree on your backend self referanced tables is your key. You have to add a relation to your model, pointing itself then fluent api or migraiton both is useful

public class Category: BaseEntity
{
    public Guid Id{ get; set; }

    public string Name { get; set; }

    public Guid? TopCategoryId{ get; set; }

    public virtual ICollection<Category> TopCat{ get; set; }
}

this can be wrong I didnt try, I just write on my mind. then you have to add your relation to your data annotions or fluentapi

builder.HasMany(pt => pt.Category).WithOne(p => p.TopCat)

Entity Framework Core self referencing table

  • Thanks for the comment. Just as a note, I have a working EF Core solution. I am trying to get Dapper to work the same way. – Randy Aug 23 '21 at 12:04
  • second solution wast useful, is it? I used same thing on my blog page, there is comments and parent comments. If you answer someone's comment, it will be a parent to your comment. so I self referenced table, with icollection, then I defined with one comment has child comments, each comment has one parent comment – Çağlar Can Sarıkaya Aug 25 '21 at 07:35
0

After a ton of searches, I have concluded this is not what Multi-Mapper was meant to do. However, I did find this answer on SO which solves my actual problem.

In case anyone wants to know here is my benchmark results:

Method Mean Error StdDev StdErr Min Max Q1 Q3 Median Op/s Ratio RatioSD Rank Gen 0 Gen 1 Allocated
'EF Core (async)' 96.87 ms 3.920 ms 11.309 ms 1.154 ms 77.79 ms 126.71 ms 87.62 ms 105.04 ms 95.68 ms 10.32 1.00 0.00 4 - - 979 KB
'EF Core (compiled)' 83.69 ms 1.674 ms 4.910 ms 0.494 ms 73.57 ms 96.66 ms 79.87 ms 86.79 ms 83.52 ms 11.95 0.88 0.11 3 142.8571 - 1,297 KB
'Dapper (async, buffered)' 77.34 ms 1.646 ms 4.828 ms 0.485 ms 65.97 ms 88.00 ms 74.04 ms 80.87 ms 76.98 ms 12.93 0.81 0.12 2 166.6667 - 686 KB
'Dapper (unbuffered)' 66.64 ms 1.322 ms 3.551 ms 0.387 ms 57.91 ms 74.21 ms 64.25 ms 68.98 ms 66.69 ms 15.01 0.70 0.09 1 125.0000 - 593 KB
'Dapper Test (unbuffered)' 66.16 ms 1.322 ms 2.902 ms 0.381 ms 60.57 ms 72.30 ms 63.87 ms 67.97 ms 65.93 ms 15.11 0.67 0.07 1 285.7143 142.8571 1,746 KB
  • Dapper Test is based on the SO answer I linked to.
Randy
  • 1,137
  • 16
  • 49