将匿名类型转换为任何类型
如何将 LINQ to SQL 的匿名类型转换为指定类型。
引言
本文介绍如何使用 .NET 3.5 扩展来处理从匿名类型到特定类型的转换。这在使用 LINQ to SQL 以列表和/或数组的形式检索数据时尤其有用。
问题
通过 Microsoft LINQ to SQL,您可以在享受强类型编程语言的同时,还能掌控您的数据。数据对象由类表示,当您通过 DBML 文件将数据结构链接到您的 Visual Studio 项目时,这些类会自动创建。
现在,您可以直接在 Visual Studio 编程环境中编写数据检索过程,而无需在 SQL 和编程之间分心。
同时,LINQ to SQL 有时也会带来一些挑战。其中之一就是 LINQ to SQL 查询返回的 匿名 类型。当您在一个查询中连接多个数据库表时,您必须选择如何组织返回的序列。很多时候,您需要创建一个返回匿名类型对象列表或数组的序列。许多程序员,包括我自己在内,认为在业务和/或 UI 层操作此类匿名类型对象并非最佳实践。
解决方案
假设我们有一个包含两个表的数据库:*Client* 和 *Order*。
Client 表包含客户特定信息。在本例中,是 Name 和 AddressID,后者显然指向某个存储该客户地址信息的仓库,但目前这不是我们的关注点。Order 表包含描述订单的文本(字符串格式)以及一个 *ClientID* 列,该列指向下订单的客户的 ID。
假设我们想获取所有客户的信息以及每个客户的订单数量。
让我们通过运行以下查询来查看数据
Group By
查询返回一个包含 ID、Name、Address ID 和 Total Orders 的行集,适用于 *Client* 表中的所有客户。在过去,您会使用 ADO.NET 来检索和处理数据。
解决问题
如果您想使用 LINQ to SQL,您会创建一个过程来检索包含所需信息的序列
public static List<Client> GetClientsWithTotalOrders()
{
//create DataContext object, use it, discard it:
using (CodeProjectDataContext db = new GetDataContext())
{
//join Client with Order:
var res = db.Clients
.Join(
db.Orders,
o => o.ID,
c => c.ClientID,
(c, o) => new { c }
)
//group by Client
.GroupBy(o => o.c)
//define output object
.Select(o => new { ID = o.Key.ID, AddressID = o.Key.AddressID,
Name = o.Key.Name, TotalOrders = o.Count() })
//output to List of objects:
.ToList()
;
//cast the output sequence to List of Clients and return:
return (List<Client>)res;
}
}
在此过程中,我们连接了 *Client* 和 *Order* 表,并按 *Client* 分组,计算了每个客户下了多少订单。结果返回格式如下:ID、Address ID、Name 和 Total Orders。
利用创建部分类的能力,我们在 Client
类中添加一个属性,并将我们想要的任何属性/方法添加到由 Visual Studio 自动设计器生成的类中。
public int TotalOrders { get; set; }
现在,Client
类具有以下属性:ID
、Name
、AddressID
和 TotalOrders
。LINQ to SQL GetClientsWithTotalOrders()
过程返回一个包含所有这些属性的对象列表。
但是,如果您尝试编译该过程,您会收到一个错误
Error 1 Cannot convert type 'System.Collections.Generic.List<AnonymousType#1>'
to 'System.Collections.Generic.List<CodeProject.LinkToSql.Client>'
C:\Development\VS2008\Projects\CodeProject\CodeProject\LinkToSql\DbHandlers.cs
38 23 CodeProject
不幸的是,编译器无法识别程序创建的匿名类型与您的 Client
类具有相同的属性集。这意味着您在检索数据后将不得不处理匿名 List
类型。我们如何将此匿名类型转换为 Client
类型?
通过编写 扩展 来处理匿名类型对象。
我创建了两个扩展来处理这个问题
public static object ToType<T>(this object obj, T type)
{
//create instance of T type object:
var tmp = Activator.CreateInstance(Type.GetType(type.ToString()));
//loop through the properties of the object you want to covert:
foreach (PropertyInfo pi in obj.GetType().GetProperties()
{
try
{
//get the value of property and try
//to assign it to the property of T type object:
tmp.GetType().GetProperty(pi.Name).SetValue(tmp,
pi.GetValue(obj, null), null)
}
catch { }
}
//return the T type object:
return tmp;
}
此扩展允许将匿名类型对象转换为指定类型。如果您有一个匿名类型的对象并想将其转换为 Client
类型,则需要调用此扩展。
object obj=getSomeObjectOfAnonymoustype();
Client client=obj.ToType(typeof (Client));
让我们看看它是如何工作的
首先,它使用 Activator
.CreateInstance
() 过程创建一个空的临时 Client
类型对象。然后,它遍历调用对象的所有属性信息,获取其值,并将该值重新分配给新创建的 Client
对象的相应属性。最后,它返回一个属性已从调用对象填充的 Client
类型对象。
因此,对于单个对象,问题已解决。
那么对于这种对象的列表呢?或者数组呢?
我创建的第二个扩展是将一个 匿名 类型对象的 List
转换为特定类型对象的 List
。
public static object ToNonAnonymousList<T>(this List<T> list, Type t)
{
//define system Type representing List of objects of T type:
var genericType = typeof(List<>).MakeGenericType(t);
//create an object instance of defined type:
var l = Activator.CreateInstance(genericType);
//get method Add from from the list:
MethodInfo addMethod = l.GetType().GetMethod("Add");
//loop through the calling list:
foreach (T item in list)
{
//convert each object of the list into T object
//by calling extension ToType<T>()
//Add this object to newly created list:
addMethod.Invoke(l, new object[] { item.ToType(t) });
}
//return List of T objects:
return l;
}
上面代码的第一行相当有趣
var genericType = typeof(List<>).MakeGenericType(t);
当我们在 typeof(List<>)
上调用 MakeGenericType(t)
函数时,它会用 List
对象的类型 T
替换,并返回一个表示 T
对象列表的 Type
对象。
在此之后,一切都变得非常简单
Activator 创建一个空的 T
对象列表。GetType().GetMethod("Add")
返回 MethodInfo
对象,我们将使用它来调用新创建列表的 Add()
方法。
然后,我们遍历原始列表,通过调用我们自己的扩展 ToType<T>()
将每个元素的原始类型更改为 T
,最后将此项添加到类型为 T
的列表中。返回的结果是类型为 T
的列表。
让我们用这个新扩展来更新过程。
public static List<Client> GetClientsWithTotalOrders()
{
//create DataContext object, use it, discard it:
using (CodeProjectDataContext db = new GetDataContext())
{
//join Client with Order:
var res = db.Clients
.Join(
db.Orders,
o => o.ID,
c => c.ClientID,
(c, o) => new { c }
)
//group by Client
.GroupBy(o => o.c)
//define output object
.Select(o => new { ID = o.Key.ID, AddressID = o.Key.AddressID,
Name = o.Key.Name, TotalOrders = o.Count() })
//output to List of objects:
.ToList()
//apply extension to covert into Client
.ToNonAnonymousList(typeof(Client))
;
//cast the output sequence to List of Clients and return:
return (List<Client>)res;
}
}
调用 ToNonAnonymousList(typeof(Client))
将匿名类型列表转换为 Client
类型列表。
历史
- 发布于 2009 年 8 月 2 日
当我发布这篇文章时,我收到了一些评论,询问为什么我要通过调用这些扩展来转换匿名类型,而我们可以直接使用以下语法。
.Select(o => new Client { ID = o.Key.ID, AddressID = o.Key.AddressID,
Name = o.Key.Name, TotalOrders = o.Count() })
正如您可能注意到的,这个语句创建了一个 Client
对象而不是一个匿名对象。
如果您在论坛上搜索,您会发现许多开发人员抱怨这种语法对许多 LINQ to SQL 查询不起作用,并且会返回以下错误。
"Explicit construction of entity type 'xxxxxxxx' in query is not allowed"
此错误是由于 Microsoft 添加的一项检查。“添加此检查是因为它本应从一开始就存在但却缺失了。手动构造实体实例作为投影会将可能格式错误的对象的缓存弄乱,导致程序员困惑并给我们带来大量错误报告。此外,投影出的实体是否应放入缓存或是否应被跟踪也是模糊不清的。实体的使用模式是它们在查询外部创建,并通过 DataContext
插入到表中,然后在稍后通过查询检索,绝不会由查询创建”。