您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
.NET 业务框架开发实战
 
作者:小洋(燕洋天) 来源:博客园 发布于:2014-12-26
  1843  次浏览      19
 

前言:其实这个系列还是之前的".NET 分布式架构开发实战 ",之所以改了名字,主要是因为文章的标题带来了不少的歧义:系列文章中本打算开发一个简化业务发的流程的Framework,然后用这个Framework再来实战,开发一个分布式的应用。改了名字。给大家带来了不便,敬请见谅。

DAL的重构

之前在开发DAL中,提出了一些思想,也设计了一些接口。现在就把DAL的一些设计完善起来。说是“完善”,并不是说把所有的代码都实现,而是把该定义的接口,方法敲定下来。Richard认为,设计一个架构或者Framework的时候,开始是接口的定义,定义好各层之间交互的接口,然后才是具体代码的实现。

因为在设计Framework的时候,首先要考虑这个Framework的使用者是谁,希望他们怎么样来使用开发出来的这个Framework。在这里,Richard很明白:Framework的使用者就是自己公司里的开发人员。而且还要使得开发的使用尽量的方便,不要到处去配置一些文档,最好就是把Framework引入进来,稍微配一下就使用。

在Richard设计的Framework中,就DAL而言,如果希望DAL返回DataTable,DataReader等给BLL,那么需要配置的仅仅只是指明数据库的连接字符串;如果希望DAL返回的数据实体给BLL,那么就得把一张张的表映射成为实体,然后让这些实体继承IDataEntity接口就行了(生成实体可以用ORM工具,或者自己手写代码)。

Richard思考了之前对DAL的设计,在此他做了一些改进。

首先就是对于IDataContext的重新设计和理解:之前的设计是定义了IDataContext,然后用不同的方式实现这个接口,如LinqDataContext.Provider就是用Linq的方法来返回结果(DataResult)。现在Richard认为IDataContext其实就是用来操作数据库的,所以返回的结果就应该是操作数据之后的结果,如Update操作就返回受影响的行数或者是否更新成功。至于是否要把一些额外的信息包装返回给BLL,就不是IDataContext的实现者的事情了。而且Richard还考虑到了需要在一定程度上支持原生的ADO.NET,起码给ADO.NET预留接口。

基于此,Richard就把IDataContext定义为一个接口声明,然后再定义了IDataEntityContext,和IDataTableContext来继承IDataContext,他们的关系图如下:

其中IDataEntityContext使用Linq和Entity Framework来实现,而IDataTableContext就是用ADO.NET的方式来实现。

IDataEntityContext接口的和系列文章中定义的一些方法差不多,但是做了修改。其中有一点要提的就是:ICriteria就是所有条件对象要实现的接口(查询对象也是条件对象的一种)。例如,可以根据相应的条件删除,更新数据。

Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/--> /// <summary>
/// 所有的数据实体执行者实现这个借口
/// </summary>
public interface IDataEntityContext:IDataContext
{
TEntity Add<TEntity>(TEntity entity) where TEntity : IDataEntity;
List<TEntity> Add<TEntity>(List<TEntity> entityList) where TEntity : IDataEntity;

bool Update<TEntity>(TEntity entity) where TEntity : IDataEntity;
bool Update<TEntity>(List<TEntity> entityList) where TEntity : IDataEntity;
bool Update(ICriteria condiftion, object value);

bool Delete<TEntity>(TEntity entity) where TEntity : IDataEntity;
bool Delete<TEntity>(List<TEntity> entityList) where TEntity : IDataEntity;
bool Delete(ICriteria condition);

int GetCount(ICriteria condition);
List<TEntity> Query<TEntity>(ICriteria condition);
List<TEntity> Query<TEntity>(ICriteria condition, int pageIndex, int pageSize, ref int entityCount) where TEntity : IDataEntity;
List<object> Query(ICriteria condiftion);
}

另外就是多了一个 List<object> Query(ICriteria condiftion);方法,之所以有这个方法,Richard考虑到,可能开发人员想要直接自己写SQL语句去执行,如select avg(Count),sum(Name) from Customer...,开发人员可以写任意的语句,所以返回一个实体类不现实,就返回一个List<object>。

还有一点就是关于查询对象的改进:以前仅仅只是定义了查询对象的接口,现在用ICriteria 接口中定义来条件对象,而且还可以在条件对象声明是在对数据操作是否采用事务或者缓存。

Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/--> /// 
    /// 所有的条件对象都要从这个接口继承
    /// 
    public interface ICriteria
    {
        string Name { get; set; }
        bool IsCache { get; set; }
        bool IsTransaction { get; set; }
    }

之后Richard又定义了一个IDataProvider,接口,声明如下 :

Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/--> /// <summary>/// 数据提供者要实现的借口/// </summary>public interface IDataProvider{DataResult<TEntity> Add<TEntity>(TEntity entity) where TEntity : IDataEntity;DataResult<TEntity> Add<TEntity>(List<TEntity> entityList) where TEntity : IDataEntity;

DataResult<TEntity> Update<TEntity>(TEntity entity) where TEntity : IDataEntity;DataResult<TEntity> Update<TEntity>(List<TEntity> entityList) where TEntity : IDataEntity;bool Update(ICriteria condiftion, object value);

DataResult<TEntity> Delete<TEntity>(TEntity entity) where TEntity : IDataEntity;DataResult<TEntity> Delete<TEntity>(List<TEntity> entityList) where TEntity : IDataEntity;bool Delete(ICriteria condiftion);

int GetCount(ICriteria condition);

DataResult<TEntity> GetOne<TEntity>(ICriteria condition) where TEntity : IDataEntity;DataResult<TEntity> GetList<TEntity>(ICriteria condition) where TEntity : IDataEntity;DataResult<TEntity> GetPageData<TEntity>(ICriteria condition, int pageIndex, int pageSize, ref int entityCount) where TEntity : IDataEntity;List<object> GetCustomData(ICriteria condiftion);}

之所以要定义这个接口,其实 Richard就是想让实现了IDataContext的类踏踏实实的去做底层的数据操作,至于数据操作之后的结果以什么形式给BLL,不用IDataContext的实现者来关心,而是用IDataProvider的实现者来关心。

在IDataProvider的实现者在底层就是调用了IDataContext的实现者的方法,然后在IDataProvider中,对外提供了一些更加友好和方便使用的方法,最后在BLL中直接依赖的就是IDataProvider,而不是IDataContext。

另外,对于IDataProvider返回的DataResult也做了一些修改:如果返回的是数据实体,即 使用的是IDataEntityContext来提供底层的数据操作,那么DataResult<TEntity>是没有问题的;但是如果使用的是IDataTableContext,那么返回DataResult<TEntity>就不行了,因为IDataTableContext查询方法可能返回的DataTable,或者DataReader.所以,在设计中叶预留了一个接口:让IDataProvider返回的结果实现IDataResult接口,那么ataResult<TEntity>继承这个接口,主要用来返回数据实体,如下:

业务层初步构想

前言:本篇主要讲述如何把DAL和BLL衔接起来。

DAL的设计就到这里,下一篇文章就开始讲述对业务层的一些思考。

1. DAL和BLL之前的Mapping

首先,业务类和数据实体类不是一 一对应的关系,换句话说,不是一个业务类就一定对应数据库中的一张表。业务类是用只是使用数据实体中的数据而已,所以一个业务类中的数据往往来自多个数据实体。

每个业务类都是有自己的一些属性的,把数据以数据实体或者DataTable的形式从DAL获取之后,BLL类就使用这些数据,BLL不会把这些原生的数据实体暴露给UI。BLL类会把UI中要是用的数据装入到自己的属性中。

所以在这个过程中就有一个赋值的过程,或者称为mapping映射。当Richard提出这个想法后,项目组的同事就问他:为什么要做的这么复杂,还要一 一 的赋值,为什么不直接把数据实体给UI使用,为什么一定要在中间这么转一下呢?

Richard分析了一些原因:

1. 如果直接把数据实体给了UI,那么UI那端就很清楚DAL了,以后数据访问方式从ADO.NET 到了EF,那么UI 就动了,又回到以前了。

2. 在BLL中可以对从DAL取出来的数据进行一些处理,如转换格式,计算,组合等。

Richard想到把BLL和DAL彻底的解耦:业务类中不存在数据实体类的引用。这样设计之后灵活性就很大了。最后达到的效果就是:通过配置,配置业务类每个属性的数据的来源。而这个业务类完全不知道这些数据到底来源于哪个或者哪些数据实体。

这样确实很灵活,Richard兴奋不已。

2. 如何Mapping

初步想法通过配置文件。如现在有一个Product的业务类,定义如下:


Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/-->    public class ProductBL
    {

        public string ProductName { get; set; }

        public decimal Price { get; set; }

        public string Description { get; set; }        

    }

那么如何给这些属性赋值,同时也不引用数据实体。Richard用配置文件来实现的,这里Richard就约定了:配置文件的名字就是“业务类的名字”+“Mapping.xml”.所以Product的配置文件就是ProductBLMapping.xml

<?xml version="1.0" encoding="utf-8" ?><BusinessModel name="ProductBL" mappingTo="DAL.ProductEntity" >
<property name="ProductName" mappingTo="Name" type="System.String"/>
<property name="Price" mappingTo="Price" type="System.Decimal"/>
<property name="Description" mappingTo="Description" type="System.String"/></BusinessModel>

然后再运行的时候就通过反射来赋值。

现在问题又来了:

1. 每次都是通过反射来赋值,性能很成问题。

2. 如果配置文件出错,调试很不方便。

3. 如何处理一个业务类对应对个数据实体的情况,如:

代码

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/-->    public class ProductBL
    {
        public string ProductName { get; set; }
        public decimal Price { get; set; }
        public string Description { get; set; }

        //来自CustomDAL
        public string CustomerName { get; set; }

    }

但是好处很明显:

1. DAL和BLL解耦

2. 很便于查询对象的实现。例如:在UI代码写:

ICriteria condition=CriteriaFactory.Create(typeof(ProductBL).Where("ProductName",  Operation.Equal,"book");

当然ProductName是业务类ProductBL的属性,在查询对象最后解析为SQL语句的时候就可以利用ProductBLMapping.xml来生成SQL。

(注:小洋请大家想想,上面的思想来自于.NET中哪个开源框架?)

对于性能方面,Richard尝试这样解决:

在第一次Mapping的时候,就把这些mapping的信息保存在静态字典中,下次在mapping的时候,就不用再读配置文件了,而且读内存中的字典。

但是这样,随着业务类的增加,内存使用也加大,而且赋值方式还是反射。

3. 再次构思

Richard接着考虑:如何处理一个业务类对应对个数据实体的情况?于是配置文件就改为了:

Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/--><?xml version="1.0" encoding="utf-8" ?><BusinessModel name="ProductBL" ><property name="ProductName" mappingTo="DAL.ProductEntityName" type="System.String"/><property name="Price" mappingTo="DAL.ProductEntityPrice" type="System.Decimal"/><property name="Description" mappingTo="DAL.ProductEntityDescription" type="System.String"/><property name="CustomerName" mappingTo="DAL.CustomerEntity.Name" type="System.String"/> </BusinessModel>

基本的问题算是解决了,但是性能的问题依然存在。

Richard又开始考虑更加好的方式。

业务层Mapping的选择策略

前言:在上一篇文章中提到了mapping,感觉很像在重新实现NHibernate。其实文章的本意是想反映出Richard在思考的时候的一些选择:利用现有的,还是最后自己用别的方式实现。如果一上来就说什么什么好,那太武断了,也很片面,系列文章反复的在强调一点:技术有它的适用场景,没有完美的技术。很多的朋友说本系列在近似的开发一个ORM,其实不是:ORM就是把数据库表转为数据实体,但是本篇中是使用已经转换后的数据实体。是把数据实体的数据给业务类。

开始之前希望在这下面的两点上达到共识:

1. 最好不要把DAL的数据实体(Linq或者Entity Framework生成的),或者原生的DataTable暴露给UI那边(除非一定要,或者有特殊的原因)。

2. UI使用的是BLL类(或者基于消息的Scheme格式)。

而且本篇讨论业务类中的mapping,也就是数据的获取方式,当然,业务类的设计远远不止这些。

Richard思考了配置文件的方式,诚然用配置文件确实灵活,但是灵活也是有代价的,因为Framework最后还得公司的开发人员使用,过多的配置和过高的学习成本使得Framework失去了很大的意义。

Richard开始思考了,想到了还有一种最简单的mapping的方式:就是直接一个个的赋值,如:

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/-->       public class ProductBL
       {
         public string ProductName { get; set; }
         public decimal Price { get; set; }
         public string Description { get; set; }

         public void Mapping(m_Product productEntity)
         {
            this.ProductName = productEntity.Name;
            this.Price = productEntity.Price;
            this.Description = productEntity.Description;
         }
        }

很明显,这个过程很简单却很繁琐。

和之前使用配置文件的方式相比:

优点:

1. 便于使用和理解

2. 便于调试

缺点:

1. 和数据实体耦合的很紧(其实这不算是缺点,这是和之前配置文件的方式比较而言认为缺点)。上面的代码中就直接使用了m_Product.(大家可以参看之前一篇文章中用配置文件的优缺点)

2. 编写的过程很繁琐。全部是手动的mapping。

而且还有关键的一点就是:查询对象怎么生成最终的SQL语句?

例如,下面的代码:

ICriteria condition=CriteriaFactory.Create(typeof(ProductBL).Where("ProductName", Operation.Equal,"book");

如果采用配置文件的mapping方式,很清楚:在配置文件中ProductBL的ProductName对应m_Product实体的Name字段,也就是对应数据库表m_Product的Name字段(因为在BLL中使用的是通过linq或者Entity Framework生成的m_Product实体)。上面的查询对象最后生成类似select * from m_Product where Name=’book’的语句。

Richard想到NHibernate的实现:在NHibernate也有查询对象,在NHibernate中的查询对象的实现也是依赖NHibernate的那个mapping的配置文件的。

并不是说没有查询对象就不行,不用查询对象,用Linq和Entity Framework也是可以实现的。但是数据层就没有“以不变应万变”了的效果,而且开发人员要掌握各种的数据访问技术:ADO.NET, Linq等。(可以参看.NET 分布式架构开发实战之三 数据访问深入一点的思考一文)。

现在Richard面临的问题就是:

1. 不用配置文件mapping,这样查询对象就不好实现。

2. 手动的敲入代码mapping,重复的劳动。

Richard思考是否更好的方式解决上面的问题。于是第三种方式就产生了。

3. 第三种Mapping方法。

第三种mapping的方法就是综合了之前两种mapping的优点,而避开了他们的缺点。

Richard想到解决手动mapping的方法就是:图形化的代码生成来代替手写代码。而且要想办法保存数据库字段的一些信息。

很巧的就是:linq和EF生成的实体中的字段信息就反映了数据表字段的信息。这点可以利用起来。下面的草图是用Visio画出的,代表了Richard的想法。其实Richard也没有一下就开发出下面的工具,一切还是处于设计阶段。

Richard设计出了自动生成代码的工具(工具的开发Richard思考过了,可以采用最简单的实现方式:一个Windows程序。也想过用DSL工具开发,但是DSL得学习过程还是有点复杂的)。

注:虽然说是代码生成工具,其实一开始Richard也是想的很简单:就是一个写文本的操作。

在上面的界面中,选择要和哪个数据实体类mapping,可以通过选择“MappingName”来实现。然后点击“Properties”按钮,出现了如下的界面:

这是一个专门用来配置mapping的界面:点击“Add”按钮,添加一个业务类的属性,然后用”MappingTo”来设置这个属性的数据从数据实体类的那个字段中获取。在选择数据实体字段的时候,也把这个选中数据实体的字段信息保存起来,供给之后的查询对象使用。

基本思路Richard已经有了。现在的问题就是把上面选选中数据字段信息保存在哪里,而且还得和业务类的属性对应,例如,Id对应业务类Product的ProductId,而不是其他的属性。

在mapping的时候,一般是在业务类中定义一个属性,然后赋值:

public string ProductId { get; set; }

  this.ProductName = productEntity.Id;

为了保存数据实体字段的信息,业务类的属性声明就改为下面了:

代码

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/-->public static readonly PropertyInfo 
ProductIdProperty = RegisterProperty(
      typeof(Product),
      new PropertyInfo("ProductId",typeof(M_Product)","Id")); 

    public string ProductId
    {
      get { return ReadProperty(ProductIdProperty); }
      set { LoadProperty(ProductIdProperty, value); }
    }

上面的代码通过生成的方式就比较方便,而且上面的属性声明还有更多其他的用途。初一看和WPF中依赖属性很像,确实思路也是从WPF借鉴而来的。这里简称“Mapping属性”。

今天就写到这里,真是对不住大家,因为本篇写的比较的啰嗦,而且还没有写完。下篇讲述Mapping属性的实现原理和原因,就是为什么要是用ProductIdProperty那种声明方式。

Mapping属性原理和验证规则的实现策略

前言:之前的讨论一直关注在怎么从DAL中获取数据,以及数据的Mapping问题。实际上,一个业务框架最主要的作用就是简化业务逻辑的编写和开发。

1. 框架的借鉴

一个框架的产生不是那么简单的,有很多的问题需要Richard去考虑:

1.避免重新造轮子

2.借鉴现有的成熟的框架的思想

在开发的过程中,Richard一直使用Visual Studio IDE开发。而且每次随着VS新版本的发布,总是伴随着新技术的产生。很多的时候,开发人员只是关注在新技术的使用和学习上。但是对于新技术,还有另外一方面是很值得关注的:实现的原理,和为什么这样实现,即,思想。新技术,毫无疑问是一些大师们思考的结果,从他们的思想中借鉴,益处是很大的。

在Richard学习的过程中,有一个地方特别引起来他的关注:那就是依赖属性概念的提出,先是WPF,然后在他学习WF的时候,也看到了依赖属性的再次使用。他考虑,把依赖属性的思想使用到自己正在开发的业务框架中来。

首先,他分析了现在的依赖属性的实现方式(以WPF为例),

代码

Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/-->public class FrameworkElement: UIElement, ...

{

public static readonly DependencyProperty MarginProperty;

...

}

public Thickness Margin
{

set { SetValue(MarginProperty, value); }

get { return (Thickness)GetValue(MarginProperty); }

}

static FrameworkElement()
{

FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata(

new Thickness(), FrameworkPropertyMetadataOptions.AffectsMeasure);

MarginProperty = DependencyProperty.Register("Margin",

typeof(Thickness), typeof(FrameworkElement), metadata,

new ValidateValueCallback(FrameworkElement.IsMarginValid)); ...

}

在Richard一直认为,一个属性的声明是很简单的,而且长期以来Richard都一直使用下面的方式:

public Thickness Margin
{

       get;set;

}

比较而言,依赖属性最大的好处就于:给普通的属性提供更加多的信息,而且提供了更多的功能:验证,触发回调事件。当然,使用普通的属性也能达到:

public Thickness Margin
{

       get{return margin;}
       set
       {

              if(margin<0)
                     ...
              ...
       }
}

相比而言,依赖的属性的方式更加优雅,而且扩展性也好。

还有一点比较重要的就是,一旦把一个属性变为依赖属性,那么.NET Framework就开始管理这个属性,如自动的验证,值的改变和跟踪。这样就把任务交给了Framework,开发人员做事情就比较方便了。

Richard想起了之前在开发业务类中遇到的问题:例如下面的代码:

public class Product
{

        public string ProductName{get;set;}

        public double Price{get;set;}

}

很多的时候,在增加或者更新一个Product的时候,由于逻辑的需要,往往要判断ProductName不为空,而且Price要大于零等。所以每次都需要写代码判断:

public void Add()
        {

              if(string.IsNullOrEmpty(this.ProductName){...}

              if(this.Price<0){...}

        }

问题还不止这些,如果在其他的业务类中也需要同样,而且类似的验证,那只有一行行的写类似的代码,最好的情况就是copy一些代码。

这样写代码确实很累,后面Richard也想用一些方式来改进,用到了Enterprise Library中的Validation验证模块,于是代码就变成了下面的样子:

public class Product
{

        [NotNullValidator]

        public string ProductName{get;set;} 

        public double Price{get;set;}    

}

使用声明的开发,AOP的思想,其实这样的方式相比之前而言,确实已经很不错了。在把业务类的数据保存的时候只要调用Validation验证模块的Validate()方法就行了。确实很方便,但是存在的问题就是:每次调用Validate()方法时候,就会把这个业务类的所有属性都会检查一遍(那些加了验证标签的属性),这样,性能方面不好,而且还不能针对某一个属性单独的验证。

2. 综合考虑

Richard还考虑到了另外的一点:之前一直在解决mapping的问题,说到底就是把从DAL中拿到的数据赋值给业务类的属性。而且还要基于业务类创建查询对象,最后把查询对象解析为SQL语句,所以还要保存业务属性和DAL中数据实体属性的对应关系,即哪个业务属性对应哪个数据实体属性(也是表字段)。

综合上面的考虑,Richard决定把依赖属性的优势利用起来(自动的验证,数据改变跟踪,另外加上权限的验证),而且给依赖属性更多的元数据信息:把mapping的字段信息保存在依赖属性中。所以,现在属性的声明如下:

代码

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/-->public static readonly PropertyInfo ProductIdProperty = RegisterProperty(
            new PropertyInfo("ProductId",typeof(M_Product)","Id")); 

    public string ProductId
    {

      get { return ReadProperty(ProductIdProperty); }
      set { LoadProperty(ProductIdProperty, value); }
    }

在上面的属性声明中,就指定从业务类(如,Product)的属性从哪个数据实体(typeof(m_Product))的哪个属性(如,Id)取值。

RegisterProperty就是把属性的信息保存在一个字典中:

Dictionary<Type,List< IPropertyInfo>>

其中PropertyInfo继承了IPropertyInfo接口。

最后的结果就是:所有业务类的mapping属性都被保存在了一个全局的静态字典中。

另外还有一个全局的静态字典用来保存每个属性所对应的验证规则:

Dictionary< IPropertyInfo,List<ICheckRule>>

所有的验证规则都是从ICheckRule接口继承。

一个比较强大的属性就产生了。当然,在mapping属性中的验证只是基本的验证,还有更加复杂的业务验证将会放在其他的地方,实现方式或者类似WPF那么:采用回调,如new ValidateValueCallback(FrameworkElement.IsMarginValid)。

所以,借鉴于mapping属性就解决了三个问题:

1.mapping和查询对象的实现

2.部分验证规则的声明

3.业务属性的管理

   
1843 次浏览       19
 
相关文章

深度解析:清理烂代码
如何编写出拥抱变化的代码
重构-使代码更简洁优美
团队项目开发"编码规范"系列文章
 
相关文档

重构-改善既有代码的设计
软件重构v2
代码整洁之道
高质量编程规范
 
相关课程

基于HTML5客户端、Web端的应用开发
HTML 5+CSS 开发
嵌入式C高质量编程
C++高级编程
最新课程计划
信息架构建模(基于UML+EA)3-21[北京]
软件架构设计师 3-21[北京]
图数据库与知识图谱 3-25[北京]
业务架构设计 4-11[北京]
SysML和EA系统设计与建模 4-22[北京]
DoDAF规范、模型与实例 5-23[北京]

使用decj简化Web前端开发
Web开发框架形成之旅
更有效率的使用Visual Studio
MVP+WCF+三层结构搭建框架
ASP.NET运行机制浅析【图解】
编写更好的C#代码
10个Visual Studio开发调试技巧
更多...   

.NET框架与分布式应用架构设计
.NET & WPF & WCF应用开发
UML&.Net架构设计
COM组件开发
.Net应用开发
InstallShield

日照港 .NET Framework & WCF应用开发
神华信息 .NET单元测试
北京 .Net应用软件系统架构
台达电子 .NET程序设计与开发
赛门铁克 C#与.NET架构设计
广东核电 .Net应用系统架构
更多...