Пользовательское сопоставление в Dapper

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

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

запрос:

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; }
}

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

3 ответов


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

CTE дублировать имена столбцов:

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

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

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

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

по-видимому, это не решение. Но проблема может быть легко решена путем введения пользовательского соглашения об именах, например "{prefix}{propertyName}" (где по умолчанию префикс "{className}_") и реализация его через 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 делегат и unitilize в указать LocationID как разделенный столбец. Однако этого недостаточно. 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 удержание вышеуказанного запроса.


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

см. Мой первый комментарий по существу, для получения дополнительной информации.

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

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

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

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