Custom mapping in Dapper

后端 未结 3 926
别跟我提以往
别跟我提以往 2021-02-04 12:03

I\'m attempting to use a CTE with Dapper and multi-mapping to get paged results. I\'m hitting an inconvenience with duplicate columns; the CTE is preventing me from having to Na

3条回答
  •  不要未来只要你来
    2021-02-04 12:40

    There are more than one issues, let cover them one by one.

    CTE duplicate column names:

    CTE does not allow duplicate column names, so you have to resolve them using aliases, preferably using some naming convention like in your query attempt.

    For some reason I have it in my head that a naming convention exists which will handle this scenario for me but I can't find mention of it in the docs.

    You probably had in mind setting the DefaultTypeMap.MatchNamesWithUnderscores property to true, but as code documentation of the property states:

    Should column names like User_Id be allowed to match properties/fields like UserId?

    apparently this is not the solution. But the issue can easily be solved by introducing a custom naming convention, for instance "{prefix}{propertyName}" (where by default prefix is "{className}_") and implementing it via Dapper's CustomPropertyTypeMap. Here is a helper method which does that:

    public static class CustomNameMap
    {
        public static void SetFor(string prefix = null)
        {
            if (prefix == null) prefix = typeof(T).Name + "_";
            var typeMap = new CustomPropertyTypeMap(typeof(T), (type, name) =>
            {
                if (name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
                    name = name.Substring(prefix.Length);
                return type.GetProperty(name);
            });
            SqlMapper.SetTypeMap(typeof(T), typeMap);
        }
    }
    

    Now all you need is to call it (one time):

    CustomNameMap.SetFor();
    

    apply the naming convention to your query:

    WITH TempSites AS(
        SELECT
            [S].[SiteID],
            [S].[Name],
            [S].[Description],
            [L].[LocationID],
            [L].[Name] AS [Location_Name],
            [L].[Description] AS [Location_Description],
            [L].[SiteID] AS [Location_SiteID],
            [L].[ReportingID]
        FROM (
            SELECT * FROM [dbo].[Sites] [1_S]
            WHERE [1_S].[StatusID] = 0
            ORDER BY [1_S].[Name]
            OFFSET 10 * (1 - 1) ROWS
            FETCH NEXT 10 ROWS ONLY
        ) S
            LEFT JOIN [dbo].[Locations] [L] ON [S].[SiteID] = [L].[SiteID]
    ),
    MaxItems AS (SELECT COUNT(SiteID) AS MaxItems FROM Sites)
    
    SELECT *
    FROM TempSites, MaxItems
    

    and you are done with that part. Of course you can use shorter prefix like "Loc_" if you like.

    Mapping the query result to the provided classes:

    In this particular case you need to use the Query method overload that allows you to pass Func map delegate and unitilize the splitOn parameter to specify LocationID as a split column. However that's not enough. Dapper's Multi Mapping feature allows you to split a single row to a several single objects (like LINQ Join) while you need a Site with Location list (like LINQ GroupJoin).

    It can be achieved by using the Query method to project into a temporary anonymous type and then use regular LINQ to produce the desired output like this:

    var sites = cn.Query(sql, (Site site, Location loc) => new { site, loc }, splitOn: "LocationID")
        .GroupBy(e => e.site.SiteID)
        .Select(g =>
        {
            var site = g.First().site;
            site.Locations = g.Select(e => e.loc).Where(loc => loc != null).ToList();
            return site;
        })
        .ToList();
    

    where cn is opened SqlConnection and sql is a string holding the above query.

提交回复
热议问题