Пользовательское отображение в Dapper

Я пытаюсь использовать CTE с Dapper и multi-mapping, чтобы получать постраничные результаты. Я столкнулся с неудобством с дублирующимися столбцами; CTE препятствует мне иметь имя столбцов, например.

Я хотел бы отобразить следующий запрос на следующие объекты, а не на несоответствие между именами столбцов и свойствами.

Query:

WITH TempSites AS(
    SELECT
        [S].[SiteID],
        [S].[Name] AS [SiteName],
        [S].[Description],
        [L].[LocationID],
        [L].[Name] AS [LocationName],
        [L].[Description] AS [LocationDescription],
        [L].[SiteID] AS [LocationSiteID],
        [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

Объекты:

public class Site
{
    public int SiteID { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public List<Location> Locations { get; internal set; }
}

public class Location
{
    public int LocationID { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public Guid ReportingID { get; set; }
    public int SiteID { get; set; }
}

По какой-то причине у меня есть в голове, что существует соглашение об именах, которое будет обрабатывать этот сценарий для меня, но я не могу найти упоминание об этом в документах.

Ответ 1

Есть несколько вопросов, пусть они охватывают их один за другим.

Названия дубликатов CTE:

CTE не позволяет дублировать имена столбцов, поэтому вы должны разрешать их с помощью псевдонимов, предпочтительно используя какое-либо соглашение об именах, например, в вашей попытке запроса.

По какой-то причине у меня есть в голове, что существует соглашение об именах, которое будет обрабатывать этот сценарий для меня, но я не могу найти упоминание об этом в документах.

Вероятно, вы имели в виду настройку свойства DefaultTypeMap.MatchNamesWithUnderscores на true, но в качестве документации кода состояния состояния:

Должны ли имена столбцов, такие как User_Id, соответствовать свойствам/полям, например UserId?

очевидно, это не решение. Но проблему можно легко решить, введя условное соглашение об именах, например "{prefix}{propertyName}" (где по умолчанию используется префикс "{className}_") и реализуется через Dapper CustomPropertyTypeMap. Вот вспомогательный метод, который делает это:

public static class CustomNameMap
{
    public static void SetFor<T>(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);
    }
}

Теперь вам нужно только позвонить (один раз):

CustomNameMap.SetFor<Location>();

примените соглашение об именовании к вашему запросу:

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

и вы закончите с этой частью. Конечно, вы можете использовать более короткий префикс типа "Loc_", если хотите.

Сопоставление результата запроса с предоставленными классами:

В этом конкретном случае вам нужно использовать перегрузку метода Query, которая позволяет передать делегат Func<TFirst, TSecond, TReturn> map и унифицировать параметр splitOn, чтобы указать LocationID как разделенный столбец. Однако этого недостаточно. Dapper Функция Multi Mapping позволяет разделить одну строку на несколько одиночных объектов (например, LINQ Join), в то время как вам понадобится Site с Location списком (например, LINQ GroupJoin).

Это может быть достигнуто с помощью метода Query для проецирования во временный анонимный тип, а затем использовать обычный LINQ для создания нужного вывода следующим образом:

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();

где cn открыт SqlConnection, а sql - это string, содержащий вышеуказанный запрос.

Ответ 2

Вы можете сопоставить имя столбца с другим атрибутом, используя ColumnAttributeTypeMapper.

См. мой первый комментарий к Gist для более подробной информации.

Вы можете сделать сопоставление, например

public class Site
{
    public int SiteID { get; set; }
    [Column("SiteName")]
    public string Name { get; set; }
    public string Description { get; set; }
    public List<Location> Locations { get; internal set; }
}

public class Location
{
    public int LocationID { get; set; }
    [Column("LocationName")]
    public string Name { get; set; }
    [Column("LocationDescription")]
    public string Description { get; set; }
    public Guid ReportingID { get; set; }
    [Column("LocationSiteID")]
    public int SiteID { get; set; }
}

Отображение может быть выполнено с использованием одного из следующих 3 методов

Метод 1

Вручную установите собственный TypeMapper для вашей модели один раз:

Dapper.SqlMapper.SetTypeMap(typeof(Site), new ColumnAttributeTypeMapper<Site>());
Dapper.SqlMapper.SetTypeMap(typeof(Location), new ColumnAttributeTypeMapper<Location>());

Метод 2

Для библиотек классов .NET Framework >= v4.0 вы можете использовать PreApplicationStartMethod для регистрации ваших классов для пользовательского сопоставления типов.

using System.Web;
using Dapper;

[assembly: PreApplicationStartMethod(typeof(YourNamespace.Initiator), "RegisterModels")]

namespace YourNamespace
{
    public class Initiator
    {
        private static void RegisterModels()
        {
             SqlMapper.SetTypeMap(typeof(Site), new ColumnAttributeTypeMapper<Site>());
             SqlMapper.SetTypeMap(typeof(Location), new ColumnAttributeTypeMapper<Location>());
             // ...
        }
    }
}

Метод 3

Или вы можете найти классы, к которым применяется ColumnAttribute, путем отражения и сопоставления типов. Это может быть немного медленнее, но он автоматически отображает все ваши сборки в вашей сборке. Просто вызовите RegisterTypeMaps() после загрузки сборки.

    public static void RegisterTypeMaps()
    {
        var mappedTypes = Assembly.GetAssembly(typeof (Initiator)).GetTypes().Where(
            f =>
            f.GetProperties().Any(
                p =>
                p.GetCustomAttributes(false).Any(
                    a => a.GetType().Name == ColumnAttributeTypeMapper<dynamic>.ColumnAttributeName)));

        var mapper = typeof(ColumnAttributeTypeMapper<>);
        foreach (var mappedType in mappedTypes)
        {
            var genericType = mapper.MakeGenericType(new[] { mappedType });
            SqlMapper.SetTypeMap(mappedType, Activator.CreateInstance(genericType) as SqlMapper.ITypeMap);
        }
    }

Ответ 3

Нижеприведенный код должен отлично работать для загрузки списка сайтов со связанными местоположениями

var conString="your database connection string here";
using (var conn =   new SqlConnection(conString))
{
    conn.Open();
    string qry = "SELECT S.SiteId, S.Name, S.Description, L.LocationId,  L.Name,L.Description,
                  L.ReportingId
                  from Site S  INNER JOIN   
                  Location L ON S.SiteId=L.SiteId";
    var sites = conn.Query<Site, Location, Site>
                     (qry, (site, loc) => { site.Locations = loc; return site; });
    var siteCount = sites.Count();
    foreach (Site site in sites)
    {
        //do something
    }
    conn.Close(); 
}