Dapper 中的自定义映射
Custom mapping in Dapper
我正在尝试将 CTE 与 Dapper 和多重映射结合使用来获取分页结果。我对重复的列感到不便;例如,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; }
}
出于某种原因,我认为存在一个命名约定可以为我处理这种情况,但我在文档中找不到提及它。
下面的代码应该可以很好地加载具有相关位置的站点列表
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();
}
问题不止一个,让我们一一介绍。
CTE 重复列名:
CTE 不允许重复的列名,因此您必须使用别名来解析它们,最好使用类似于查询尝试中的命名约定。
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.
您可能想将 DefaultTypeMap.MatchNamesWithUnderscores
属性 设置为 true
,但正如 属性 的代码文档所述:
Should column names like User_Id be allowed to match properties/fields like 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 功能允许您在需要 Site
和 Location
list(类似于 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种方法之一完成映射
方法一
为您的模型手动设置自定义 TypeMapper 一次:
Dapper.SqlMapper.SetTypeMap(typeof(Site), new ColumnAttributeTypeMapper<Site>());
Dapper.SqlMapper.SetTypeMap(typeof(Location), new ColumnAttributeTypeMapper<Location>());
方法二
对于 class .NET Framework >= v4.0 的库,您可以使用 PreApplicationStartMethod 注册您的 classes 以进行自定义类型映射。
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>());
// ...
}
}
}
方法三
或者您可以通过反射和设置类型映射找到应用了 ColumnAttribute 的 classes。这可能会慢一点,但它会自动为您完成程序集中的所有映射。加载程序集后只需调用 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);
}
}
我正在尝试将 CTE 与 Dapper 和多重映射结合使用来获取分页结果。我对重复的列感到不便;例如,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; }
}
出于某种原因,我认为存在一个命名约定可以为我处理这种情况,但我在文档中找不到提及它。
下面的代码应该可以很好地加载具有相关位置的站点列表
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();
}
问题不止一个,让我们一一介绍。
CTE 重复列名:
CTE 不允许重复的列名,因此您必须使用别名来解析它们,最好使用类似于查询尝试中的命名约定。
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.
您可能想将 DefaultTypeMap.MatchNamesWithUnderscores
属性 设置为 true
,但正如 属性 的代码文档所述:
Should column names like User_Id be allowed to match properties/fields like 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 功能允许您在需要 Site
和 Location
list(类似于 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种方法之一完成映射
方法一
为您的模型手动设置自定义 TypeMapper 一次:
Dapper.SqlMapper.SetTypeMap(typeof(Site), new ColumnAttributeTypeMapper<Site>());
Dapper.SqlMapper.SetTypeMap(typeof(Location), new ColumnAttributeTypeMapper<Location>());
方法二
对于 class .NET Framework >= v4.0 的库,您可以使用 PreApplicationStartMethod 注册您的 classes 以进行自定义类型映射。
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>());
// ...
}
}
}
方法三
或者您可以通过反射和设置类型映射找到应用了 ColumnAttribute 的 classes。这可能会慢一点,但它会自动为您完成程序集中的所有映射。加载程序集后只需调用 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);
}
}