如何将List<Dapper.SqlMapper.FastExpando>转换为运行时类型(List<IDictionary<string, object>>)?

3

我正在使用一种方法,它的返回值是IEnumerable<dynamic>。在运行时,对于特定的调用,它会返回List<Dapper.SqlMapper.FastExpando>

var x0 = repo.Find(proc, param); 
//x0 runtime type is {List<Dapper.SqlMapper.FastExpando>}

LINQPad.Extensions.Dump 表明 x0 的运行时类型是:List<IDictionary<String, Object>>,但我看起来无法转换为 List<IDictionary<String, Object>>。以下是 Linqpad Dump 的截图: enter image description here

最终我需要将每个字典的值连接到单个 IEnumerable<DateTime> 中。

IEnumerable<DateTime> GetDates(int productId) 
{
    const string proc = "[dbo].[myproc]";
    dynamic param = new { Id = "asdf" };
    var x0 = repo.Find(proc, param); 
    //...
    //linq conversion from x0 to IEnumerable<DateTime> here.
}

我遇到的错误

List<IDictionary<String, Object>> x5 = repo.Find(proc, param);

结果是:

RuntimeBinderException: Cannot implicitly convert type 'object' to
 'System.Collections.Generic
     .List<System.Collections.Generic.IDictionary<string,object>>'.
An explicit conversion exists (are you missing a cast?)

背景:我正在使用 Dapper 包装器,但无法更改返回非规范化结果的数据库表/存储过程。该存储过程返回 100 行 1 个数据元素的结果,而不是返回 1 行 100 列。我想避免创建一个表示这 100 列的类,并想利用 Dapper 的自动能力通过 columnName,columnValue 的 IDictionary 将列数据转置为行。

更新:似乎这是一个动态参数的问题。当内联指定时,它可以正常工作。如果在本地指定,然后作为参数传递,则会失败。

IEnumerable<DateTime> GetDates(int productId) 
{
    const string proc = "[dbo].[myproc]";
    dynamic param = new { Id = "asdf" };

    //next line throws RuntimeBinderException: 'object' does not 
    //contain a definition for 'First'.
    //IDictionary<String, object> x0 = repo.Find(proc, param).First();

    //this succeeds:
    IDictionary<String, object> x0 = repo.Find(proc, new { Id = "asdf" }).First();

    IEnumerable<DateTime> qry2
       = x0.Values.AsQueryable()
           .Where(x => x != null)
           .Select(x => (DateTime) x);
    return qry2;
}

以下是FindQuery的签名:

//Repository::Find
public IEnumerable<dynamic> Find(string procName, object param = null)

//Dapper SqlMapper::Query
public static IEnumerable<dynamic> Query(this IDbConnection cnn, string sql, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null)

当我看到答案和您的评论时,我认为repo.Find返回一个对象,但它可以(而且应该)被更改为返回IEnumerable<IDictionary<string, object>>,这是Dapper的Query方法的返回类型。FastExpando(在当前源代码中更改为DapperRow)实现了IDictionary<string, object> - Gert Arnold
@GertArnold,@ofstream - 看起来这是一个关于 Dapper 包装器的微妙问题。它的签名是 public IEnumerable<dynamic> Find(string procName, object param = null),而 Dapper 的签名是 public static IEnumerable<dynamic> Query(this IDbConnection cnn, string sql, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null),因此似乎在参数 param 上进行了从 objectdynamic 的类型转换。我认为将包装器签名更改为 dynamic param 应该可以解决这个问题。 - Jay Walker
重要的是返回参数。.Cast<IDictionary<String, Object>>() 应该可以工作(Servy 的回答)。 - Gert Arnold
请查看我的问题更新。这是与“动态参数”有关的问题。 - Jay Walker
当输入参数改变输出时,必须重载Find函数。 - Gert Arnold
不确定这是否有效,但请尝试修改签名为Find<T>(string procName,T param = null); - It'sNotALie.
4个回答

2
你可以使用LINQ的Cast方法将序列中的每个项转换类型。
List<IDictionary<String, Object>> data = repo.Find(proc, param)
    .AsEnumerable()
    .Cast<IDictionary<String, Object>>()
    .ToList();

我在那行代码上遇到了RuntimeBinderException异常:'object'不包含“Cast”的定义。 - Jay Walker
1
@JayWalker 如果它实际上是 IQueryable 而不是 IEnumerable,请添加 AsEnumerable - Servy
通过这个更改,我得到了以下错误:“object”不包含“AsEnumerable”的定义。repo.Find返回一个IEnumerable<dynamic>。我也尝试了将其更改为var data,但是仍然出现相同的异常。 - Jay Walker
@Servy 那个方法返回对象。 - It'sNotALie.

1

Repo.Find 不会返回一个 IEnumerable<dynamic>。它返回的是 object。如果你确定它总是返回一个 List<IDictionary<String, Object>>,只需这样做:

List<IDictionary<String, Object>> x5 = (List<IDictionary<String, Object>>)repo.Find(proc, param);

当我使用var x5 = repo.Find()时,我会收到一个RuntimeBinderException异常,指出“object”不包含“AsEnumerable”的定义。看起来运行时类型是:System.Collections.Generic.List<Dapper.SqlMapper.FastExpando> - Jay Walker
请查看我的问题更新。这是与“动态参数”有关的问题。 - Jay Walker
那真的很奇怪。我会看看能否找出原因。 - It'sNotALie.

1
问题在于将参数指定为dynamic会导致方法被动态调用,扩展方法无法链接。
因此,您可以使用var代替dynamic:
 var param = new { Id = "asdf" };
 var data = repo.Find(proc, param)
.Cast<IDictionary<String, Object>>()
.ToList();

或者在调用之前将 dynamic 参数转换为 object

 var data = repo.Find(proc, (object)param)
.Cast<IDictionary<String, Object>>()
.ToList();

如果Find的确返回了System.Collections.Generic.List<Dapper.SqlMapper.FastExpando>这个运行时类型,那么只需将结果强制转换为dynamic,以确保它使用其运行时类型进行分配。重要的是将其分配给IEnumerable<>,因为它是协变类型(IList<>List<>会失败)。
IEnumerable<IDictionary<String, Object>> data = (dynamic) repo.Find(proc, param);

0

解决方案

不要从您的方法中返回动态类型。Dapper有一个ToList<T>扩展方法,因此您可以将其转换为任何支持的类型。在这种情况下,如果您真的需要低级关联数组,请传回List<Dictionary<string,object>>。

值得注意的是,C#动态类型不会跨程序集边界进行封送,因此这可能是您遇到的问题。如果您正在跨程序集进行封送,则应使用Expando、List<Dictionary<string,object>>或某些由两个程序集引用的其他类型。

解释

这是Dapper SQLMapper源代码的旧版本,其中包括FastExpando(第941行)。 请注意FastExpando的类定义。

private class FastExpando : System.Dynamic.DynamicObject, IDictionary<string,object>


1
不确定为什么DapperDev团队将FastExpando设为私有。从关系型数据库中流动数据,应用一些轻微的数据处理,然后发送到NO-SQL存储中,这是有用的情况。将FastExpando作为一等公民会使这些场景更加易于处理。 - Allan McLemore

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接