您的位置:首页 > 其它

LINQ快速开发设计最佳实践(三) LINQ数据访问与业务逻辑层对象模板

2009-02-19 01:45 751 查看

一.摘要

LINQ在项目架构中的角色层次到底是什么?DataCotext对象应该放在哪里?层次间如何调用?在项目设计中我就不停的思考这些问题,其实最后确定下来LINQ的结构体系还是在几个版本的尝试后,其间经过了几次论证和修改.作为本系列文章主题内容的收尾之作,分享一下我的思考,以及我设计的LINQ业务逻辑对象与数据访问对象模板代码.

二.当三层架构遇上LINQ

传统的三层架构我不需要多介绍:UI(Web)层,业务逻辑层,数据访问层.其中可能还有模型层,公共层等辅助层.

数据访问层是和数据持久化打交道的.打交道的对象不仅仅是数据库.数据库仅仅是数据持久化的一种方式而已.当我们使用LINQ的时候,往往使用LINQTOSQL来实现ORM,上一章我技巧性的介绍了如何创建一个我认为标准的LINQTOSQL的Model对象.此后我们都是通过操作Model和DataContext对象实现对数据库的操作.所以我曾想舍弃数据访问层,有了LINQTOSQL觉得不在需要这个层次了.LINQ的语法更接近业务对象查询的逻辑.感觉直接在业务逻辑层使用Model对象进行LINQ查询就可以了.但是又考虑或者Model层也许应该改名叫数据访问层,因为是它在和数据库打交道.又或者......为了广大程序员的精神安全,在此省略万字.总之最后我的实践结论是:

1.Model层不可少,用于存放我们创建的实现了ORM的Model对象.

2.数据访问层不可少.DataContext对象应该放置在数据访问层.每一个表至少都有一个数据访问层对象.每个表都只能负责自己的基础的CURD操作.其中逻辑的实现是使用LINQ语法操作Model对象实现.

3.业务逻辑层中,可以存在两个数据访问层对象(即数据库中的两个表)对应一个业务逻辑对象的情况,因为业务逻辑对象有时候需要进行业务抽象.比如标签和标签名字语言表,完全可以抽象为一个"标签业务逻辑对象".

注:这一条在我的项目中没有实现,我的每一个数据库表都有一个业务逻辑对象.

4.业务逻辑层对象之间不能相互调用.但是业务逻辑对象可以调用任何数据访问层对象.

5.数据访问层对象之间不能相互调用.

总有人怀疑业务逻辑层和数据访问层存在的必要性,觉得他们两者应该合并.但是我相信有经验的开发人员都很清楚,这两者职责分明逻辑清楚,都是必不可少的.比如要实现程序的多数据库支持就可以使用依赖注入的方式,创建多个数据库的数据访问层,然后在初始化时选择性加载.

大家可能还经常碰到这种情况:业务逻辑层很多的方法都是单纯的返回数据访问层对象的方法数据,只有一个"return"语句.我觉得这是一个颗粒度上面的问题.首先要明确数据访问层才是最细颗粒度的,其实最细颗粒度的方法我们在程序中是很少直接使用的,都是要增加很多的逻辑在里面.所以在创建业务逻辑层对象的时候,如果一个最基础的方法可能根本不会被UI层调用,则没必要创建.但是在业务逻辑层创建了所有的最细颗粒度的方法,我觉得也不算错.一般我将这两类方法使用过两个region折叠块分离,也还算代码清晰.

三.实例简介

TagItem是我们常见的"标签",在博客园发表文章的时候也可以为一篇文章添加多个标签.假设数据库中有这么两个表,一个是TagItem主表,另一个是语言表.对于现实世界一个"标签"表示的抽象,可以有多种语言命名.





四.数据访问层模板.

下面是一个完整的TagItemLanguage表的的数据访问层对象代码.对于TagItem表,我们只需要批量替换将"TagItemLanguage"替换成"TagItem"即可,其实更科学的是使用模板生成工具比如CodeSmith自动生成数据访问类的代码.

/***************************************************************************************
**
**FileName:TagItemLanguageDA.cs
**Creator:ziqiu.zhang
**CreateTime:2008-11-10
**FunctionalDescription:TagItemLanguage数据访问类
**Remark:
**
**Copyright(c)eLongCorporation.Allrightsreserved.
****************************************************************************************/

usingSystem;
usingSystem.Collections.Generic;
usingSystem.Linq;
usingSystem.Text;
usingSystem.Data.Linq;
usingCom.Elong.Model.Tag;
usingCom.Elong.Model.Common;
usingCom.Elong.Common.Comment;

namespaceCom.Elong.DataAccess.Tag
{
///<summary>
///TagItemLanguage数据访问类
///</summary>
publicclassTagItemLanguageDA:IDisposable
{

#region====================PrivateField====================
privateboolisDisposed=false;
#endregion

#region====================Property=========================
privatestringm_ConnectionString;
///<summary>
///数据库连接字符串
///</summary>
publicstringConnectionString
{
get{returnm_ConnectionString;}
set{m_ConnectionString=value;}
}
#endregion

#region====================ConstructedMethod===============
///<summary>
///禁止使用
///</summary>
privateTagItemLanguageDA()
{}

///<summary>
///构造函数
///</summary>
///<paramname="connectionString">数据库连接串</param>
publicTagItemLanguageDA(stringconnectionString)
{
m_ConnectionString=connectionString;
}
#endregion

#region====================PublicMethod====================

#region=====Select=====
///<summary>
///根据主键得到对象
///</summary>
///<paramname="pkid">主键</param>
///<returns>对象</returns>
publicTagItemLanguageGetItemByPkid(intpkid)
{
TagItemLanguageresult=newTagItemLanguage();
TagDataContextdc=newTagDataContext(ConnectionString);
dc.DeferredLoadingEnabled=true;
varquery=
fromtagItemLanguageindc.TagItemLanguage
wheretagItemLanguage.pkid==pkid
selecttagItemLanguage;
result=query.SingleOrDefault();
returnresult;
}

///<summary>
///根据主键列表得到对象集合
///</summary>
///<paramname="pkid">主键列表</param>
///<returns>对象集合</returns>
publicList<TagItemLanguage>GetListByPkid(List<int>pkidList)
{
if(pkidList.Count>2100)
{
thrownewParameterOverflowException("传入的pkid列表个数大于2100,超过了Sql允许的最大参数个数。");
}
List<TagItemLanguage>result=newList<TagItemLanguage>();
TagDataContextdc=newTagDataContext(ConnectionString);
dc.DeferredLoadingEnabled=true;
varquery=
fromtagItemLanguageindc.TagItemLanguage
wherepkidList.Contains(tagItemLanguage.pkid)
selecttagItemLanguage;
result=query.ToList();
returnresult;
}
#endregion

#region=====Insert=====
///<summary>
///插入对象
///</summary>
///<paramname="item">TagItemLanguage对象</param>
///<returns>变更集</returns>
publicChangeSetInsert(TagItemLanguageitem)
{
TagDataContextdc=newTagDataContext(ConnectionString);
dc.TagItemLanguage.InsertOnSubmit(item);
ChangeSetresult=dc.GetChangeSet();
dc.SubmitChanges();
returnresult;
}

///<summary>
///插入集合
///</summary>
///<paramname="item">TagItemLanguage集合</param>
///<returns>变更集</returns>
publicChangeSetInsert(List<TagItemLanguage>itemList)
{
TagDataContextdc=newTagDataContext(ConnectionString);
dc.TagItemLanguage.InsertAllOnSubmit(itemList);
ChangeSetresult=dc.GetChangeSet();
dc.SubmitChanges();
returnresult;
}
#endregion

#region=====InsertUpdate=====
///<summary>
///InsertUpdate对象
///</summary>
///<paramname="item">TagItemLanguage对象</param>
///<returns>变更集</returns>
publicChangeSetInsertUpdate(TagItemLanguageitem)
{
TagDataContextdc=newTagDataContext(m_ConnectionString);
varquery=
fromtagItemLanguageindc.TagItemLanguage
wheretagItemLanguage.TagName==item.TagName
&&tagItemLanguage.TagItemId==item.TagItemId
&&tagItemLanguage.LanguageCode==item.LanguageCode
selecttagItemLanguage;

List<TagItemLanguage>itemList=query.ToList();
if(itemList!=null&&itemList.Count>0)
{
item.pkid=itemList[0].pkid;
if(itemList.Count>1)
{
//发现了多条重复的数据.保留第一条,删除其他的条目
itemList.RemoveAt(0);
this.PhysicsDelete(itemList);
WebLog.CommentLog.ErrorLogger.Error(string.Format(@"在TagItemLanguage表发现错误数据.
TagName:{0},TagItemId:{1},LanguageCode:{2},保留pkid:{3},删除其他数据.",
item.TagName,
item.TagItemId.ToString(),
item.LanguageCode,
item.pkid.ToString()));
}
//更新对象
ChangeSetresult=this.Update(item);
returnresult;
}
else
{
//插入对象
ChangeSetresult=this.Insert(item);
returnresult;
}
}

///<summary>
///InsertUpdate对象集合
///</summary>
///<paramname="itemList">TagItemLanguage集合</param>
///<returns>变更集数组:索引0为插入操作变更集,索引1为更新操作变更集</returns>
publicChangeSet[]InsertUpdate(List<TagItemLanguage>itemList)
{
ChangeSet[]result=newChangeSet[2];
List<string>tagNameList=newList<string>();
List<string>languageCodeList=newList<string>();
List<int>tagItemIdList=newList<int>();

foreach(TagItemLanguagetempIteminitemList)
{
if(!tagNameList.Contains(tempItem.TagName))
{
tagNameList.Add(tempItem.TagName);
}
if(!languageCodeList.Contains(tempItem.LanguageCode))
{
languageCodeList.Add(tempItem.LanguageCode);
}
if(!tagItemIdList.Contains(tempItem.TagItemId))
{
tagItemIdList.Add(tempItem.TagItemId);
}
}

//选取所有可能存在于数据库中的集合.
TagDataContextdc=newTagDataContext(m_ConnectionString);
varquery=
fromtagItemLanguageindc.TagItemLanguage
wheretagNameList.Contains(tagItemLanguage.TagName)
&&languageCodeList.Contains(tagItemLanguage.LanguageCode)
&&tagItemIdList.Contains(tagItemLanguage.TagItemId)
selecttagItemLanguage;

//从数据库集合中筛选出需要更新和插入的列表
List<TagItemLanguage>allList=query.ToList();
List<TagItemLanguage>insertList=newList<TagItemLanguage>();
List<TagItemLanguage>updateList=newList<TagItemLanguage>();
foreach(TagItemLanguagetempIteminitemList)
{
List<TagItemLanguage>itemItemList=allList.Where(c=>c.LanguageCode==tempItem.LanguageCode&&c.TagName==tempItem.TagName&&c.TagItemId==tempItem.TagItemId).ToList();
if(itemItemList!=null&&itemItemList.Count>0)
{
tempItem.pkid=itemItemList[0].pkid;
if(itemItemList.Count>1)
{
//发现了多条重复的数据.保留第一条,删除其他的条目
itemItemList.RemoveAt(0);
this.PhysicsDelete(itemItemList);
WebLog.CommentLog.ErrorLogger.Error(string.Format(@"在TagItemLanguage表发现错误数据.
TagName:{0},TagItemId:{1},LanguageCode:{2},保留pkid:{3},删除其他数据.",
tempItem.TagName,
tempItem.TagItemId.ToString(),
tempItem.LanguageCode,
tempItem.pkid.ToString()));
}
updateList.Add(tempItem);
}
else
{
insertList.Add(tempItem);
}
}

//执行操作
result[0]=this.Insert(insertList);
result[1]=this.Update(updateList);
returnresult;

}
#endregion

#region=====Update=====
///<summary>
///更新对象
///</summary>
///<paramname="item">TagItemLanguage对象</param>
///<returns>变更集</returns>
publicChangeSetUpdate(TagItemLanguageitem)
{
TagDataContextdc=newTagDataContext(ConnectionString);
item.Detach();
dc.TagItemLanguage.Attach(item,true);
ChangeSetresult=dc.GetChangeSet();
dc.SubmitChanges();
returnresult;
}

///<summary>
///更新对象集合
///</summary>
///<paramname="itemList">TagItemLanguage集合</param>
///<returns>变更集</returns>
publicChangeSetUpdate(List<TagItemLanguage>itemList)
{
TagDataContextdc=newTagDataContext(ConnectionString);
foreach(TagItemLanguagetempIteminitemList)
{
tempItem.Detach();
}
dc.TagItemLanguage.AttachAll(itemList,true);
ChangeSetresult=dc.GetChangeSet();
dc.SubmitChanges();
returnresult;
}
#endregion

#region=====Delete=====
///<summary>
///物理删除对象
///</summary>
///<paramname="item">TagItemLanguage对象</param>
///<returns>变更集</returns>
publicChangeSetPhysicsDelete(TagItemLanguageitem)
{
TagDataContextdc=newTagDataContext(m_ConnectionString);
item.Detach();
dc.TagItemLanguage.Attach(item);
dc.TagItemLanguage.DeleteOnSubmit(item);
ChangeSetresult=dc.GetChangeSet();
dc.SubmitChanges();
returnresult;
}

///<summary>
///物理删除对象集合
///</summary>
///<paramname="itemList">TagItemLanguage集合</param>
///<returns>变更集</returns>
publicChangeSetPhysicsDelete(List<TagItemLanguage>itemList)
{
TagDataContextdc=newTagDataContext(m_ConnectionString);
foreach(TagItemLanguagetempIteminitemList)
{
tempItem.Detach();
}
dc.TagItemLanguage.AttachAll(itemList);
dc.TagItemLanguage.DeleteAllOnSubmit(itemList);
ChangeSetresult=dc.GetChangeSet();
dc.SubmitChanges();
returnresult;
}
#endregion

#endregion

#regionIDisposable成员

publicvoidDispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

publicvirtualvoidDispose(booldisposing)
{
if(!this.isDisposed)
{
if(disposing)
{
//释放非托管资源
}

//释放托管资源
m_ConnectionString=null;
isDisposed=true;
}
}

~TagItemLanguageDA()
{
Dispose(false);
}

#endregion
}
}

.csharpcode,.csharpcodepre
{
font-size:small;
color:black;
font-family:consolas,"CourierNew",courier,monospace;
background-color:#ffffff;
/*white-space:pre;*/
}
.csharpcodepre{margin:0em;}
.csharpcode.rem{color:#008000;}
.csharpcode.kwrd{color:#0000ff;}
.csharpcode.str{color:#006080;}
.csharpcode.op{color:#0000c0;}
.csharpcode.preproc{color:#cc6633;}
.csharpcode.asp{background-color:#ffff00;}
.csharpcode.html{color:#800000;}
.csharpcode.attr{color:#ff0000;}
.csharpcode.alt
{
background-color:#f4f4f4;
width:100%;
margin:0em;
}
.csharpcode.lnum{color:#606060;}

这个标准的数据访问类中,除了基本的Insert,Select,Update,Delete操作外,需要特别讲解的就是上面的InsertUpdate操作.在我们的数据库中有一个IsDeleted标志位用来表示本条记录是否被逻辑删除.而当用户希望插入另外一条记录的时候,有时候在数据库中并不是Insert操作而仅仅是恢复逻辑删除的数据.

如果每次Insert前,都要先Select看是否存在本条数据,是十分笨重的.所以InsertUpdate提供了"逻辑插入"功能,可以自己判断是需要Insert还是Update.

五.业务逻辑层模板

再来看看我们通用的业务逻辑层对象的代码.则是一个最简单的业务逻辑层对象,其中大部分都是单纯的对数据访问层方法的返回,但是其中有的"逻辑删除"方法是自己特有的.

/*****************************************************************************************
**FileName:TagCategoryLanguageBL.cs
**Creator:ziqiu.zhang
**CreateTime:2008-11-10
**FunctionalDescription:TagCategoryLanguage业务逻辑类
**Remark:
**
**Copyright(c)eLongCorporation.Allrightsreserved.
****************************************************************************************/

usingSystem;
usingSystem.Collections.Generic;
usingSystem.Linq;
usingSystem.Data.Linq;
usingSystem.Text;
usingCom.Elong.DataAccess.Tag;
usingCom.Elong.Model.Tag;
usingCom.Elong.BusinessRules.Common;
usingCom.Elong.Model.Common;

namespaceCom.Elong.BusinessRules.Tag
{
///<summary>
///TagCategoryLanguage业务逻辑类
///</summary>
publicclassTagCategoryLanguageBL:IDisposable
{

#region====================PrivateField====================
privateboolisDisposed=false;
privateTagCategoryLanguageDAtagCategoryLanguageDA;
#endregion

#region====================Property=========================
privatestringm_ConnectionString;
///<summary>
///数据库连接字符串
///</summary>
publicstringConnectionString
{
get{returnm_ConnectionString;}
set{m_ConnectionString=value;}
}
#endregion

#region====================ConstructedMethod===============
///<summary>
///禁止使用
///</summary>
privateTagCategoryLanguageBL()
{}

///<summary>
///构造函数
///</summary>
///<paramname="connectionString">数据库连接串</param>
publicTagCategoryLanguageBL(stringconnectionString)
{
m_ConnectionString=connectionString;
tagCategoryLanguageDA=newTagCategoryLanguageDA(connectionString);
}
#endregion

#region====================BasicMethod====================

#region=====Select=====
///<summary>
///根据主键得到对象
///</summary>
///<paramname="pkid">主键</param>
///<returns>对象</returns>
publicTagCategoryLanguageGetItemByPkid(intpkid)
{
returntagCategoryLanguageDA.GetItemByPkid(pkid);
}

///<summary>
///根据主键列表得到对象集合
///</summary>
///<paramname="pkid">主键列表</param>
///<returns>对象集合</returns>
publicList<TagCategoryLanguage>GetListByPkid(List<int>pkidList)
{
returntagCategoryLanguageDA.GetListByPkid(pkidList);
}
#endregion

#region=====Insert=====
///<summary>
///插入对象
///</summary>
///<paramname="item">TagCategoryLanguage对象</param>
///<returns>变更集</returns>
publicChangeSetInsert(TagCategoryLanguageitem)
{
returntagCategoryLanguageDA.Insert(item);
}

///<summary>
///插入集合
///</summary>
///<paramname="item">TagCategoryLanguage集合</param>
///<returns>变更集</returns>
publicChangeSetInsert(List<TagCategoryLanguage>itemList)
{
returntagCategoryLanguageDA.Insert(itemList);
}
#endregion

#region=====InsertUpdate=====
///<summary>
///InsertUpdate对象
///</summary>
///<paramname="item">TagCategoryRefCity对象</param>
///<returns>变更集</returns>
publicChangeSetInsertUpdate(TagCategoryLanguageitem)
{
returntagCategoryLanguageDA.InsertUpdate(item);
}

///<summary>
///InsertUpdate集合
///</summary>
///<paramname="itemList">TagCategoryRefCity集合</param>
///<returns>变更集</returns>
publicChangeSet[]InsertUpdate(List<TagCategoryLanguage>itemList)
{
returntagCategoryLanguageDA.InsertUpdate(itemList);
}
#endregion

#region=====Update=====
///<summary>
///更新对象
///</summary>
///<paramname="item">TagCategoryLanguage对象</param>
///<returns>变更集</returns>
publicChangeSetUpdate(TagCategoryLanguageitem)
{
returntagCategoryLanguageDA.Update(item);
}

///<summary>
///更新集合
///</summary>
///<paramname="item">agCategory集合</param>
///<returns>变更集</returns>
publicChangeSetUpdate(List<TagCategoryLanguage>itemList)
{
returntagCategoryLanguageDA.Update(itemList);
}
#endregion

#region=====Delete=====
///<summary>
///物理删除对象
///</summary>
///<paramname="item">TagCategoryLanguage对象</param>
///<returns>变更集</returns>
publicChangeSetPhysicsDelete(TagCategoryLanguageitem)
{
returntagCategoryLanguageDA.PhysicsDelete(item);
}

///<summary>
///物理删除集合
///</summary>
///<paramname="item">TagCategoryLanguage集合</param>
///<returns>变更集</returns>
publicChangeSetPhysicsDelete(List<TagCategoryLanguage>itemList)
{
returntagCategoryLanguageDA.PhysicsDelete(itemList);
}

///<summary>
///逻辑删除对象
///</summary>
///<paramname="item">TagCategoryLanguage对象</param>
///<returns>变更集</returns>
publicChangeSetLogicDelete(intpkid)
{
TagCategoryLanguageitem=tagCategoryLanguageDA.GetItemByPkid(pkid);
if(item.IsDeleted!=1)
{
item.IsDeleted=1;
}
ChangeSetresult=tagCategoryLanguageDA.Update(item);
returnresult;
}

///<summary>
///逻辑删除集合
///</summary>
///<paramname="item">TagCategoryLanguage集合</param>
///<returns>变更集</returns>
publicChangeSetLogicDelete(List<int>pkidList)
{
List<TagCategoryLanguage>itemList=tagCategoryLanguageDA.GetListByPkid(pkidList);
foreach(TagCategoryLanguageiteminitemList)
{
item.IsDeleted=1;
}
returntagCategoryLanguageDA.Update(itemList);
}
#endregion

#endregion

#region====================ExtendMethod====================
#endregion

#regionIDisposable成员

publicvoidDispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

publicvirtualvoidDispose(booldisposing)
{
if(!this.isDisposed)
{
if(disposing)
{
//释放非托管资源
tagCategoryLanguageDA.Dispose();
}

//释放托管资源
m_ConnectionString=null;
isDisposed=true;
}
}

~TagCategoryLanguageBL()
{
Dispose(false);
}

#endregion

}
}

六.如何组合应用

上面是TagItemLanguage表的BL(BusinessLogic)和DA(DataAccess)对象,同理TagItem表的TagItemBL和TagItemDA几乎是一模一样的代码.

一个TagItem对象带有一个TagItemLanguage对象集合的属性,经常我们更新一个TagItem需要更新关联的TagItemLanguage对象,这个时侯我们就可以在TagItemBL对象中创建一个方法,先调用TagItemDA更新TagItem表,再调用TagItemLanguageDA更新TagItemLanguage表.

七.经验和总结

使用LINQ第一个挑战是拆分业务逻辑和对象职责,第二挑战就是性能.既然将每个表的更新都放到了具体的对象去做,那么很显然会造成大量的数据库访问请求.比如一开始对于一个集合的逻辑更新,如果循环每条记录去判断是要更新还是删除,那就会造成多次数据库连接.而我的做法是首先群穷举出所有的可能需要的数据条目,都取出来后在程序中进行查询来判断哪些对象需要更新,哪些需要添加,最后执行一次批量更新和批量插入操作.一共访问三次数据库.我虽然加重了传输数据量的传输但是已经大幅减少了数据库连接次数.一般数据库和服务器的通信都是内网的,所以牺牲数据量是值得的,实践证明数据库连接才是最宝贵的资源.

另外我发现LINQTOSQL生成的SQL效率很高,比初级开发人员的SQL效率要高.项目的性能问题从开发的第一天我就开始担忧了.但是没想到居然还可以接受.这也和目前频道初期的访问量没有上来有关系.YY一下,艺龙点评频道即将开始推广活动了,希望到时候大家多多支持!最近看到千橡收购艺龙流通股消息,也为我们这些员工制造了YY的空间.如果真的合作,这么大的SNS流量过来系统是否还撑得住,拭目以待!


.csharpcode,.csharpcodepre
{
font-size:small;
color:black;
font-family:consolas,"CourierNew",courier,monospace;
background-color:#ffffff;
/*white-space:pre;*/
}
.csharpcodepre{margin:0em;}
.csharpcode.rem{color:#008000;}
.csharpcode.kwrd{color:#0000ff;}
.csharpcode.str{color:#006080;}
.csharpcode.op{color:#0000c0;}
.csharpcode.preproc{color:#cc6633;}
.csharpcode.asp{background-color:#ffff00;}
.csharpcode.html{color:#800000;}
.csharpcode.attr{color:#ff0000;}
.csharpcode.alt
{
background-color:#f4f4f4;
width:100%;
margin:0em;
}
.csharpcode.lnum{color:#606060;}

.csharpcode,.csharpcodepre
{
font-size:small;
color:black;
font-family:consolas,"CourierNew",courier,monospace;
background-color:#ffffff;
/*white-space:pre;*/
}
.csharpcodepre{margin:0em;}
.csharpcode.rem{color:#008000;}
.csharpcode.kwrd{color:#0000ff;}
.csharpcode.str{color:#006080;}
.csharpcode.op{color:#0000c0;}
.csharpcode.preproc{color:#cc6633;}
.csharpcode.asp{background-color:#ffff00;}
.csharpcode.html{color:#800000;}
.csharpcode.attr{color:#ff0000;}
.csharpcode.alt
{
background-color:#f4f4f4;
width:100%;
margin:0em;
}
.csharpcode.lnum{color:#606060;}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: