web-dev-qa-db-fra.com

Mappage personnalisé dans Dapper

J'essaie d'utiliser un CTE avec Dapper et multi-mapping pour obtenir des résultats paginés. Je rencontre un inconvénient avec les colonnes en double; le CTE m'empêche d'avoir à nommer des colonnes par exemple.

Je voudrais mapper la requête suivante sur les objets suivants, pas la non-concordance entre les noms de colonne et les propriétés.

Requete:

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

Objets:

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

Pour une raison quelconque, je pense qu'il existe une convention de dénomination qui gérera ce scénario pour moi, mais je ne trouve pas de mention dans les documents.

10
Ant Swift

Il y a plus d'un problème, laissez-les couvrir un par un.

Noms des colonnes en double CTE:

CTE n'autorise pas les noms de colonne en double, vous devez donc les résoudre à l'aide d'alias, de préférence en utilisant une convention de dénomination comme dans votre tentative de requête.

Pour une raison quelconque, je pense qu'il existe une convention de dénomination qui gérera ce scénario pour moi, mais je ne trouve pas de mention dans les documents.

Vous aviez probablement en tête de définir le DefaultTypeMap.MatchNamesWithUnderscores propriété à true, mais comme la documentation de code de la propriété indique:

Les noms de colonnes comme User_Id devraient-ils être autorisés à correspondre à des propriétés/champs comme UserId?

apparemment, ce n'est pas la solution. Mais le problème peut être facilement résolu en introduisant une convention de dénomination personnalisée, par exemple "{prefix}{propertyName}" (où le préfixe par défaut est "{className}_") et l'implémenter via Dapper CustomPropertyTypeMap . Voici une méthode d'aide qui fait cela:

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

Maintenant, il vous suffit de l'appeler (une fois):

CustomNameMap.SetFor<Location>();

appliquez la convention de dénomination à votre requête:

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

et vous avez terminé avec cette partie. Bien sûr, vous pouvez utiliser un préfixe plus court comme "Loc_" si vous le souhaitez.

Mappage du résultat de la requête aux classes fournies:

Dans ce cas particulier, vous devez utiliser la surcharge de méthode Query qui vous permet de passer Func<TFirst, TSecond, TReturn> map délègue et unitilise le paramètre splitOn pour spécifier LocationID comme colonne fractionnée. Mais cela ne suffit pas. La fonction Multi Mapping de Dapper vous permet de diviser une seule ligne en plusieurs objets uniques (comme LINQ Join ) alors que vous avez besoin d'une liste Site avec Location (comme LINQ GroupJoin).

Cela peut être réalisé en utilisant la méthode Query pour projeter dans un type anonyme temporaire, puis utiliser LINQ normal pour produire la sortie souhaitée comme ceci:

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 est ouvert SqlConnection et sql est un string contenant la requête ci-dessus.

18
Ivan Stoev

Vous pouvez mapper un nom de colonne avec un autre attribut à l'aide de ColumnAttributeTypeMapper .

Voir mon premier commentaire sur le Gist pour plus de détails.

Vous pouvez faire le mappage comme

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

Le mappage peut être effectué à l'aide de l'une des 3 méthodes suivantes

Méthode 1

Définissez manuellement le TypeMapper personnalisé pour votre modèle comme:

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

Méthode 2

Pour les bibliothèques de classes de .NET Framework> = v4.0, vous pouvez utiliser PreApplicationStartMethod pour enregistrer vos classes pour le mappage de type personnalisé.

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

Méthode 3

Ou vous pouvez trouver les classes auxquelles ColumnAttribute est appliqué via la réflexion et les mappages de types définis. Cela peut être un peu plus lent, mais il effectue automatiquement tous les mappages de votre assemblage pour vous. Appelez simplement RegisterTypeMaps() une fois votre assembly chargé.

    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);
        }
    }
5
Sen Jacob

Le code ci-dessous devrait fonctionner correctement pour charger une liste de sites avec des emplacements associés

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