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