Linq 之WebService返回Linq对象解决方案

写在篇头:本文为原创,转贴请注明,如有雷同,纯属巧合。 顺祝各位大朋友和小朋友儿童节快乐! 

使用了一段Linq2SQL,作为数据传输对象,Linq确实能有效的提高业务逻辑的开发效率 J

在Linq对象的使用上,也有不少短板。比如级联修改、删除等操作的开发非常容易出错,而且难以修正,因不明其问题所在。此类问题,网上的解决解答有很多,不过还是需要DEV自己过滤和实践。有时间的话,我会记录下来实际开发中的案例及解决方案,本篇暂不涉及。

相信很多.Net的DEV已经习惯采用多层架构的设计开发,并且业务逻辑层(以下采用BL表示)的一种流行部署方式是使用WebService。同时如果Dev又使用了Linq2SQL的技术,以Linq对象作为数据传输的载体,通过WebService返回到Client,那么问题就来了:

Linq对象是对数据表的映射,并且能很好的处理表间的级联关系 – 内聚,其实是一种对象引用。但是这种引用是一种嵌套引用,即A中有B,B中有A。这样的话WebService返回Linq对象时(有级联数据的)会产生异常,因为WebService要序列化这个对象为string,而Linq的嵌套引用的特性将使序列化的string为无穷长。

案例描述

Linq2SQL:DicBO是主表,DicBOProperty是子表

WebService方法:获取DicBO对象    

View Code
[WebMethod]
public DicBO GetLinq(string boID, bool loadProperties)
{
DicBO res
= null;

try
{
using (UIModelDataContext dc = new UIModelDataContext(CONN_STR))
{
if (dc.Connection.State != System.Data.ConnectionState.Open)
{
dc.Connection.Open();
}

dc.CommandTimeout
= 1200;
dc.DeferredLoadingEnabled
= false; // 设定不延迟加载

// 是否加载级联的属性表数据
if (loadProperties)
{
dc.DeferredLoadingEnabled
= false;

DataLoadOptions dlo
= new DataLoadOptions();
dlo.LoadWith
<DicBO>(b => b.DicBOProperties);
dc.LoadOptions
= dlo;

}

var query
= dc.GetTable<DicBO>().Where(v => v.BOID == boID);

if (1 == query.Count())
{
res
= query.SingleOrDefault();
}
else if (1 < query.Count())
{
throw new Exception("More than one record.");
}
}
}
catch (Exception ex)
{
res
= null;
}

return res;
}



OK,我们来运行一下这个WebService,参数loadProperties指示是否要加载子表。设定2个测试用例,如下

  1. 当loadProperties=false时,不加载,仅获取主表

    点击"调用",结果显示:

  2. 当loadProperties=true时,加载,获取主表及子表数据

    点击"调用",发生错误:

    System.InvalidOperationException: 生成 XML 文档时出错。 ---> System.InvalidOperationException: 序列化类型 Stone.Software.UI.ModelDesign.DicBO 的对象时检测到循环引用。

    在 System.Xml.Serialization.XmlSerializationWriter.WriteStartElement(String name, String ns, Object o, Boolean writePrefixed, XmlSerializerNamespaces xmlns)

    在 Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriter1.Write25_DicBO(String n, String ns, DicBO o, Boolean isNullable, Boolean needType)

    在 Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriter1.Write4_DicBOProperty(String n, String ns, DicBOProperty o, Boolean isNullable, Boolean needType)

    在 Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriter1.Write25_DicBO(String n, String ns, DicBO o, Boolean isNullable, Boolean needType)

    在 Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriter1.Write26_DicBO(Object o)

    在 Microsoft.Xml.Serialization.GeneratedAssembly.DicBOSerializer.Serialize(Object objectToSerialize, XmlSerializationWriter writer)

    在 System.Xml.Serialization.XmlSerializer.Serialize(XmlWriter xmlWriter, Object o, XmlSerializerNamespaces namespaces, String encodingStyle, String id)

    --- 内部异常堆栈跟踪的结尾 ---

    在 System.Xml.Serialization.XmlSerializer.Serialize(XmlWriter xmlWriter, Object o, XmlSerializerNamespaces namespaces, String encodingStyle, String id)

    在 System.Xml.Serialization.XmlSerializer.Serialize(TextWriter textWriter, Object o, XmlSerializerNamespaces namespaces)

    在 System.Xml.Serialization.XmlSerializer.Serialize(TextWriter textWriter, Object o)

    在 System.Web.Services.Protocols.XmlReturnWriter.Write(HttpResponse response, Stream outputStream, Object returnValue)

    在 System.Web.Services.Protocols.HttpServerProtocol.WriteReturns(Object[] returnValues, Stream outputStream)

    在 System.Web.Services.Protocols.WebServiceHandler.WriteReturns(Object[] returnValues)

    在 System.Web.Services.Protocols.WebServiceHandler.Invoke()

这的确是个令人头疼的问题,上网搜索了不少,但都没有一个可以立竿见影的解答。

当然这不是Linq设计的Bug,也不是WebService设计的bug,而是这两种技术的特性不兼容造成的。如何解决这个棘手的问题呢???

劳动人民的智慧这时候又要启动了(嘿嘿……),解决方法分为3步

1> 如果你做过XML编程,别忘了.Net还提供了一项技术 – 序列化&反序列化 (详细的技术参考http://msdn.microsoft.com/zh-cn/library/e123c76w(v=VS.80).aspx,这里不做讲解

 现在,我们手动加工一下自动产生的Linq对象代码(偶称其为XLinq对象 J):  

XLinq
[XmlTypeAttribute(TypeName = "DicBO")]
[XmlRootAttribute(Namespace
= "", IsNullable = false)]
[
global::System.Data.Linq.Mapping.TableAttribute(Name = "dbo.Dic_BO")]
public partial class DicBO : INotifyPropertyChanging, INotifyPropertyChanged
{

private static PropertyChangingEventArgs emptyChangingEventArgs = new PropertyChangingEventArgs(String.Empty);

private string _BOID;

private string _Description;

private EntitySet<ParamBO> _ParamBOs;

private EntitySet<DicBOProperty> _DicBOProperties;

#region Extensibility Method Definitions
partial void OnLoaded();
partial void OnValidate(System.Data.Linq.ChangeAction action);
partial void OnCreated();
partial void OnBOIDChanging(string value);
partial void OnBOIDChanged();
partial void OnDescriptionChanging(string value);
partial void OnDescriptionChanged();
#endregion

public DicBO()
{
this._ParamBOs = new EntitySet<ParamBO>(new Action<ParamBO>(this.attach_ParamBOs), new Action<ParamBO>(this.detach_ParamBOs));
this._DicBOProperties = new EntitySet<DicBOProperty>(new Action<DicBOProperty>(this.attach_DicBOProperties), new Action<DicBOProperty>(this.detach_DicBOProperties));
OnCreated();
}

[XmlElementAttribute(
"BOID")]
[
global::System.Data.Linq.Mapping.ColumnAttribute(Storage = "_BOID", DbType = "varchar(50)", CanBeNull = false, IsPrimaryKey = true)]
public string BOID
{
get
{
return this._BOID;
}
set
{
if ((this._BOID != value))
{
this.OnBOIDChanging(value);
this.SendPropertyChanging();
this._BOID = value;
this.SendPropertyChanged("BOID");
this.OnBOIDChanged();
}
}
}

[XmlElementAttribute(
"Description")]
[
global::System.Data.Linq.Mapping.ColumnAttribute(Storage = "_Description", DbType = "varchar(200)", CanBeNull = false)]
public string Description
{
get
{
return this._Description;
}
set
{
if ((this._Description != value))
{
this.OnDescriptionChanging(value);
this.SendPropertyChanging();
this._Description = value;
this.SendPropertyChanged("Description");
this.OnDescriptionChanged();
}
}
}

[XmlArray(
"ParamBOs")]
[
global::System.Data.Linq.Mapping.AssociationAttribute(Name = "DicBO_ParamBO", Storage = "_ParamBOs", ThisKey = "BOID", OtherKey = "BOID")]
public EntitySet<ParamBO> ParamBOs
{
get
{
return this._ParamBOs;
}
set
{
this._ParamBOs.Assign(value);
}
}

[XmlArray(
"DicBOProperties")]
[
global::System.Data.Linq.Mapping.AssociationAttribute(Name = "DicBO_DicBOProperty", Storage = "_DicBOProperties", ThisKey = "BOID", OtherKey = "BOID")]
public EntitySet<DicBOProperty> DicBOProperties
{
get
{
return this._DicBOProperties;
}
set
{
this._DicBOProperties.Assign(value);
}
}

// 以下代码省略

}

[XmlTypeAttribute(TypeName
= "DicBOProperty")]
[XmlRootAttribute(Namespace
= "", IsNullable = false)]
[
global::System.Data.Linq.Mapping.TableAttribute(Name = "dbo.Dic_BOProperty")]
public partial class DicBOProperty : INotifyPropertyChanging, INotifyPropertyChanged
{

private static PropertyChangingEventArgs emptyChangingEventArgs = new PropertyChangingEventArgs(String.Empty);

private string _BOPropertyID;

private string _BOPropertyName;

private string _BOPropertyType;

private int _BOPropertyLength;

private string _BOID;

private EntitySet<BDMappingDetail> _BDMappingDetails;

private EntityRef<DicBO> _DicBO;

#region Extensibility Method Definitions
partial void OnLoaded();
partial void OnValidate(System.Data.Linq.ChangeAction action);
partial void OnCreated();
partial void OnBOPropertyIDChanging(string value);
partial void OnBOPropertyIDChanged();
partial void OnBOPropertyNameChanging(string value);
partial void OnBOPropertyNameChanged();
partial void OnBOPropertyTypeChanging(string value);
partial void OnBOPropertyTypeChanged();
partial void OnBOPropertyLengthChanging(int value);
partial void OnBOPropertyLengthChanged();
partial void OnBOIDChanging(string value);
partial void OnBOIDChanged();
partial void OnBOPropertyValueChanging(string value);
partial void OnBOPropertyValueChanged();
#endregion

public DicBOProperty()
{
this._BDMappingDetails = new EntitySet<BDMappingDetail>(new Action<BDMappingDetail>(this.attach_BDMappingDetails), new Action<BDMappingDetail>(this.detach_BDMappingDetails));
this._DicBO = default(EntityRef<DicBO>);
OnCreated();
}

[XmlElementAttribute(
"BOPropertyID")]
[
global::System.Data.Linq.Mapping.ColumnAttribute(Storage = "_BOPropertyID", DbType = "varchar(50)", CanBeNull = false, IsPrimaryKey = true)]
public string BOPropertyID
{
get
{
return this._BOPropertyID;
}
set
{
if ((this._BOPropertyID != value))
{
this.OnBOPropertyIDChanging(value);
this.SendPropertyChanging();
this._BOPropertyID = value;
this.SendPropertyChanged("BOPropertyID");
this.OnBOPropertyIDChanged();
}
}
}

[XmlElementAttribute(
"BOPropertyName")]
[
global::System.Data.Linq.Mapping.ColumnAttribute(Storage = "_BOPropertyName", DbType = "varchar(50)", CanBeNull = false, UpdateCheck = UpdateCheck.Never)]
public string BOPropertyName
{
get
{
return this._BOPropertyName;
}
set
{
if ((this._BOPropertyName != value))
{
this.OnBOPropertyNameChanging(value);
this.SendPropertyChanging();
this._BOPropertyName = value;
this.SendPropertyChanged("BOPropertyName");
this.OnBOPropertyNameChanged();
}
}
}

[XmlElementAttribute(
"BOPropertyType")]
[
global::System.Data.Linq.Mapping.ColumnAttribute(Storage = "_BOPropertyType", DbType = "varchar(50)", CanBeNull = false, UpdateCheck = UpdateCheck.Never)]
public string BOPropertyType
{
get
{
return this._BOPropertyType;
}
set
{
if ((this._BOPropertyType != value))
{
this.OnBOPropertyTypeChanging(value);
this.SendPropertyChanging();
this._BOPropertyType = value;
this.SendPropertyChanged("BOPropertyType");
this.OnBOPropertyTypeChanged();
}
}
}

[XmlElementAttribute(
"BOPropertyLength")]
[
global::System.Data.Linq.Mapping.ColumnAttribute(Storage = "_BOPropertyLength", DbType = "int", UpdateCheck = UpdateCheck.Never)]
public int BOPropertyLength
{
get
{
return this._BOPropertyLength;
}
set
{
if ((this._BOPropertyLength != value))
{
this.OnBOPropertyLengthChanging(value);
this.SendPropertyChanging();
this._BOPropertyLength = value;
this.SendPropertyChanged("BOPropertyLength");
this.OnBOPropertyLengthChanged();
}
}
}

[XmlElementAttribute(
"BOID")]
[
global::System.Data.Linq.Mapping.ColumnAttribute(Storage = "_BOID", DbType = "varchar(50)", CanBeNull = false, UpdateCheck = UpdateCheck.Never)]
public string BOID
{
get
{
return this._BOID;
}
set
{
if ((this._BOID != value))
{
if (this._DicBO.HasLoadedOrAssignedValue)
{
throw new System.Data.Linq.ForeignKeyReferenceAlreadyHasValueException();
}
this.OnBOIDChanging(value);
this.SendPropertyChanging();
this._BOID = value;
this.SendPropertyChanged("BOID");
this.OnBOIDChanged();
}
}
}

[XmlArray(
"BDMappingDetails")]
[
global::System.Data.Linq.Mapping.AssociationAttribute(Name = "DicBOProperty_BDMappingDetail", Storage = "_BDMappingDetails", ThisKey = "BOPropertyID", OtherKey = "BOPropertyID")]
public EntitySet<BDMappingDetail> BDMappingDetails
{
get
{
return this._BDMappingDetails;
}
set
{
this._BDMappingDetails.Assign(value);
}
}

[XmlIgnore]
[
global::System.Data.Linq.Mapping.AssociationAttribute(Name = "DicBO_DicBOProperty", Storage = "_DicBO", ThisKey = "BOID", OtherKey = "BOID", IsForeignKey = true)]
public DicBO DicBO
{
get
{
return this._DicBO.Entity;
}
set
{
DicBO previousValue
= this._DicBO.Entity;
if (((previousValue != value)
|| (this._DicBO.HasLoadedOrAssignedValue == false)))
{
this.SendPropertyChanging();
if ((previousValue != null))
{
this._DicBO.Entity = null;
previousValue.DicBOProperties.Remove(
this);
}
this._DicBO.Entity = value;
if ((value != null))
{
value.DicBOProperties.Add(
this);
this._BOID = value.BOID;
}
else
{
this._BOID = default(string);
}
this.SendPropertyChanged("DicBO");
}
}
}

// 以下代码省略

}

重点看一下[Xml???Attribute]的部分,之所以添加XML的Attribute特性,是因为我要控制哪些数据需要序列化,哪些不用。原则如下

// 1. Linq类添加特性 - [XmlTypeAttribute(TypeName = "Linq class name")] & [XmlRootAttribute(Namespace = "", IsNullable = false)]

// 2. Linq类ColumnAttribute特性的属性添加特性 - [XmlElementAttribute("Property name")]

// 3. Linq类的子表对象(一般是EntitySet<T>类型的属性)添加特性 - [XmlArray("Property name")]

// 4. 外键属性添加特性 - [XmlIgnore]

 

2>  然后添加3个共用泛型方法去做Linq和XML的转换 3> 最后修改下WebService的GetLinq方法

Common methods
public static string Linq2Xml<TLinq>(TLinq linq)
where TLinq : class
{
string res = null;

using (StringWriter sw = new StringWriter())
{
XmlSerializer xs
= new XmlSerializer(typeof(TLinq));
xs.Serialize(sw, linq);
res
= sw.ToString();
}

return res;
}

public static TLinq Xml2Linq<TLinq>(Stream stream)
where TLinq : class
{
XmlSerializer xs
= new XmlSerializer(typeof(TLinq));
return (TLinq)xs.Deserialize(stream);
}

public static TLinq Xml2Linq<TLinq>(string linqXML)
where TLinq : class
{
TLinq linq
= null;

// Remove XML declare
if (linqXML.StartsWith("<?"))
{
int startIdx = linqXML.IndexOf("?>");
linqXML
= linqXML.Substring(startIdx);
startIdx
= linqXML.IndexOf("<");
linqXML
= linqXML.Substring(startIdx);
}

using (MemoryStream ms = TextHelper.Instance.String2Stream(linqXML)) // 一个辅助方法做string to Stream,很简单,这里不提供代码
{
linq
= Xml2Linq<TLinq>(ms);
}

return linq;
}

返回值改为string类型:是由Linq对象转化而来的XML格式的string

WebService.GetLinq
[WebMethod]
public string GetLinq(string boID, bool loadProperties)
{
string res = null;

try
{
using (UIModelDataContext dc = new UIModelDataContext(CONN_STR))
{
DicBO bo
= null;

if (dc.Connection.State != System.Data.ConnectionState.Open)
{
dc.Connection.Open();
}

dc.CommandTimeout
= 1200;
dc.DeferredLoadingEnabled
= false;

if (loadProperties)
{
DataLoadOptions dlo
= new DataLoadOptions();
dlo.LoadWith
<DicBO>(b => b.DicBOProperties);
dc.LoadOptions
= dlo;
}

var query
= dc.GetTable<DicBO>().Where(v => v.BOID == boID);

if (1 == query.Count())
{
bo
= query.SingleOrDefault();
}
else if (1 < query.Count())
{
throw new Exception("More than one record.");
}

res
= Common.Linq2Xml<DicBO>(bo);
}
}
catch (Exception ex)
{
res
= null;
}

return res;
}

好了,大功告成,执行上面的2个测试用例

  1. 当loadProperties=false时,不加载,仅获取主表

    点击"调用",结果显示:

     

  2. 当loadProperties=true时,加载,获取主表及子表数据

    点击"调用",结果显示:

    (下面还有数据,省略了)

有兴趣的朋友们还可以做个客户端,接收到WebService的返回字符串,调用public static TLinq Xml2Linq<TLinq>()方法转化生成Linq对象。

 

一口气写了这么多,真累……

不过还是总结一下:自动产生的Linq类文件,是通过一个内置的T4模板生成的,希望Microsoft或有心的朋友们开发一套T4,能够生成支持序列化的XLinq对象 :P

MS.Net是一个非常优秀的开发平台,只要开动脑筋,应该木有解决不了的问题!


posted @ 2011-06-01 15:19  金子老豆  阅读(795)  评论(0编辑  收藏  举报