写在篇头:本文为原创,转贴请注明,如有雷同,纯属巧合。 顺祝各位大朋友和小朋友儿童节快乐!
使用了一段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对象
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个测试用例,如下
-
当loadProperties=false时,不加载,仅获取主表
点击"调用",结果显示:
-
当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):
[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方法
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
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个测试用例
-
当loadProperties=false时,不加载,仅获取主表
点击"调用",结果显示:
-
当loadProperties=true时,加载,获取主表及子表数据
点击"调用",结果显示:
(下面还有数据,省略了)
有兴趣的朋友们还可以做个客户端,接收到WebService的返回字符串,调用public static TLinq Xml2Linq<TLinq>()方法转化生成Linq对象。
一口气写了这么多,真累……
不过还是总结一下:自动产生的Linq类文件,是通过一个内置的T4模板生成的,希望Microsoft或有心的朋友们开发一套T4,能够生成支持序列化的XLinq对象 :P
MS.Net是一个非常优秀的开发平台,只要开动脑筋,应该木有解决不了的问题!