Dapper: Mapping dynamic pivot columns from stored

2019-04-11 12:40发布

问题:

My stored procedure is returning dynamic pivot columns as available. I am using the SQLMapper and COlumnTypeAttribute, But in the results I can only see the first column and its value, but the dynamic pivot column(s) and their values(s) are empty.

the sample data could look like

The first column is fixed, Rest of the columns are pivot columns.

TSBNumber      SystemXY   SystemBB   SystemTT
asdas          1/1/2013    
1231                      1/1/2014
12312          1/1/2013
ASAWS                     1/1/2013
awsdS                               1/1/2013

Store Procedure

DECLARE @PivotColumnHeaders NVARCHAR(MAX)

SELECT @PivotColumnHeaders =  
   COALESCE(
     @PivotColumnHeaders + ',[' +  cast(SystemFullName as Nvarchar) + ']',
     '[' + cast(SystemFullName as varchar)+ ']'
   )
FROM System
WHERE (@SelectedSystemIDs IS NULL OR  System.ID IN(select  * from dbo.SplitInts_RBAR_1(@SelectedSystemIDs, ',')))     
AND ((@PlatformID IS NULL) OR  (System.PlatformID = @PlatformID) OR (@PlatformID = 12 AND System.PlatformID <= 2))  

DECLARE @PivotTableSQL NVARCHAR(MAX)
SET @PivotTableSQL = N'
   SELECT *
   FROM (
     SELECT
       TSBNumber [TSBNumber],
       SystemFullName,
       ClosedDate
     FROM ServiceEntry 
     INNER JOIN System 
       ON ServiceEntry.SystemID = System.ID
     where
      (ServiceEntry.TSBNumber IS NOT NULL)
       AND 
       (ServiceEntry.ClosedDate IS NOT NULL)
       AND
       ( 
       (''' + @SelectedTsbIDs + ''' = '''+ '0' + ''') OR
         (ServiceEntry.TSBNumber in (select * from dbo.SplitStrings_Moden(''' + @SelectedTsbIDs + ''', ''' + ',' + ''')))
        )  
   ) AS PivotData
   PIVOT (
     MAX(ClosedDate)
     FOR SystemFullName IN (
       ' + @PivotColumnHeaders + '
     ) 
   ) AS PivotTable
' 
EXECUTE (@PivotTableSQL)

ColumnAttributeTypeManager

namespace RunLog.Domain.Exports
{
    /// <summary>
    /// Uses the Name value of the ColumnAttribute specified, otherwise maps as usual.
    /// </summary>
    /// <typeparam name="T">The type of the object that this mapper applies to.</typeparam>
    public class ColumnAttributeTypeMapper<T> : FallbackTypeMapper
    {
        public static readonly string ColumnAttributeName = "ColumnAttribute";

        public ColumnAttributeTypeMapper()
            : base(new SqlMapper.ITypeMap[]
{
new CustomPropertyTypeMap(
typeof(T),
(type, columnName) =>
type.GetProperties().FirstOrDefault(prop =>
prop.GetCustomAttributes(false)
.OfType<ColumnAttribute>()
.Any(attr => attr.Name == columnName)
)
),
new DefaultTypeMap(typeof(T))
})
        {
        }
        //public ColumnAttributeTypeMapper()
        //    : base(new SqlMapper.ITypeMap[]
        //    {
        //        new CustomPropertyTypeMap(typeof (T), SelectProperty),
        //        new DefaultTypeMap(typeof (T))
        //    })
        //{
        //}

        //private static PropertyInfo SelectProperty(Type type, string columnName)
        //{
        //    return
        //        type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).
        //            FirstOrDefault(
        //                prop =>
        //                prop.GetCustomAttributes(false)
        //                    // Search properties to find the one ColumnAttribute applied with Name property set as columnName to be Mapped 
        //                    .Any(attr => attr.GetType().Name == ColumnAttributeName
        //                                 &&
        //                                 attr.GetType().GetProperties(BindingFlags.Public |
        //                                                              BindingFlags.NonPublic |
        //                                                              BindingFlags.Instance)
        //                                     .Any(
        //                                         f =>
        //                                         f.Name == "Name" &&
        //                                         f.GetValue(attr).ToString().ToLower() == columnName.ToLower()))
        //                && // Also ensure the property is not read-only
        //                (prop.DeclaringType == type
        //                     ? prop.GetSetMethod(true)
        //                     : prop.DeclaringType.GetProperty(prop.Name,
        //                                                      BindingFlags.Public | BindingFlags.NonPublic |
        //                                                      BindingFlags.Instance).GetSetMethod(true)) != null
        //            );
        //}
    }

    public class MyModel
    {
        [Column("TSBNumber")]
        public string TSBNumber { get; set; }

        [Column(Name = "FileKey")]
        public string MyProperty2 { get; set; }

        //public string MyProperty2 { get; set; } // Uses Default Mapping
        // ...
    }

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
    public class ColumnAttribute : Attribute
    {
        public string Name { get; set; }
        public ColumnAttribute() { }
        public ColumnAttribute(string Name) { this.Name = Name; }
    }

    public class FallbackTypeMapper : SqlMapper.ITypeMap
    {
        private readonly IEnumerable<SqlMapper.ITypeMap> _mappers;

        public FallbackTypeMapper(IEnumerable<SqlMapper.ITypeMap> mappers)
        {
            _mappers = mappers;
        }


        public ConstructorInfo FindConstructor(string[] names, Type[] types)
        {
            foreach (var mapper in _mappers)
            {
                try
                {
                    ConstructorInfo result = mapper.FindConstructor(names, types);
                    if (result != null)
                    {
                        return result;
                    }
                }
                catch (NotImplementedException)
                {
                }
            }
            return null;
        }

        public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor, string columnName)
        {
            foreach (var mapper in _mappers)
            {
                try
                {
                    var result = mapper.GetConstructorParameter(constructor, columnName);
                    if (result != null)
                    {
                        return result;
                    }
                }
                catch (NotImplementedException)
                {
                }
            }
            return null;
        }

        public SqlMapper.IMemberMap GetMember(string columnName)
        {
            foreach (var mapper in _mappers)
            {
                try
                {
                    var result = mapper.GetMember(columnName);
                    if (result != null)
                    {
                        return result;
                    }
                }
                catch (NotImplementedException)
                {
                }
            }
            return null;
        }
    }
}

Executing Stored Procedure

  public static string ServiceTsbExport(DateTime StartDate, DateTime EndDate, int UserRoleID,
                                                string SelectedSystemIDs,
                                                 string SelectedTsbIDs)
        {
            EFDbContext db = new EFDbContext();


            Dapper.SqlMapper.SetTypeMap(typeof(MyModel), new ColumnAttributeTypeMapper<MyModel>());
            return db.Database.SqlQuery<MyModel>("[dbo].[spExportServiceTSB] @parm1, @parm2, @parm3, @parm4, @parm5",
                        new SqlParameter("parm1", StartDate),
                        new SqlParameter("parm2", EndDate),
                        new SqlParameter("parm3", SelectedSystemIDs),
                        new SqlParameter("parm4", SelectedTsbIDs),
                        new SqlParameter("parm5", UserRoleID)
                        ).ToList().ToHTMLTable();
        }

回答1:

I think you're making this very hard while it could be simple. I've once done almost exactly the same thing. Here's an anonymized version of it:

using Dapper;

...

using (var cnn = new SqlConnection(@"Data Source=... etc."))
{
    cnn.Open();

    var p = new DynamicParameters();
    p.Add("@params", "Id=21");

    var obs = cnn.Query(sql:"GetPivotData", param: p,
                        commandType:CommandType.StoredProcedure);

    var dt = ToDataTable(obs);
}

This ToDataTable method is probably similar to your ToHTMLTable method. Here it is:

public DataTable ToDataTable(IEnumerable<dynamic> items)
{
    if (items == null) return null;
    var data = items.ToArray();
    if (data.Length == 0) return null;

    var dt = new DataTable();
    foreach(var pair in ((IDictionary<string, object>)data[0]))
    {
        dt.Columns.Add(pair.Key, (pair.Value ?? string.Empty).GetType());
    }
    foreach (var d in data)
    {
        dt.Rows.Add(((IDictionary<string, object>)d).Values.ToArray());
    }
    return dt;
}

The heart of the logic is that the dynamic returned by Dapper's Query() extension method can be cast to an IDictionary<string, object>.