65.9K
CodeProject 正在变化。 阅读更多。
Home

LINQ to SQL 序列化

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.74/5 (22投票s)

2007年12月20日

CPOL

5分钟阅读

viewsIcon

195019

downloadIcon

1375

使用泛型、扩展方法和反射功能,为 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 设计器生成的那些实体类看起来非常适合我的项目所需的业务对象。

但很快,我意识到我错了。

  1. LINQ to SQL 类不支持二进制序列化。虽然我可以手动修改它们以满足我的需求,但这非常耗时,而且如果将来表发生更改,维护起来也很困难。
  2. 如果表之间存在关系,LINQ to SQL 类就无法被 XML 序列化器序列化。

我将使用 Northwind 数据库和一个 WebService 项目来演示 XML 序列化问题。

首先,让我们创建一个 WebService 项目并添加一个名为 NorthWind.dbml 的 LINQ to SQL 类,然后将 SuppliersProducts 表拖放到 LINQ to SQL 设计器表面。您会看到为这两个表中的 SupplierProduct 实体生成了两个类,以及表示 SuppliersProducts 表之间关系的关联。

Screenshot - DataContext.JPG

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

执行此 WebMethodGetProduct)将失败并出现以下异常

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 是一个巨大的工程。

我的项目需要

  1. 每个实体都有一个可序列化的业务对象类,并且我不需要实体之间的关联
  2. 这些业务实体类可用于 XML 序列化和二进制序列化
  3. 生成这些类的过程应该尽可能简化,并且可以重用于其他类似项目

解决方案

因此,这是我的解决方案——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 设计器生成的实体类相同,并且除了关联属性之外,它们还具有相同的属性。

Screenshot - ProxyClasses.JPG

您可以这样使用代理类

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<T> 接口实现,因此我使用 .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 查询中使用它,直接返回一个可序列化实体的类型化列表。这使得代码紧凑且易于理解。

Screenshot - Extension.PNG

现在有了这个 LinqSqlSerialization 类库,我就不必手动创建或修改这些业务对象了,跨层移动实体对象也无需担心。一个圆满的结局!

历史

  • 2007年12月20日:初始发布
© . All rights reserved.