LINQ to SQL 序列化
使用泛型、扩展方法和反射功能,为 LINQ to SQL 类实现一个通用的序列化类库
引言
我非常喜欢 .NET Framework 3.5 中的 LINQ to SQL 功能。如果您不知道 LINQ to SQL 是什么,请在此处阅读白皮书 这里。
我喜欢它使数据库编码变得简单易用的方式。开发人员无需使用不同的编程模型(CLR 函数和 SQL 存储过程),也无需在不同的编程语言(C#/VB.NET 和 T-SQL)之间切换。Visual Studio 2008 中的 LINQ to SQL 设计器也让我们的生活更加轻松——我们可以将表/视图/存储过程拖放到 LINQ to SQL 设计器表面。设计器会自动生成 .NET 类来表示这些数据库实体,甚至包括表之间的关系。
问题
我的项目包含多个层,某些层要求在将业务对象发送到另一层之前对其进行序列化。目前,我使用强类型数据集来表示数据库中的两个多表,但我仍然不得不手动创建近 100 个可序列化的业务对象来表示这些实体。
LINQ to SQL 给了我希望,能够最大限度地减少创建这些业务对象类的努力。LINQ to SQL 设计器生成的那些实体类看起来非常适合我的项目所需的业务对象。
但很快,我意识到我错了。
- LINQ to SQL 类不支持二进制序列化。虽然我可以手动修改它们以满足我的需求,但这非常耗时,而且如果将来表发生更改,维护起来也很困难。
- 如果表之间存在关系,LINQ to SQL 类就无法被 XML 序列化器序列化。
我将使用 Northwind 数据库和一个 WebService
项目来演示 XML 序列化问题。
首先,让我们创建一个 WebService
项目并添加一个名为 NorthWind.dbml 的 LINQ to SQL 类,然后将 Suppliers
和 Products
表拖放到 LINQ to SQL 设计器表面。您会看到为这两个表中的 Supplier
和 Product
实体生成了两个类,以及表示 Suppliers
和 Products
表之间关系的关联。

在 Service.cs 中创建一个返回 Product
对象(ProductID=1
)的 WebMethod
[WebMethod]
public Product GetProduct() {
NorthWindDataContext db = new NorthWindDataContext();
Product p = db.Products.Single(prod => prod.ProductID == 1);
return p;
}
执行此 WebMethod
(GetProduct
)将失败并出现以下异常
System.InvalidOperationException: There was an error generating the XML document. --->
System.InvalidOperationException:
A circular reference was detected while serializing an
object of type Product.
at System.Xml.Serialization.XmlSerializationWriter.WriteStartElement
(String name, String ns,
Object o, Boolean writePrefixed, XmlSerializerNamespaces xmlns)
at Microsoft.Xml.Serialization.GeneratedAssembly.
XmlSerializationWriter1.Write3_Product
(String n, String ns, Product o, Boolean isNullable, Boolean needType)
at Microsoft.Xml.Serialization.GeneratedAssembly.
XmlSerializationWriter1.Write2_Supplier
(String n, String ns, Supplier o, Boolean isNullable, Boolean needType)
at Microsoft.Xml.Serialization.GeneratedAssembly.
XmlSerializationWriter1.Write3_Product
(String n, String ns, Product o, Boolean isNullable, Boolean needType)
at Microsoft.Xml.Serialization.GeneratedAssembly.
XmlSerializationWriter1.Write4_Product
(Object o)
at Microsoft.Xml.Serialization.GeneratedAssembly.ProductSerializer.Serialize
(Object objectToSerialize, XmlSerializationWriter writer)
at System.Xml.Serialization.XmlSerializer.Serialize
(XmlWriter xmlWriter, Object o, XmlSerializerNamespaces namespaces,
String encodingStyle, String id)
--- End of inner exception stack trace ---
at System.Xml.Serialization.XmlSerializer.Serialize
(XmlWriter xmlWriter, Object o, XmlSerializerNamespaces namespaces,
String encodingStyle, String id)
at System.Xml.Serialization.XmlSerializer.Serialize(TextWriter textWriter, Object o)
at System.Web.Services.Protocols.XmlReturnWriter.Write
(HttpResponse response, Stream outputStream, Object returnValue)
at System.Web.Services.Protocols.HttpServerProtocol.WriteReturns
(Object[] returnValues, Stream outputStream)
at System.Web.Services.Protocols.WebServiceHandler.WriteReturns(Object[] returnValues)
at System.Web.Services.Protocols.WebServiceHandler.Invoke()
此异常的原因是 Product
对象有一个 Supplier
属性(Product.Supplier
),它持有一个 Supplier
对象的引用,而这个 Supplier
对象又有一个 Products
属性,指向一组 Product
对象,其中包括第一个 Product
对象。所以存在循环引用。当 Xmlserializer
检测到此循环引用时,它会报告 InvalidOperationException
。
有两种变通方法,请在此处阅读 Rick Strahl 出色的博客 这里。第一种变通方法是将关联更改为 internal,以便 XML 序列化能够工作,但某种程度上会损害 LINQ to SQL 的功能。第二种方法是使用 WCF 序列化,通过将 DataContext
设置为 Unidirectional Serialization 模式并修改关联为 non-public。这两种变通方法都不能满足我的需求,它们都不适用于二进制序列化,而且我不想修改 DataContext
。请记住,我拥有 200 多个表,维护一个修改过的 Datacontext
是一个巨大的工程。
我的项目需要
- 每个实体都有一个可序列化的业务对象类,并且我不需要实体之间的关联
- 这些业务实体类可用于 XML 序列化和二进制序列化
- 生成这些类的过程应该尽可能简化,并且可以重用于其他类似项目
解决方案
因此,这是我的解决方案——LinqSqlSerialization.dll 类库。它包含两个类
public class SerializableEntity<T>
public static class EnumerableExtension
如何使用 LinqSqlSerialization 类库
您需要做的就是将 LinqSqlSerialization.dll 添加到项目的引用中。
以下是一些示例,展示了如何在您的项目中轻松地序列化和反序列化 LINQ to SQL 类。
XML 序列化示例
///
/// Single Object Serialization
///
NorthWindDataContext db = new NorthWindDataContext();
Product product = db.Products.Single(prod => prod.ProductID == 2);
//convert to SerializableEntity
SerializableEntity<Product> entity = new SerializableEntity<Product>(product);
XmlSerializer serizer = new XmlSerializer(entity.GetType());
System.IO.MemoryStream ms = new System.IO.MemoryStream();
//serialize
serizer.Serialize(ms, entity);
//deserialize
ms.Position = 0;
SerializableEntity<Product> cloneEntity=
serizer.Deserialize(ms) as SerializableEntity<Product>;
//Get the original Product
Product cloneProduct =cloneEntity.Entity;
//Print the clone
//The result is Chang
ConsConsole.WriteLine("The result is {0}", cloneProduct.ProductName);
///
/// Collection XML Serialization
///
NorthWindDataContext db = new NorthWindDataContext();
List<SerializableEntity<Product>> products =
db.Products.ToList<Product, SerializableEntity<Product>>();
XmlSerializer serizer = new XmlSerializer(products.GetType());
System.Text.StringBuilder sb=new System.Text.StringBuilder();
System.IO.StringWriter sw=new System.IO.StringWriter(sb);
//serialize
serizer.Serialize(sw, products);
sw.Close();
string xmlResult = sb.ToString();
//deserialize
System.IO.StringReader sr = new System.IO.StringReader(xmlResult);
List<SerializableEntity<Product>> cloneEntities =
serizer.Deserialize(sr) as List<SerializableEntity<Product>>;
//Print the clones
foreach (SerializableEntity clone in cloneEntities)
{
Console.WriteLine("The result is {0}", clone.Entity.ProductName);
}
二进制序列化示例
///
/// Single Object Binary Serialization
///
NorthWindDataContext db = new NorthWindDataContext();
Product product = db.Products.Single />(prod => prod.ProductID == 2);
//convert to SerializableEntity
SerializableEntity<Product> entity = new SerializableEntity<Product>(product);
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter serizer
= new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
System.IO.MemoryStream ms = new System.IO.MemoryStream();
//serialize
serizer.Serialize(ms, entity);
//deserialize
ms.Position = 0;
SerializableEntity<Product> cloneEntity =
serizer.Deserialize(ms) as SerializableEntity<Product>;
//Get the original Product
Product cloneProduct = cloneEntity.Entity;
//Print the clone
//The result is Chang
Console.Console.WriteLine("The result is {0}", cloneProduct.ProductName);
///
/// Collection Binary Serialization
///
NorthWindDataContext db = new NorthWindDataContext();
List<SerializablEntity<Product>> products =
db.Products.ToList<Product, SerializableEntity<Product>>();
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter serizer
= new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
System.IO.MemoryStream ms = new System.IO.MemoryStream();
//serialize
serizer.Serialize(ms, products);
//deserialize
ms.Position = 0;
List<SerializableEntity<Product>> cloneEntities =
serizer.Deserialize(ms) as List<SerializableEntity<Product>>;
foreach (SerializableEntity<Product> cloneEntity in cloneEntities)
{
//Get the original Product
Product cloneProduct = cloneEntity.Entity;
//Print the clone
Console.WriteLine("The result is {0}", cloneProduct.ProductName);
}
Web Service 示例
在 Service.cs 中添加 GetProduct
函数并运行 WebService
[WebMethod]
public SerializableEntity<Product> GetProduct()
{
NorthWindDataContext db = new NorthWindDataContext();
Product p = db.Products.Single(prod => prod.ProductID == 2);
SerializableEntity<Product> entity = new SerializableEntity<Product>(p);
return entity;
}
在客户端,当客户端从 WSDL 生成代理类时,这些代理类的名称与 LINQ to SQL 设计器生成的实体类相同,并且除了关联属性之外,它们还具有相同的属性。

您可以这样使用代理类
localhost.Service service= new localhost.Service();
localhost.Product product = service.GetProduct();
Console.WriteLine(product.ProductID); //output is 2
它是如何工作的???
我的解决方案的核心是 SerializableEntity<T>
类,它是一个实现了 IXMLSerializable
和 ISerializable
接口的泛型类。
[Serializable] //the attribute is required by the binary serialization
[XmlSchemaProvider("MySchema")] //use customized schema for XML serialization
public class SerializableEntity<T> :
IXmlSerializable, ISerializable where T : class, new()
.NET Framework 的泛型是我最喜欢的功能。我们可以使用泛型来为特定类型定义一组操作或行为。在这种情况下,我实现了带有 IXmlSerializable
和 ISerializable
的泛型 SerializableEntity
类,类型参数 T
是那些 LINQ to SQL 类。因此,它可以极大地减少工作量,而不必为每个类实现序列化。
SerializableEntity
类只有一个方法,而且逻辑相当直接,除了我对 XML 序列化做了一些特殊的实现。
//One default constructor, which is required by the XML and Binary serialization.
public SerializableEntity() { }
private T _entity;
//One parameterized constructor and one public property (Entity)
//are used for passing and reading value to and from the SerializableEntity class
public SerializableEntity(T entity)
{
this.Entity = entity;
}
public T Entity
{
get { return _entity; }
set { _entity = value; }
}
使用反射来读取和写入 LINQ to SQL 实体对象的值,用于二进制和 XML 序列化。
//
//Binary Serialization.
//Implementation for ISerializable interface
//
#region ISerializable Members
//this constructor is required for deserialization
public SerializableEntity(SerializationInfo info, StreamingContext context)
{
_entity = new T();
PropertyInfo[] properties = _entity.GetType().GetProperties();
SerializationInfoEnumerator enumerator = info.GetEnumerator();
while (enumerator.MoveNext())
{
SerializationEntry se = enumerator.Current;
foreach (PropertyInfo pi in properties)
{
if (pi.Name == se.Name) {
pi.SetValue(_entity, info.GetValue(se.Name, pi.PropertyType), null);
}
}
}
}
//this method is the implementation of ISerializable.GetObjectData member
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
PropertyInfo[] infos = _entity.GetType().GetProperties();
foreach (PropertyInfo pi in infos)
{
bool isAssociation = false;
foreach (object obj in pi.GetCustomAttributes(true))
{
if (obj.GetType() == typeof(System.Data.Linq.Mapping.AssociationAttribute))
{ isAssociation = true; break; }
}
if (!isAssociation) {
if (pi.GetValue(_entity, null) != null) {
info.AddValue(pi.Name, pi.GetValue(_entity, null));
}
}
}
}
#endregion
//
//XML Serialization.
//Implementation for IXmlSerializable interface
//
#region IXmlSerializable Members
//this method is the implementation of IXmlSerializable.GetSchema member.
//It always returns null.
public System.Xml.Schema.XmlSchema GetSchema() { return null; }
//this method is the implementation of IXmlSerializable.ReadXml member
public void ReadXml(System.Xml.XmlReader reader)
{
_entity = new T();
PropertyInfo[] pinfos = _entity.GetType().GetProperties();
if (reader.LocalName == typeof(T).Name)
{
reader.MoveToContent();
string inn = reader.ReadOuterXml();
System.IO.StringReader sr=new System.IO.StringReader(inn);
System.Xml.XmlTextReader tr = new XmlTextReader(sr);
tr.Read();
while (tr.Read())
{
string elementName = tr.LocalName;
string value = tr.ReadString();
foreach (PropertyInfo pi in pinfos)
{
if (pi.Name == elementName)
{
TypeConverter tc = TypeDescriptor.GetConverter(pi.PropertyType);
pi.SetValue(_entity, tc.ConvertFromString(value), null);
}
}
}
}
}
//this method is the implementation of IXmlSerializable.WriteXml member
public void WriteXml(System.Xml.XmlWriter writer)
{
PropertyInfo[] pinfos = _entity.GetType().GetProperties();
foreach (PropertyInfo pi in pinfos)
{
bool isAssociation = false;
foreach (object obj in pi.GetCustomAttributes(true))
{
if (obj.GetType() == typeof(System.Data.Linq.Mapping.AssociationAttribute))
{
isAssociation = true;
break;
}
}
if (!isAssociation)
{
if (pi.GetValue(_entity, null) != null)
{
writer.WriteStartElement(pi.Name);
writer.WriteValue(pi.GetValue(_entity, null));
writer.WriteEndElement();
}
}
}
}
//return xsd type
private static string GetXsdType(string nativeType)
{
string[] xsdTypes = new string[]{"boolean", "unsignedByte",
"dateTime", "decimal", "Double",
"short", "int", "long", "Byte", "Float", "string", "unsignedShort",
"unsignedInt", "unsignedLong", "anyURI"};
string[] nativeTypes = new string[]{"System.Boolean", "System.Byte",
"System.DateTime", "System.Decimal",
"System.Double", "System.Int16", "System.Int32", "System.Int64",
"System.SByte", "System.Single", "System.String", "System.UInt16",
"System.UInt32", "System.UInt64", "System.Uri"};
for (int i = 0; i < nativeTypes.Length; i++)
{
if (nativeType == nativeTypes[i]) { return xsdTypes[i]; }
}
return "";
}
#endregion
由于我使用的是一个通用的 Generics
类来包装 LINQ to SQL 类,这意味着每种实体类型都需要自己的架构,因此我实现了自定义架构,该架构是根据类型参数动态生成的。请注意,对于 SerializableEntity<T>
类的 XmlSchemaProviderAttribute
,方法名应该相同。
private static readonly string ns = "http://tempuri.org/";
// This is the method named by the XmlSchemaProviderAttribute applied to the type.
public static XmlQualifiedName MySchema(XmlSchemaSet xs)
{
// This method is called by the framework to get the schema for this type.
System.Text.StringBuilder sb = new System.Text.StringBuilder();
System.IO.StringWriter sw = new System.IO.StringWriter(sb);
XmlTextWriter xw = new XmlTextWriter(sw);
// generate the schema for type T
xw.WriteStartDocument();
xw.WriteStartElement("schema");
xw.WriteAttributeString("targetNamespace", ns);
xw.WriteAttributeString("xmlns", "http://www.w3.org/2001/XMLSchema");
xw.WriteStartElement("complexType");
xw.WriteAttributeString("name", typeof(T).Name);
xw.WriteStartElement("sequence");
PropertyInfo[] infos = typeof(T).GetProperties();
foreach (PropertyInfo pi in infos)
{
bool isAssociation = false;
foreach (object a in pi.GetCustomAttributes(true))
{
//check whether the property is an Association
if (a.GetType() == typeof(System.Data.Linq.Mapping.AssociationAttribute))
{
isAssociation = true;
break;
}
}
//only use the property which is not an Association
if (!isAssociation)
{
xw.WriteStartElement("element");
xw.WriteAttributeString("name", pi.Name);
if (pi.PropertyType.IsGenericType) {
Type[] types = pi.PropertyType.GetGenericArguments();
xw.WriteAttributeString("type", "" + GetXsdType(types[0].FullName));
} else {
xw.WriteAttributeString("type", "" +
GetXsdType(pi.PropertyType.FullName));
} xw.WriteEndElement();
}
}
xw.WriteEndElement();
xw.WriteEndElement();
xw.WriteEndElement();
xw.WriteEndDocument();
xw.Close();
XmlSerializer schemaSerializer = new XmlSerializer(typeof(XmlSchema));
System.IO.StringReader sr = new System.IO.StringReader(sb.ToString());
XmlSchema s = (XmlSchema)schemaSerializer.Deserialize(sr);
xs.XmlResolver = new XmlUrlResolver();
xs.Add(s);
return new XmlQualifiedName(typeof(T).Name, ns);
}
SerializableEntity<T>
仅适用于单个对象,但当我们使用 LINQ to SQL 时,我们大多数时候都在处理一组实体,例如
NorthWindDataContext db = new NorthWindDataContext();
var products = from p in db.Products
Select p;
由于大多数查询结果都通过 System.Collections.Generic.IEnumerable
接口实现,因此我使用 .NET Framework 3.5 的 Extension 功能来实现 EnumableExtesion
类,以便轻松地为现有类添加新功能。
public static class EnumerableExtension
{
public static List<TSerializableEntity> ToList<TSource, TSerializableEntity>
(this IEnumerable<TSource> source)
where TSource : class, new()
where TSerializableEntity : SerializableEntity<TSource>, new()
{
List<TSerializableEntity> list = new List<TSerializableEntity>();
foreach (TSource entity in source)
{
list.Add(ToTSerializableEntity(entity));
}
return list;
}
}
我可以在 LINQ to SQL 查询中使用它,直接返回一个可序列化实体的类型化列表。这使得代码紧凑且易于理解。

现在有了这个 LinqSqlSerialization
类库,我就不必手动创建或修改这些业务对象了,跨层移动实体对象也无需担心。一个圆满的结局!
历史
- 2007年12月20日:初始发布