思归呓语

衣带渐宽终不悔,为伊消得人憔悴
随笔 - 413, 评论 - 2971, 引用 - 245

导航

关于

标签

每月存档

最新留言

广告

【第1页/共28页,413条】
首页
前页
1
...
2009年08月18日

【原文地址】Code Only Enhancements
【原文发表日期】 03 August 09 11:11

自从第一个预览版发布之后,我们一直在奋力增强Code Only功能。

在下一个版本中,你将能够指定
  1. 导航属性的倒转(Inverse)关系
  2. 属性的细节(Facets),象like Nullability(可null性), MaxLength(最大长度), Precision(精度)等等
  3. 属性与字段间的映射
  4. 类型与数据表间的映射
  5. 继承策略
  6. 配置的封装

本贴的余下部分将依次对这些特性进行详述。

注册导航属性的倒转关系:

你现在可以注册倒转关系,即一个导航属性到另一个导航属性的倒转(inverse)关系,象这样:

builder.RegisterInverse(
       (Customer c) => c.Orders,
       (Order o) => o.Customer)
);

这代码表示Customer.OrdersOrder.Customer关系的另一头。把order1 加到customer1.Orders集合中去,与把order1.Customer设置成customer1具有同等的效果。

指定属性细节(Facets):

你还可以指定属性的细节,即象Nullability(可null性), MaxLength(最大长度), Precision(精度)等等这样的东西,象这样:

var customerConfig = new EntityConfiguration<Customer>();
// 我们可以推断出ID是主键
// 但无法推断出它在插入时是在数据库生成的
customerConfig.ForProperty(c => c.ID)
              .Identity();
customerConfig.ForProperty(c => c.Name)
              .MaxLength(100)
              .NonUnicode();
customerConfig.ForProperty(c => c.Website)
              .MaxLength(200)
              .Nullable()

builder.Configure(customerConfig);

这把Customer类型配置成:

  • ID 属性是Identity字段,即在我们插入数据到数据库时,其值是由数据库计算出来的。
  • Name属性,其MaxLength100个字符,是NonUnicode(非Unicode),即在SQL Server中是VARCHAR而不是NVARCHAR
  • Website属性,其MaxLength200个字符串,是Nullable

这些细节是针对概念模型Conceptual Model,即CSDL)的,从那里,也传到数据库(即SSDL)。

封装细节配置

你可以创建一个EntityConfiguration<T>的继承类来封装所有这些配置。

例如:

public class CustomerConfig: EntityConfiguration<Customer>
{
    public CustomerConfig(){
       ForProperty(c => c.ID)
                 .Identity();
       ForProperty(c => c.Name)
                 .MaxLength(100)
                 .NonUnicode();
       ForProperty(c => c.Website)
                 .MaxLenght(200)
                 .Nullable();
    }
}

我们建议你创建象这样的类,而不是配置EntityConfiguration<>,因为封装的好处。

指定数据表名

在你使用Configure<T>(..) 时,实体框架为你推断出默认的映射,继承策略(TPH)和表名。

但如果你要指定表名,你可以这么做:

var customerConfig = new EntityConfiguration<Customer>();
// 象上面那样配置细节
...

// 用一个特定的表名注册配置
builder.Tables[“dbo.Custs”] = customerConfig;

指定映射:

如果你需要对映射更多的控制(例如,要映射到一个现有的数据库或者使用企业的命名规则),那么你可以象这样指定映射:

EntityMap<Customer> customerMap = 
    Map.OfType<Customer>(
        c => new {
            cid = c.ID,
            c.Name,
            csite = c.Website
        }
    );

映射解释

这个映射表明,ID是映射到‘cid’字段,Name属性是保存到‘Name’字段,而Website是映射到‘csite’ 字段的。

没有被引用的属性是不会被持久化的,就象使用实体框架默认的代码生成时生成的部分类上的属性一样。

LINQ内涵句法(Comprehension Syntax)

你甚至也可以使用LINQ内涵句法来指定同样的事情:

EntityMap<Customer> customerMap =
    from c in Map.OfType<Customer>()       
    select new {
            cid = c.ID,
            c.Name,
            csite = c.Website
        };

用映射指定细节(Facets)

在配置完映射后,你还可以象这样在映射上指定facets:

customerMap.ForProperty(c => c.ID)
           .Identity();
customerMap.ForProperty(c => c.Name)
           .MaxLength(100)
           .NonUnicode();
customerMap.ForProperty(c => c.Website)
           .MaxLenght(200)
           .Nullable();

指定数据表

最后一步是指派数据表映射。

builder.Tables[“dbo.Custs”] = customerMap;

至此,我们为Customer类指定了自定义表,映射和自定义facets。

指定继承:

CodeOnly默认所用的继承策略是Table Per Hierarchy (每个类分层结构共用一个数据表)(或 TPH)。

但如果你需要一个不同的策略,你需要动手配置相应映射

设想一下,如果你要映射三个类:Vehicle , CarBoat,其中CarBoat是从Vehicle继承而来,而Vehicle本身是个抽象类。

clip_image001

Table Per Hierarchy (TPH,每个类分层结构共用一个数据表)

如果你想要使用TPH做映射,你可以这么做:

var vehicleMap = 
    Map.OfTypeOnly<Vehicle>(
        v => new {
            vid = v.ID,
            v.Name,
            vdesc = v.Description
            v.MaxPassengers,
        }
    ).Union(Map.OfTypeOnly<Car>(
        c => new { 
            vid = c.ID, 
            c.Name,
            vdesc = c.Description 
            c.MaxPassengers,
            trans = c.Transmission,
            tspd = c.Topspeed,
            ccty = c.EngineCapacity,
            ncyld = c.NoCylinder,
            discriminator = “CAR”
        })
    ).Union(Map.OfTypeOnly<Boat>(
        b => new { 
            vid = b.ID, 
            b.Name,
            vdesc = b.Description 
            b.MaxPassengers,
            lng = b.Length,
            b.HasSail,
            b.HasEngine
            discriminator = “BOAT”
        })
    );

builder.Tables[“dbo.vehicles”] = vehicleMap;

在TPH映射中:

  1. OfTypeOnly() 用来创建映射片段。
  2. 然后,映射片段可以联合(union)起来,这样,它们就可以指派到一个表上(TPH中整个类分层结构共用一个数据表)。
  3. 每个非抽象类型需要一个鉴别器字段(discriminator column),该字段可以是任何名称,但类分层结构中的每个非抽象类型必须有一个不同的常数(即“CAR”)。
  4. 在映射继承类时,你必须重新映射基类中已经映射了的所有属性。

Table Per Type (TPT,每个类型一个表)

如果你想要使用Table Per Type (每个类型一个表) 或TPT来映射同个类分层结构,你该这么做:

builder.Table[“dbo.Vehicles”]= 
    Map.OfType<Vehicle>(
        v => new {
            vid = v.ID,
            v.Name,
            vdesc = v.Description
            v.MaxPassengers,
        }
    );

builder.Tables[“dbo.Cars”] = 
    Map.OfType<Car>(
        c => new {
            cid = c.ID,
            trans = c.Transmission,
            tspd = c.Topspeed,
            ccty = c.EngineCapacity,
            ncyld = c.NoCylinders,
         }
     );

builder.Tables[“dbo.Boats”] = 
    Map.OfType<Boat>(
        b => new {
            bid = b.ID,
            lng = b.Length,
            b.HasSail,
            b.HasEngine
        }
    );

在TPT映射中:

  1. OfType<>()用于每个映射“片段”。
  2. 每个映射“片段”指派到不同的表上。
  3. 每个映射“片段”只映射在当前类上声明的属性,除非。。。
  4. 键属性必须在每个片段中映射(这允许参与该类型的表间的JOIN)。
  5. 没有鉴别器字段。

Table Per Class (TPC,每个类一个表,【译注】常规的说法是,每个非抽象类或每个具体类一个表)

你还可以象这样使用TPC来映射这个类分层结构:

builder.Tables[“dbo.Cars”] = 
    Map.OfTypeOnly<Car>(
        c => new {
            cid = c.ID,
            c.Name,
            vdesc = c.Description
            c.MaxPassengers,
            trans = c.Transmission,
            tspd = c.Topspeed,
            ccty = c.EngineCapacity,
            ncyld = c.NoCylinders,
         }
     );

builder.Tables[“dbo.Boats”] = 
    Map.OfTypeOnly<Boat>(
        b => new {
            bid = b.ID,
            b.Name,
            description = b.Description
            b.MaxPassengers,
            lng = b.Length,
            b.HasSail,
            b.HasEngine
        }
    );

在TPC映射中:

  1. 跟TPH一样,我们使用OfTypeOnly(..)
  2. 但跟TPH不一样,每个非抽象的类型拥有自己的表(所以,Vehicle没有自己的表,因为它是个抽象类)。
  3. 每个映射片段重新映射每个持久(non-transient)属性。
  4. 类分层结构中的抽象类没有映射。
  5. 没有鉴别器字段。

默认的外键的位置:

如果我们看到一个引用(譬如Order.Customer),我们假定其多重性(multiplicity)为0..1。意即,其外键或FK是可null的。

如果我们看到一个集合(譬如Customer.Orders),我们假定其多重性为多(many)

然后,在注册倒转(inverse)时,我们知道一个关系的两头的多重性,譬如,在上面的例子中,我们知道,每个Order可以有0..1个 Customer,每个Customer可以有多个Order

所以按约定,共有三种主要的关系类型,我们需要推断出其FK的位置:

0..1 to many –> 按约定,我们把FK放在many一端,所以上面的Customer.Orders例子中, FK放在Orders 表上。 

many to many –> 没什么选择,只能引进一个连接表(join table)。

0..1 to 0..1
–> 你可以配置FK应该在什么地方,但不配置的话,我们会引进一个连接表(join table)。

但有时候,引用可以不是0..1,而是1,例如,FK(不管它在什么地方),也许不可以null的。

你可以这样来指定FK不是nullable的:

var orderConfig = builder.Configure<Order>();
orderConfig.RegisterInverse(o => o.Customer, c => c.Orders);
orderConfig.ForProperty(o => o.Customer).NonNullable();


这告诉我们,每个Order正好有一个1Customer,而每个Customer多个Orders

能够区分一个引用是非Nullable,会引进几个新的多重性组合,对此,我们也需要约定:

1 to many –> 按约定,我们把FK放在many的一端, 并且在数据库中将其设置为非nullable。

0..1 to 1 –> 按约定,我们把FK放在1的一端, 并且将其设置为非nullable。

1 to 1 –> 跟0..1 to 0..1一样,我们无法决定该把FK放在何处,所以按约定,我们要引进一个连接表(join table)。

指定外键(FK)映射:

至此,我们为实体的属性创建了映射。

那么导航属性和外键怎么办?

所有的关系类型(除了many to many)可以不用数据库中的连接表(join table)来建模,所以我们允许你象这样,作为EntityMap的一部分来映射外键:

EntityMap<Customer> customerMap =
    from c in Map.OfType<Customer>()       
    select new {
            cid = c.ID,
            c.Name,
            csite = c.Website,
            salesPersonFK = c.SalesPerson.ID
        };

customerMap.RegisterInverse(c => c.SalesPerson, s => c.Clients);
builder.Tables[“dbo.Custs”] = customerMap;

这指定,Customer.SalesPerson导航属性,以及它的倒转(inverse)SalesPerson.Customers是保存在 dbo.Custs表的salesPersonFK字段中的。因为映射片段把 salesPersonFK 字段映射到了c.SalesPerson.ID上,而 SalesPerson.ID是相关的SalesPerson实体的主键(或者是主键的一部分),当然,外键是指向主键的。

指定连接表(Join Table)映射:

在many to many关系的情形下,你必须有一个连接表,所以按约定,没有映射信息,我们就会产生一个连接表(join table)。

但假如你需要更多的控制,你可以这么做:

var blogPostsMap = new AssociationMap<Blog, Post>(
     b => b.Posts
).Map(
    (b, p) => new {BlogId = b.ID, PostId = p.ID}
);

builder.Tables[“dbo.BlogPosts”] = blogPostsMap;

这是说, BlogPost间的many to many关系是保存在dbo.BlogPosts表中的,它有2个字段:

  • BlogId’ 是个外键,指向BlogID属性映射到的字段。
  • PostId’ 是个外键,指向PostID属性映射到的字段。

分割实体(Entity Splitting):

Code Only甚至还支持象分割实体这样高级的映射策略:

builder.Tables[“dbo.Customer”] = Map.OfType<Customer>(
   c => new {
      cid = c.ID,
      c.Name,
      active = c.IsActive
   }
);

builder.Tables[“dbo.CustomerDetails”] = Map.OfType<Customer>(
    c => new {
       cid = c.ID,
       c.Size,
       c.Industry
    }
);

这是说,Customer实体是分开保存在dbo.Customerdbo.CustomerDetails两个表中的。

封装所有的配置:

你还可以编写一个类,从EntityMap<T>继承而来,含有所有的映射,facets等等。例如,下面是一个类,包含了Product的配置:

public class ProductMap: EntityMap<Product>
{
    public ProductMap{
         this.Map( p => new {
              pid = p.ID,
              pcode = p.Name,
              cid = p.Category.ID
         });
         this.ForProperty(p => p.ID).Identity();
         this.ForProperty(p => p.Name).MaxLength(100)
             .NonUnicode();
         this.ForProperty(p => p.Category).NonNullable();
         this.RegisterInverse(p => p.Category,
                              c => c.Products);   
     }
}

这是个高度推荐的做法,因为配置Product类型变得容易之极:

builder.Tables[“dbo.Products”] = new ProductMap();

结束语:

正如你看到的,我们正在计划许多的增强,以允许最核心的场景。

你觉得怎么样?你喜欢这些API么?有什么东西你想要改变的?

一如既往,我们期待听到你的反馈。

Alex James
微软Entity Framework开发团队的Program Manager

本贴是Entity Framework开发团队的透明设计实践的一部分。想了解其工作原理,以及你的反馈是如何被使用的,请参阅 这个贴子

posted on 2009-08-18 11:19:34 by saucer  评论(1) 阅读(3886)

 
2009年08月17日

【原文地址】Feature CTP Walkthrough: Code Only for the Entity Framework
【原文发表日期】 22 June 09 02:54 PM

【译注】.NET 3.5 SP1中的Entity Framework着重于“数据库优先(Database First)”,即从数据库中逆工程生成实体数据模型。而Visual Studio 2010/.NET 4.0中的Entity Framework 4.0引进了“模型优先(Model First)”,即允许你先生成实体数据模型,然后生成映射到该模型的数据库。而Entity Framework Feature CTP1版则更进一步,推出了“只用代码(Code Only)”的预览版,不再需要EDMX文件。 相关的想法请参考这个贴子。你需要下列软件才能运行这个示范:
  1. Visual Studio 2010 Beta 1
  2. Entity Framework Feature CTP1 (同时参考Entity Framework Feature CTP 1一文,如果你对该CTP中其他特性,特别是POCO模板等,感兴趣的话)
  3. 本地的SQL Server 2008 Express或者SQL Server

Code Only全程示范

1) 创建一个名叫“"CodeOnlyWalkthru"”的控制台应用:

blog1

2) 往"CodeOnlyWalkThru"解决方案中加一个新的项目:

blog1

3) 选择“类库”,将其命名为"Entities":

blog1

 

4) 在你的Entities项目中加一个"Category"类:

右击Entities项目,加一个名叫“"Category"”的类,然后将下面的代码粘贴入该类:

public class Category
{
private List<Product> _products;
public int ID { get; set; }
public string Name { get; set; }
public virtual List<Product> Products {
get
{
if (_products == null)
_products = new List<Product>();
return _products;
}
set
{
_products = value;
}
}
}

 

5) 生成"Product"类:

在"Category.cs"类中,右击还不存在的Product类名,从上下文菜单上选择"Generate > Class(生成类)":

blog1

在新的Product类中,粘贴入下面这些代码:

public class Product
{
public int ID { get; set; }
public string Name { get; set; }
}

至此,你的项目应该象这样:

blog1

我们把这些实体类放进了一个单独的项目,这样它们会被编译进一个对Entity Framework无依赖的程序集中。因此,Entities程序集对持久性是透明的,这对一些开发人员来说,是非常重要的。对持久性有意识的代码存在于另外的程序集中,引用对持久性是透明的程序集。

6) 在"CodeOnlyWalkThru"项目中,添加对Entities项目的引用:

blog1

7) 在"CodeOnlyWalkThru"项目中,添加对"System.Data.Entity" 和 "Microsoft.Data.Entity.Ctp"的引用:

blog1

image

注: 从长远看来,我们计划将“Code Only”的功能并入核心的Entity Framework程序集。之后,你就只需第一个引用。

8) 在"CodeOnlyWalkThru" 项目中,加一个新的叫"ProductDBContext"的类:

 

blog1

9) 将下面的代码加到"ProductDBContext"类中:

public class ProductDBContext: ObjectContext
{
public ProductDBContext(EntityConnection connection)
: base(connection, "ProductDBContext")
{
ContextOptions.DeferredLoadingEnabled = true;
}
public IObjectSet<Category> Categories
{
get { return CreateObjectSet<Category>(); }
}
public IObjectSet<Product> Products
{
get { return CreateObjectSet<Product>(); }
}
}

因为这个类扩充了ObjectContext,它代表你的模型的形状,起通往你的数据库的入口的作用。注意,我们为上面两个实体类型创建了对应的ObjectSet集。 我们还加了一个构造器,它接受一个EntityConnection参数。ContextBuilder会制造一个EntityConnection (EntityConnection封装了实际的数据库连接和Entity Framework元数据(metadata)),在我们要求它生成一个ProductDBContext新实例时,会将该参数传给这个构造器。在构造器中,我们还配置Entity Framework启用DeferredLoading(即LazyLoading-懒式装载)。

10) 然后将下面的代码粘贴进"Program" 类:

SqlConnection connection = new SqlConnection(@"Data Source=.\SQLEXPRESS;Initial Catalog=ProductDB;Integrated Security=SSPI;");
using (ProductDBContext context = ContextBuilder.Create<ProductDBContext>(connection))
{
if (!context.DatabaseExists())
context.CreateDatabase();
Category food = new Category { CID = 1, Name = "Food" };
Product bovril = new Product { ID = 1, Name = "Bovril" };
Product marmite = new Product { ID = 2, Name = "Marmite" };
Product vegemite = new Product { ID = 3, Name = "Vegemite" };
food.Products.Add(bovril);
food.Products.Add(marmite);
food.Products.Add(vegemite);
context.Categories.AddObject(food);
context.SaveChanges();
// Query the database
food = context.Categories.Single();
foreach (var product in food.Products)
Console.WriteLine(product.Name);
}

 

上面的代码按约定创建了一个上下文,连向本地安装的SQLExpress中的ProductDB数据库。如果ProductDB数据库还不存在,就创建该数据库。

最后,在上下文开始运行后,你就象平常一样使用它,在这个例子中,我们创建了一个Category实例,三个Product实例,两者互相关联,然后我们从数据库中获取Category对象,然后使用LazyLoading(即DeferredLoading),对它的产品进行循环。

就是这么简单!

11) 配置ContextBuilder

迄今为止,我们是按约定来做一切的,因为Code Only可以按约定推断出任何东西。但如果我们将Category类改成象这样的话:

public class Category
{
private List<Product> _products;
public int CID { get; set; }
public string Name { get; set; }
public virtual List<Product> Products {
get
{
if (_products == null)
_products = new List<Product>();
return _products;
}
set
{
_products = value;
}
}
}

CodeOnly不知道CID是键(key)【见注】,这意味着你需要创建一个ContextBuilder实例,用它来将CID注册成键,象这样:

var builder = new ContextBuilder<ProductDBContext>();
builder.RegisterKey((Category c) => c.CID);

如果需要的话,你可以在同个Builder上注册许多个键,你配置完你的Builder之后,你可以使用它的Create方法来创建一个ProductDBContext实例:

using (ProductDBContext context = builder.Create(connection))

 

其它的代码没变。

一般来说,在需要创建ObjectContext时,你应该重用你配置的ContextBuilder,而不是每次都重新配置,这会对性能有帮助。最好的做法大概是通过一个静态变量来做。

【注】按约定,任何叫ID, Id, ClassNameID 或 ClassNameId的属性会被假定为实体键。如果找不到这些名称的属性的话,那么你需要调用RegisterKey(),来告诉Entity Framework哪个属性是键。

Beta1版的CTP中Code Only功能的已知限制:

这个版本是一个非常早期的Code Only预览版,因此它有许多已知的限制,我们计划在以后的版本中解决它们,这些限制包括:

  • 不支持复杂类型(ComplexType)
  • 不支持可配置的映射
  • 只支持Table per Hierarchy继承策略
  • 不支持Facet的指定,譬如,你无法指定数据库中一个字符串字段的最大长度,而只用默认值
  • 没有提供器模型(Provider Model),所以CodeOnly 目前只支持SqlServer
  • CodeOnly没有提供方式让你指定哪个属性参与了同个关系,但只是对方的倒转(inverse)。譬如,如果在上面的例子中,你往Product类中加了一个Category属性, 结果在Product和Category间,会有2个关系,因此在Products表中会生成2个外键,因为Entity Framework无法推断Product.Category和Category.Products只是对方的倒转。
  • 不支持多对多关系

- Alex James
Entity Framework的Program Manager

posted on 2009-08-17 11:05:11 by saucer  评论(0) 阅读(3805)

 
2009年08月16日

【原文地址】Using Repository and Unit of Work patterns with Entity Framework 4.0
【原文发表日期】 16 June 09 04:08 PM

如果你一直在关注这个博客的话,你知道我最近在讨论我们加到Entity Framework 4.0中的POCO功能的方方面面,新加的POCO支持促成了在Entity Framework中实现透明性持久的新方式,而该方式在Entity Framework 3.5中是无法实现的。

如果你错过了我的POCO系列,为方便起见,我在下面列了出来,快速浏览一下也许是个不错的主意。

POCO in Entity Framework : Part 1 – The Experience(【翻译】实体框架中的POCO支持 - 第一部分 - 体验

POCO in Entity Framework : Part 2 – Complex Types, Deferred Loading and Explicit Loading (【翻译】实体框架中的POCO支持 - 第二部分 - 复杂类型,延迟装载和显式装载

POCO in Entity Framework : Part 3 – Change Tracking with POCO (【翻译】实体框架中的POCO支持 - 第三部分 - POCO的变动跟踪

在这个贴子里,我想要看一下如何将我们的例子做进一步扩充,使用一些诸如RepositoryUnit Of Work这样常见的模式,这样我们可以在我们的例子中实现一些特定于持久性的关注。

将我们基于Northwind的例子做一下扩充,譬如说,我有兴趣支持下列与Customer(客户)实体相关的操作:

  • 通过ID来查询客户
  • 通过名字来查询客户
  • 往数据库中加一个新客户

我还想能够基于ID来查询产品。

最后,给定一个客户,我想要能够往数据库中加一个Order(订单)。

在进入细节之前,我想先说明两件事情:

  • 处理这个问题,“正确”的方式不止一个。在过程中,我会做许多简化的假设,其目的是展示一幅非常高层次的草图,示范如何实现这2个模式来解决手上的问题。
  • 通常来说,在使用TDD时,我会从测试开始,使用我的测试来逐步展开我的设计。因为在这个例子中我没有按TDD做,请耐心点,如果你看到我在做象预先定义接口这样的事情,而不是让测试和共用的东西来主宰接口的必要性,等等。

实现 Repository 模式

让我们先开始实现针对Customer实体的操作,看一下Customer的repository的样子:

public interface ICustomerRepository
{
Customer GetCustomerById(string id);
IEnumerable<Customer> FindByName(string name);
void AddCustomer(Customer customer);
}

这个repository接口看上去满足有关Customer的所有需求:

  • GetCustomerById 应该允许我通过主键来得单个客户实体
  • FindByName 应该允许我查询客户
  • AddCustomer 应该允许我往数据库中加一个客户

这暂时听上去不错,象这样,为你的repository定义一个接口是个好主意,特别是你对使用mocks(模拟对象) 或 fakes (假冒对象)来编写测试感兴趣的话,接口允许你完全不用考虑数据库,促成更好的单元测试。在将来会有更多的博客贴子讨论可测试性,mocks 和 fakes,等等。

你也许可以进一步扩展这个接口定义,定义一个共用的IRepository,来处理多个repository类型中共有的关注。如果对你有用的话,这是件值得做的事。在这个特定的例子中,我没看到其必要性,所以我就免了。但在你添加更多的repository以及重构时,这完全有可能会变得非常重要。

让我们拿这个repository来看一下如何利用Entity Framework来做一个实现,允许数据访问:

首先,我需要一个可以用来查询数据的ObjectContext。你也许想作为repository构造器的一部分生成一个ObjectContext,但最好还是把那个关注从repository中去除,在别的地方处理为好。

这是我的构造器:

public CustomerRepository(NorthwindContext context)
{
if (context == null)
throw new ArgumentNullException("context");

_context = context;
}

在上面的代码片段中,NorthwindContext是个我自己的ObjectContext类型。

让我们来提供 ICustomerRepository 接口所需的方法的实现。

GetCustomerById 非常容易实现,多亏了LINQ。使用标准的LINQ运算符,我们可以象这样实现 GetCustomerById

public Customer GetCustomerById(string id)
{
return _context.Customers.Where(c => c.CustomerID == id).Single();
}

类似地,FindByName 可以象这样。 再一次,LINQ支持使得其实现非常容易:

public IEnumerable<Customer> FindByName(string name)
{
return _context.Customers.Where( c => c.ContactName.StartsWith(name)
).ToList();
}

注意,我选择将结果呈示为IEnumerable<T>,你也许会选择将这呈示为IQueryable<T>。这么做有其含意,在这个情形下,我对呈示建立在repository返回结果之上的基于IQueryable的另外的查询组合,不是很感兴趣。

最后,让我们来看一下可以如何实现AddCustomer

public void AddCustomer(Customer customer)
{
_context.Customers.AddObject(customer);
}

你也许想把保存功能作为AddCustomer方法的一部分来实现。那么做也许对这个简单的例子没问题,但一般来说,这不是个好主意,这正是Unit of Work出场的地方,过一会儿,我们会看到如何使用这个模式来允许我们实现和协调Save行为。

这里是使用Entity Framework来处理持久性的CustomerRepository的完整实现:

public class CustomerRepository : ICustomerRepository
{
private NorthwindContext _context;

public CustomerRepository(NorthwindContext context)
{
if (context == null)
throw new ArgumentNullException("context");

_context = context;
}

public Customer GetCustomerById(string id)
{
return _context.Customers.Where(c => c.CustomerID == id).Single();
}

public IEnumerable<Customer> FindByName(string name)
{
return _context.Customers.Where(c => c.ContactName.StartsWith(name))
.AsEnumerable<Customer>();
}

public void AddCustomer(Customercustomer)
{
_context.Customers.AddObject(customer);
}
}

这里是我们可以如何在客户端代码中使用该repository:

CustomerRepository repository = new CustomerRepository(context);
Customer c = new Customer( ... );
repository.AddCustomer(c);
context.SaveChanges();

在处理与ProductOrder相关的需求时,我可以定义下列接口(建造类似CustomerRepository一样的实现)。为简便起见,我把其细节省略了。

public interface IProductRepository
{
Product GetProductById(int id);
}

public interface IOrderRepository
{
void AddOrder(Order order);
}

使用ObjectContext实现 Unit of Work (工作单元) 模式

你也许已经注意到了,尽管我们没有实现任何特定的模式来明确地地允许我们将相关操作聚合进一个工作单元(unit of work),但我们已经通过NorthwindContext(我们的ObjectContext类)免费获取了Unit of Work功能。

其想法是,我可以使用Unit of Work将一套相关的操作聚合在一起,Unit of Work记录我感兴趣的变动,直到我准备将它们保存到数据库为止。最终,当我准备保存时,我可以那么做。

我可以象这样定义一个“Unit of Work”接口:

public interface IUnitOfWork
{
void Save();
}

注意,在Unit of Work中,你也许还会选择实现Undo / Rollback功能。在使用Entity Framework时,推荐的undo做法是,丢弃你的上下文以及你想undo的变动。

我已经提到,我们的ObjectContext类型(NorthwindContext)在极大程度上支持Unit of Work模式。 基于我刚定义的契约,为了使得事情更为明确一点,我可以将NorthwindContext类改成实现IUnitOfWork接口:

public class NorthwindContext : ObjectContext, IUnitOfWork
{
public void Save()
{
SaveChanges();
}

. . .

在做了这个变动之后,我需要对我们的repository实现做个小的调整:

public class CustomerRepository : ICustomerRepository
{
private NorthwindContext _context;

public CustomerRepository(IUnitOfWork unitOfWork)
{
if (unitOfWork == null)
throw new ArgumentNullException("unitOfWork");

_context = unitOfWork as NorthwindContext;
}

public Customer GetCustomerById(string id)
{
return _context.Customers.Where(c => c.CustomerID == id).Single();
}

public IEnumerable<Customer> FindByName(string name)
{
return _context.Customers.Where(c => c.ContactName.StartsWith(name))
.AsEnumerable<Customer>();
}

public void AddCustomer(Customer customer)
{
_context.Customers.AddObject(customer);
}
}

 

就这么简单!现在,我们有了一个对IUnitOfWork友好的repository,你甚至可以使用基于IUnitOfWork的上下文来协调跨越多个repository的工作。下面是一个将订单加到数据库的例子,需要多个repository的工作来查询数据,最终将记录保存回数据库中去:

IUnitOfWork unitOfWork = new NorthwindContext();

CustomerRepository customerRepository = new CustomerRepository(unitOfWork);
Customer customer = customerRepository.GetCustomerById("ALFKI");

ProductRepository productRepository = new ProductRepository(unitOfWork);
Product product = productRepository.GetById(1);

OrderRepository orderRepository = new OrderRepository(unitOfWork);

Order order = new Order(customer);
order.AddNewOrderDetail(product, 1);

orderRepository.AddOrder(order);

unitOfWork.Save();

非常有趣地看到,为了在Entity Framework之上使用Repository 和 Unit of Work模式来访问数据,我们要编写的代码是如此地少。Entity Framework的LINQ支持以及原装的Unit of Work功能使得在Entity Framework之上建立repository简单之极。

另一个要注意的事情是,我在这个贴子里讨论的很多东西,都不是与Entity Framework 4.0特别有关,所有这些一般原理在Entity Framework 3.5下也适用。但要能够象上面展示的那样,在POCO支持的基础之上,使用RepositoryUnit of Work,确实显示了Entity Framework 4.0中的威力。我希望你会发现这非常有用。

最后,我要重申一下,有很多方式可以处理这个题目,在应用Entity Framework, Repository 和 Unit of Work时,还有许多变种方案可以符合你的需求。你也许会选择实现公共的IRepository接口,你也许还会选择实现Repository<TEntity>基类,来给予你一些跨越所有repository的常用repository功能。

尝试一些方法,看哪个最适用于你。我在这里讨论的只是一般的指南而已,但我希望这足够让你起步。更重要的是,我希望这示范了如何可以使用我们在Entity Framework 4.0中引进的POCO支持,来帮助你建造Entity Framework友好的领域模型,而不必违背透明持久性方面的基本原则。

包含我上面一些示范代码的项目附在本贴之后。在我们的将来贴子中讨论单元测试,TDD和可测试性时,我们还将对这个题目做更多的讨论,敬请期待。

Faisal Mohamood
Entity Framework的Program Manager

posted on 2009-08-16 12:39:49 by saucer  评论(1) 阅读(4417)

 
2009年08月15日

【原文地址】POCO in the Entity Framework : Part 3 – Change Tracking with POCO
【原文发表日期】 10 June 09 08:43 AM

上一个POCO贴子里,我提到了跟踪POCO变动的两种可行性:基于快照的变动跟踪(Snapshot based Change Tracking)使用代理的基于通知的变动跟踪(Notification based Change Tracking with Proxies)。 在这个贴子里,我将对这两个选项做进一步的讨论,讨论它们的优缺点,以及使用它们的含意(implications)。我们还将在《实体框架设计博客》上贴出针对至今为止受到的关于Entity Framework 4.0中POCO支持的一些反馈的想法。

基于快照的变动跟踪(不用代理的纯POCO类)

就象我在这个系列的第二部分中提到的,基于快照(Snapshot)的变动跟踪,就是纯粹的POCO实体的做法,不使用代理来处理变动跟踪。这是个简明的变动跟踪方案,依赖于实体框架所维护的之前和之后值的完整快照,在SaveChanges中对这些值进行比较,决定到底哪些值与初始值有所不同。在这个模型中,除非你用了懒式装载,你的实体的运行时类型跟你定义的POCO实体类型是一模一样的。

这个做法没什么问题,如果你在乎你的实体类型的运行时纯粹性(不使用代理),完全可以依赖这个方案,走纯粹的POCO实体之路(不依赖于代理类型实现额外的功能)。

基于快照的变动跟踪唯一潜在的问题是,有几件事情你需要注意,因为在你的对象变动时没有向实体框架做直接变动通知,实体框架的对象状态管理器将与你的对象图不再同步。

让我们来看一个展示这个问题的例子:

Customer customer = (from c in context.Customers
where c.CustomerID == "ALFKI"
select c).Single();

ObjectStateEntry ose = context.ObjectStateManager.GetObjectStateEntry(customer);

Console.WriteLine("Customer object state: {0}", ose.State); // Unchanged

customer.Country = "UK";

Console.WriteLine("Customer object state: {0}", ose.State); // Still Unchanged

在这个例子中,Customer是个纯POCO类型,跟基于EntityObjectIPOCO的实体不一样,对该实体做变动并不自动与状态管理器保持同步,因为在你的纯POCO实体与实体框架间没有自动的通知机制。所以,在查询状态管理器时,它会认为你的客户对象的状态是Unchanged(未被改动),尽管我们显式地对该实体的一个属性做了变动。

如果你调用SaveChanges而不选择acceptChangesDuringSave(在保存后接受变动之选项)的话,你会看到在保存后,其状态变成了Modified(改动过了)。这是因为在保存时,基于快照的变动跟踪机制开始起作用,检测到了变动。当然,默认的SaveChanges调用会将状态变回到 Unchanged ,因为默认的Save行为是在保存后接受变动。

在后面一点,我们将进一步讨论在你需要让你的对象,对象图以及状态管理器间保持同步时该怎么做,但让我们先来看一下可为你所用的POCO的另一类变动跟踪机制。

使用代理的基于通知的变动跟踪

如果你在乎在你对实体值,关系和对象图做变动时的非常高效的和即时性的变动跟踪的话,这是个另样的方案,基于代理的变动跟踪。如果你把某个特定实体类型上的所有映射属性都声明为virtual的话,你就可以利用基于代理的变动跟踪了。

使用代理来跟踪变动的实体总是与实体框架的对象状态管理器保持同步,因为代理会在实体的值和关系变动时通知实体框架。总的来说,这使得变动跟踪更为有效,因为对象状态管理器可以略去比较属性的原始值和当前值这一步,如果它知道属性没有变动的话。

实际上,你从代理上得到的变动跟踪行为跟从基于EntityObject的非POCO实体或IPOCO实体上得到的变动跟踪行为是完全一样的。

让我们用同一个例子来看一下:

Customer customer = (from c in context.Customers
where c.CustomerID == "ALFKI"
select c).Single();

ObjectStateEntry ose = context.ObjectStateManager.GetObjectStateEntry(customer);

Console.WriteLine("Customer object state: {0}", ose.State); // Unchanged

customer.Country = "UK";

Console.WriteLine("Customer object state: {0}", ose.State); // Modified

这个例子是不言自明的,在你对实体做变动时,对象状态管理器被通知到你的变动了。在调用SaveChanges时,不会导致另外的花销(overhead)。

但是,基于代理的变动跟踪也意味着你的实体的运行时类型跟你定义的类型不完全一样,而是你的类型的子类。这在许多场景下(譬如序列化)也许不太合适,你需要选择在你的应用和领域的需求和约束下最合适的方法。

不使用代理,与状态管理器保持同步

两种方法都有其优缺点,但不是基于代理的纯POCO方案,其之简明和优雅也意味着它会成为你们中许多人的默认选择,也就毫不奇怪了。所以让我们来看一下在这种模式下,如何使状态管理器与你的对象图保持同步。

ObjectContext.DetectChanges()

在你使用基于快照的纯POCO实体时,有一个至关重要的方法:ObjectContext.DetectChanges()

这个API在你无论何时改变对象图时都应该显式调用,它会告知状态管理器它需要与的你对象图做同步。

ObjectContext.SaveChanges在默认情形下会隐式调用DetectChanges,所以,如果你所做的就是对你的对象们做一系列的变动,然后立刻调用Save的话,你不必显式调用DetectChanges。但是,要记住的是,取决于你的对象图的大小,DetectChanges也许会花销很大(而且,取决于你在做什么,DetectChanges也许是多余的),所以,有可能你可以略去SaveChanges中隐式调用的DetectChanges,这一点,在我们讨论Entity Framework 4.0中新引进的SaveChanges的重载方法时会做更多的讨论。

让我们来看一下DetectChanges如何影响我们前面看过的例子:

Customer customer = (from c in context.Customers
where c.CustomerID == "ALFKI"
select c).Single();

ObjectStateEntry ose = context.ObjectStateManager.GetObjectStateEntry(customer);

Console.WriteLine("Customer object state: {0}", ose.State); // Unchanged

customer.Country = "UK";
Console.WriteLine("Customer object state: {0}", ose.State); // Still Unchanged

context.DetectChanges();
Console.WriteLine("Customer object state: {0}", ose.State); // Modified


显式调用DetectChanges 会导致状态管理器与你的对象的状态保持一致。

因为DetectChanges的潜在花销,依赖于状态管理器与对象图保持一致的ObjectContext上的其他一些APIs是不显式调用它的。因此,无论什么时候你在context上做依赖于状态的操作时,你需要调用DetectChanges

Object Servces(对象服务)中的下列API的行为取决于ObjectStateManager的当前状态,因此,如果它们操作的部分对象图的情况与对象图的实际状态不同步的话,会受影响:

ObjectContext API:

  • AddObject
  • Attach
  • AttachTo
  • DeleteObject
  • Detach
  • GetObjectByKey
  • TryGetObjectByKey
  • ApplyCurrentValues
  • ApplyPropertyChanges
  • ApplyOriginalValues
  • Refresh
  • ExecuteQuery

ObjectStateManager API:

  • ChangeObjectState
  • ChangeRelationshipState
  • GetObjectStateEntry
  • TryGetObjectStateEntry
  • GetObjectStateEntries

ObjectStateEntry API:

  • Any

EntityCollection/EntityReference API:

  • Any

SaveChanges的新重载方法

在Entity Framework 3.5中, 在调用ObjectContext.SaveChanges()时,有两种可能:

  • SaveChanges() – 无参数选项,允许你保存变动,同时隐式接受变动。
  • SaveChanges(bool acceptChangesDuringSave) – 这个选项允许你跳出默认的“accept changes during save”(保存时同时接受变动)行为,你需要在保存后显式地使用AcceptAllChanges来接受变动。

在加了DetectChanges行为后,再加另外的重载方法很明显会搞乱API。为了解决这个问题,引进了SaveOptions这个列举标记:

[Flags]
public enum SaveOptions
{
None = 0,
DetectChangesBeforeSave = 1,
AcceptChangesAfterSave = 2,
}

SaveChanges现在包括了象这样一个重载:

public virtual int SaveChanges(SaveOptions options);

SaveOptions将允许我们(Entity Framework开发团队)以可控制得多的方式来扩展API,假如我们需要在将来添加另外的选项的话。

但这个重载的有趣性还有另外一个原因:注意,这个SaveChanges重载是virtual。在Entity Framework 4.0中,你可以用你自己的定制行为,作为你选择编写的ObjectContext的继承类的一部分,来覆盖SaveChanges行为。无论你是否使用Entity Framework中的POCO支持,这将允许你以一种Entity Framework 3.5中不可能的方式来做定制。

至此,我想要重点阐述的Entity Framework 4.0中有关POCO的事情几乎都说完了,请接着期待下一篇在Entity Framework中使用模式的贴子。

Faisal Mohamood
Entity Framework开发团队的Program Manager

posted on 2009-08-15 14:15:15 by saucer  评论(0) 阅读(3804)

 
2009年05月29日

【原文地址】POCO in the Entity Framework : Part 2 – Complex Types, Deferred Loading and Explicit Loading
【原文发表日期】 28 May 09 09:03 AM

在上星期的贴子《POCO Experience in Entity Framework》 (实体框架中的POCO体验)中,我讨论了Entity Framework 4.0中POCO支持的基本。在这个贴子里,我将讨论与POCO相关的另外几个方面。

复杂类型(Complex Types)

POCO中的复杂类型支持跟常规的基于EntityObject的实体中的复杂类型支持一样。你要做的就是将它们声明为POCO类,然后在你的POCO实体中使用和声明基于它们的属性。

作为例子,这里是一个InventoryDetail复杂类型,代表我的Product实体的一个部分:

public class InventoryDetail
{
public Int16 UnitsInStock { get; set; }
public Int16 UnitsOnOrder { get; set; }
public Int16 ReorderLevel { get; set; }
}

把我的Product类修改成包含一个这个类型的属性,用来组合几个有关库存细节的字段:

public class Product
{
public int ProductID { get; set; }
public string ProductName { get; set; }
public int SupplierID { get; set; }
public string QuantityPerUnit { get; set; }
public decimal UnitPrice { get; set; }
public InventoryDetail InventoryDetail { get; set; }
public bool Discontinued { get; set; }
public Category Category { get; set; }
}

然后你可以做你以前对复杂类型所能做的一切,这是一个查询的例子:

var outOfStockProducts = from c in context.Products
where c.InventoryDetail.UnitsInStock == 0
select c;

 

你可以看到,POCO中的复杂类型支持用起来非常直截了当。但在POCO中使用复杂类型支持时,你需要记住几件事情:

  1. 你必须将复杂类型定义为类(class),结构体(struct)是不支持的。
  2. 在你的复杂类型类中,你不能使用继承。

既然在讨论复杂类型,我想我要提一下另外一件事,你知道Visual Studio 2010中的实体框架设计器支持复杂类型的声明么?

在Visual Studio 2008中,你只能手工将复杂类型的声明加到CSDL中去,但随着Visual Studio 2010中的设计器中对复杂类型支持的推出,这一切都成了历史。

image

更酷的是,因为Visual Studio 2010支持多定向(Multi-Targeting),你可以在开发针对.NET Framework 3.5,使用了Entity Framework 3.5的应用中也使用这个功能!

延迟/懒式装载

在我2个星期前发表的《延迟装载初览》一文中,我提到了实体框架现在支持延迟装载了。默认的、代码生成的基于EntityObject的实体类型将提供延迟装载,自然毫不奇怪。如果你想知道POCO对象中是否也支持延迟装载,那么我想,你会很高兴地知道,你在POCO中也能得到延迟装载支持。

为在POCO实体中使用延迟装载支持,你需要做2件事情:

  1. 将你想要懒式装载的属性声明为virtual,这些属性可以是任何实现了ICollection<T> 的集合类,或者它们可以是代表了1/0..1 关系的引用。

例如,这里是更新过的Category实体类的部分代码,我将其改成支持延迟装载了:

public class Category
{
public int CategoryID { get; set; }
public string CategoryName { get; set; }
public string Description { get; set; }
public byte[] Picture { get; set; }
public virtual List<Product> Products { get; set; }
...

     2.   在上下文中启用延迟装载选项:

context.ContextOptions.DeferredLoadingEnabled = true;

 

这就行了,你现在就将得到POCO类型的自动延迟装载,而不用做任何其他什么事情。

那么这玩意到底是怎么工作的,底层是怎么进行的?

这玩意能工作的原因是因为,在我将集合类型的属性标记为virtual后,这允许实体框架在运行时为我的POCO类型提供一个代理(proxy) 实例,正是这个代理实现了自动的延迟装载。该代理实例是基于一个继承自我的POCO实体类的类型,所以你提供的所有功能都被保留下来了。从开发人员的角度来看,即使延迟装载或许是个需求,这也允许你编写透明持久性的代码。

如果你在调试器中检查实际的实例时,你会看到该实例的底层类型与我原先声明的类型是不同的:

image

虽然实体框架尽力以最小的摩擦提供自动的延迟装载,但在处理你想要添加或附加手工生成的实例时,或者当你序列化/发序列化实例时,这是你需要知道的事情。

为POCO实体手工生成代理实例

为了允许可以添加或附加的代理实例的生成,你可以使用ObjectContext上的CreateObject工厂方法来生成实体实例:

Category category = context.CreateObject<Category>();

 

要把这个记住,在生成你想要用于实体框架的实例时,要使用CreateObject

用“变动跟踪代理(Change Tracking Proxies)”来提供更有效的变动跟踪

到目前为止,我们讨论过的标准POCO实体都依赖于基于快照(snapshot)的变动跟踪,即,实体框架会保管实体变动之前的值和关系的快照,这样,在保存(Save)时,可以与当前的值做比较。但这个比较的花销是相当大的,如果跟基于EntityObject的实体的变动跟踪的方式相比的话。

还有另外一种类型的代理,它允许你在使用POCO实体时得到比较好的变动跟踪性能。

如果你熟悉IPOCO接口,你知道IEntityWithChangeTracker是要求你在类中实现、来向实体框架提供变动通知的接口之一。

变动跟踪代理从你的POCO实体类继承而来,在运行时给你提供这个功能,而不要求你自己实现IPOCO接口。

从许多方面讲,用这种方式的话,你是鱼与熊掌都兼得了:你得到了POCO类的透明持久性,在变动跟踪方面你也得到了EntityObject / IPOCO 的性能。

为了得到变动跟踪代理,基本的规则是,你的类必须是公开的,非抽象的或者非密封的(non-sealed)。你的类对所有要持久的属性都必须实现公开的virtual getters/setters。最后,你必须将基于集合的关系导航属性严格声明为ICollection<T>。它们不能是具体的实现或者继承自ICollection<T>的另外的接口(与延迟装载代理有所不同)。

这里是我的Product POCO类的例子,它将在运行时给我提供更有效的基于代理的变动跟踪:

public class Product
{
public virtual int ProductID { get; set; }
public virtual string ProductName { get; set; }
public virtual int SupplierID { get; set; }
public virtual string QuantityPerUnit { get; set; }
public virtual decimal UnitPrice { get; set; }
public virtual InventoryDetail InventoryDetail { get; set; }
public virtual bool Discontinued { get; set; }
public virtual Category Category { get; set; }
}

再说一遍,要记住,如果你要将实体加到或附加到上下文的话,你必须使用CreateObject来生成代理实例。但不依赖代理的纯粹的POCO实体和基于代理的实体可以共处。你只有在涉及基于代理的实体时才需使用CreateObject。

如果我想在同个POCO类型中同时启用延迟装载和更好的变动跟踪,该怎么办?

这两个东西不是互相排斥的,你不必在延迟装载代理和变动跟踪代理间做选择。如果你想要延迟装载,以及有效的变动跟踪,你只要按照变动跟踪代理的规则办,以及启用延迟装载选项。变动跟踪代理会给你提供延迟装载,如果延迟装载选项是启用了的话。

显式装载

这个延迟装载的功能确实很棒,但你们中很多人大概想要完全控制你是如何装载相关实体的吧。你甚至会选择走纯粹的POCO之路,而不诉诸于任何自动代理生成能给你提供的功能。

这是完全可以接受的,(在很多情形下也许是更好的方法),你可以使用显式关系装载,对你是如何在数据库中查询数据的做完全的控制。

在POCO中做显式装载,有2个选项:

一个是使用 ObjectContext.LoadProperty ,设置你想要装载的导航属性的名称:

context.LoadProperty(beveragesCategory, "Products");

这是可行的,但你可以看出来,这并不类型安全(type safe)。如果我没有正确的导航属性的名称的话,我会得到一个运行时异常。

你们中的一些人大概更喜欢这个:

context.LoadProperty(beveragesCategory, c => c.Products);

我可以使用lambda表达式来指定我想要显式装载的属性,这提供了更好的类型安全。

上面是我计划在这第二个贴子里讨论的所有的内容了。但以后还会有更多内容,在这个系列的最后一篇里,我们将讨论在处理纯POCO(非代理化的)实例以及在你的对象图和对象状态管理器(Object State Manager)间保持一致时需要知道的几件事情。我们也会讨论你也许想要知道的SaveChanges方法的多个变种。

与此同时,请看一下我更新过的样例代码,其中包括了我们在本贴里讨论过的一些东西。

Faisal Mohamood
Entity Framework的Program Manager

posted on 2009-05-29 14:12:55 by saucer  评论(3) 阅读(5137)

 
2009年05月28日

【译者按】 Entity Framework 1.0 发布也有一段时间了,但感觉用的人很少。其中一个很大的原因,也许就是不支持POCO。要知道,Entity Framework 1.0的做法是让你的实体从EF的基类继承而来,这对很多人,特别是崇尚DDD的人来说,那是一副难以下咽的药啊。曾有微软开发人员提供了一个 POCO Adapter,但那究竟不是正规的做法。Visual Studio 2010 和 .NET 4.0 提供了许许多多的新特性,真是让人激动,向往,大有一种回到.NET 1.0 刚出来时的感觉。春天啊(应该是夏天啊),你终于回来了(虽然早晨/晚上还是unseasonably冷)。其中的Entity Framework 4.0版本将提供POCO支持,对很多人来说,这是开始使用Entity Framework的时候了。ADO.NET 团队博客上贴出了一些关于EF和POCO的贴子,非常值得一读。

【原文地址】POCO in the Entity Framework: Part 1 - The Experience
【原文发表日期】 21 May 09 05:46 PM

上个星期,我在《POCO初览》中提到了POCO实体支持是我们加到Entity Framework 4.0中的新功能之一。这个星期,我要对Entity Framework 4.0中POCO支持的细节进行深入讨论。

要讨论的东西很多,包括

  • Entity Framework 4.0中总的POCO体验
  • POCO的变化跟踪
  • 关系修整
  • 复杂类型
  • 延迟(懒式)装载和显式装载
  • 最佳实践

在这个贴子中,我将主要着重于总的体验,这样,你可以马上启用Entity Framework 4.0中的POCO支持。我将使用一个简单的例子做示范,这样,你可以看到在Entity Framework 4.0中使用POCO的感觉。我将使用Northwind数据库,在随后的贴子中,将继续建立在这个例子的基础之上。

第一步: 创建模型,关闭默认的代码生成

虽然POCO允许你以透明持久化的方式编写自己的实体类,但还是有必要“接入”持久性和EF元数据,这样你的POCO实体可以从数据库中复原,以及持久化到数据库中。为实现这个,你还是需要使用实体框架设计器创建一个实体数据模型(Entity Data Model),或者提供跟你在Entity Framework 3.5中生成的完全一样的CSDL, SSDL 和 MSL 元数据文件。所以,首先,我将使用ADO.NET实体数据模型向导(Entity Data Model Wizard)来生成一个EDMX。

image

  1. 创建一个类库项目来定义你的POCO类型,我将其命名为NorthwindModel。这个项目与持久性毫不相关,对实体框架没有依赖性。
  2. 创建一个类库项目,它将包含与持久性相关的代码,我将其命名为NorthwindData。这个项目除了对NorthwindModel项目有依赖外,还对实体框架(System.Data.Entity)有依赖。
  3. NorthwindData 项目中添加新项,加一个名为“Northwind.edmx ”的ADO.NET实体数据模型(这么做将自动在项目中添加对实体框架的依赖)。
  4. 选择“从数据库生成”,给Northwind数据库建造模型
  5. 暂时只选CategoriesProducts这两个你感兴趣的表到你的实体数据模型中。

至此,生成了可以为我所用的实体数据模型,但在开始编写代码前,还有最后一步:关闭代码生成。毕竟你感兴趣的是POCO啊,请去除负责从Northwind.edmx生成基于EntityObject代码的Custom Tool(自定义工具)属性,这将关闭你的模型的相应代码生成。

image

现在,我们可以编写POCO实体了。

第二步: 编写你的POCO实体

我将为Category和Product编写简单的POCO实体,这些类将加到NorthwindModel项目中去。注意,我在这里展示的,不应该看成是最佳实践,这里的意图只是示范可以工作的最简单的例子。在以后我们将根据需要将对这些例子进行扩展和定制,之后建立在其基础之上,使用RepositoryUnit of Work模式做进一步的扩展。

这里是Category实体的样例代码:

    public class Category
    {
        public int CategoryID { get; set; }
        public string CategoryName { get; set; }
        public string Description { get; set; }
        public byte[] Picture { get; set; }
        public List<Product> Products { get; set; }
    }

注意,在模型中我既为标量字段定义了属性,也定义了导航属性。模型中的导航属性(Navigation Property)转换成了List<Product>。

类似地,Product实体可以这样编写:

    public class Product
    {
        public int ProductID { get; set; }
        public string ProductName { get; set; }
        public int SupplierID { get; set; }
        public string QuantityPerUnit { get; set; }
        public decimal UnitPrice { get; set; }
        public Int16 UnitsInStock { get; set; }
        public Int16 UnitsOnOrder { get; set; }
        public Int16 ReorderLevel { get; set; }
        public bool Discontinued { get; set; }
        public Category Category { get; set; }
    }

在这个例子中,因为其间的关系,一个Product只允许属于一个Category,因此我们有一个对Category的引用(而不象Category中的导航属性,是一个List<T>集合)。

第三步: 编写你的实体框架上下文

为把所有这些东西结合在一起,我所要做的最后一件事情是,提供一个上下文实现(就象使用默认的代码生成时你得到的ObjectContext实现一样)。上下文(context)是把持久化意识带进你的应用的胶水(glue),它允许你编写查询,将实体复原,以及将变化存回到数据库中去。该上下文将是NorthwindData项目的一部分。

为简单起见,我将我们的上下文类包括在了包含实体类型的同个类库项目中了,但在我们讨论诸如Repository Unit of Work等模式时,我们将做这样的设置,即,你将拥有一个纯粹的POCO类库,不带任何一丝与持久性相关的东西。

这里是针对我们场景的简单的上下文实现:

public class NorthwindContext : ObjectContext
    {   
        public NorthwindContext() : base("name=NorthwindEntities", 
"NorthwindEntities") { _categories = CreateObjectSet<Category>(); _products = CreateObjectSet<Product>(); } public ObjectSet<Category> Categories { get { return _categories; } } private ObjectSet<Category> _categories; public ObjectSet<Product> Products { get { return _products; } } private ObjectSet<Product> _products; }

注意,ObjectSet<T> 是Entity Framework 4.0中引进的一个特殊的ObjectQuery<T>。

就这么简单!现在你有了纯粹的POCO实体,一个简单的上下文,允许你编写象这样的查询:

    NorthwindContext db = new NorthwindContext();
 
    var beverages = from p in db.Products
                    where p.Category.CategoryName == "Beverages"
                    select p;

从数据库中复原的实体是纯粹的POCO实体,你可以对这些实体做变动,将变动保存到数据库中,跟平常的EntityObjectIPOCO实体非常类似。你将得到实体框架提供的所有服务,唯一的区别是,你现在使用的是纯粹的POCO实体。

就我们的简化了的例子而言,尚有无数改进的可能性。但在那之前,我想先把一些基本的问题解决掉。

在使用POCO之前我需要一个实体数据模型么?

是的,Entity Framework 4.0中的POCO支持只是去除了在你的实体类中带特定于持久性的关注的需求而已。但还是需要你提供CSDL/SSDL/MSL (总称EDMX)元数据,这样实体框架才能够将你的实体和元数据结合起来,以允许你访问数据。我们还在另外开发一个东西,它允许你做真正的“代码优先(code-first)”的开发,而不需要预先定义好的EDMX模型。这个功能的社区预览版将在几个月内在网上发布,一有机会我们就会将其加入实体框架。一如既往,你的反馈对我们极其有用。

我始终都只能手写这些实体和上下文么?

不, Entity Framework 4.0中有一个非常强大和灵活的代码生成机制,该机制是基于T4之上的(Alex曾在这里的博客上讨论过)。你可以提供你自己的模板,允许你按照你自己选择的方式建造实体。我们还在努力,以提供一些标准的模板,可为你生成POCO实体。

使用POCO实体时,元数据是怎么映射的?

在Entity Framework 3.5中,基于EntityObjectIPOCO的实体都是依赖着使用映射特性(attributes),对实体类型和属性进行修饰和映射到概念性模型中对应的元素的。Entity Framework 4.0 引入了基于约定(convention)的映射,以允许不用显式的修饰,就可将实体类型,属性,复杂类型和关系映射到概念性模型。一个简单的规则是,在你的POCO类中使用的实体类型名称,属性名称,和复杂类型名称必须匹配那些在概念性模型中定义了的相应名称。命名空间的名称不在考虑之中,类中的命名空间定义和概念性模型中的命名空间定义不必相符。

我的实体类中的所有属性都需要有公开的getters和setters么?

你可以在你的POCO类型的属性上使用任何访问控制修饰符(access modifier),只要被映射的任何属性都不是虚拟的,以及你不需要局部信任(partial trust)支持。在局部信任下运行时,对你的实体类的访问控制修饰符的可见性有一些特定的要求。我们将对在涉及局部信任时,所支持的完整的访问控制修饰符集提供详细的文档。

对基于集合的导航属性都支持哪些集合类型?

任何属于ICollection<T>的类型都是支持的。如果你对ICollection<T>类型的字段不以具体的类型初始化的话,那么从数据库中复原实体集合时, 将提供List<T>

我可以有单向的关系么?例如,我想在我的Product类中有一个 Category 属性,但在Category类中我不想要一个Products集合。

是的,这是支持的。唯一的限制是,你的实体类型必须反映模型中所定义的东西。如果你不想拥有对应于关系的某一边的导航属性的话,那么你需要从模型中将其完全剔除。

POCO支持延迟(懒式)装载么?

是的,POCO是通过使用代理类型来支持延迟(懒式)装载的,这些代理类型是在你的POCO类之上提供自动的懒式装载行为的。在讨论到延迟装载时,我们会对此做详述,在那之前,知道一下我们也支持通过使用“Include”来实现的早期装载(eager loading),象这样:

var beverageCategory = (from c in context.Categories
.Include("Products") where c.CategoryName == "Beverages" select c).Single();

如果我要使用延迟(懒式)装载的话,我不用这么做, 在讨论代理时我们会对此进行讨论。

修整(fix-up)关系

要了解有2种类型的修整:1)查询时的修整, 2) 对你的实体/关系进行改动时的修整。

查询时的修整

查询时的修整是在同一个ObjectContext上使用不同的查询来装载相关的实体时发生的。例如,如果我查询了一个分类实例“Beverages(饮料)”,之后又查询一个产品实例“Chai”(是在Beverages分类中的),我想要chai.Category 指向Beverages分类实例,不用我做额外的工作。

这在今天是自动的,而且已经工作。

对你的实体/关系进行改动时的修整

这是在添加/去除与另一个实体相关的实体或者改变关系时两个实体间的关系修整。设想一下这样的例子:在我创建一个名叫“Diet Chai”的新产品时,我想要将其与Beverages 分类相关联。

在Entity Framework 4.0 Beta1 中,在这种情形下,POCO实体得不到自动的关系修整。在处理实体间的关系变动时,你需要确认你的POCO类型包含了在关系两头正确处理关系修整的逻辑。

例如,在我的Northwind例子中,我在Category上定义了如下的方法,以支持添加/去除订单。

        public void AddProduct(Product p)
        {
            if (Products == null)
            {
                Products = new List<Product>();
            }
 
            if (!Products.Contains(p))
            {
                Products.Add(p);
            }            
            
            p.Category = this;
        }

总的来说,使用象这样的模式来支持添加/去除相关项,要比使用关系集合来添加/去除相关实体为好。你还可以选择将实际由EF支持的集合的getter/setter变成私有或内部访问控制,来支持更为细颗粒的访问,但就象前面提到的那样,其中一些东西取决于你是否需要局部信任支持这样的需求。

我们在这个“简短的综述”中对总的POCO体验做了一番讨论。要讨论的东西还有很多很多,下个星期我将继续这个讨论。与此同时,请看一下附件中的完整解决方案,如果你对我们在这里讨论的所有概念的相应例程代码感兴趣的话。请记住,你必须在运行这个例程的机器上安装一个本地Northwind数据库。

在下一个贴子中,我们将看一下复杂类型,变化跟踪,代理,懒式装载和早期装载。在那之前,请阅读一下MSDN这里的关于POCO的文档,以了解Entity Framework 4.0中的POCO支持的细节。

请告知我们你们的想法!

Faisal Mohamood
Entity Framework的Program Manager

posted on 2009-05-28 14:19:46 by saucer  评论(1) 阅读(7159)

 
2009年01月09日

大家都知道,Windows 7 beta(7000)已于昨天推出,参与微软Connect,以及有MSDN和TechNet订阅的都可以下载了。

下载完后,决定不是全新安装,而是选择了从先前安装的Windows 7的6801版本升级,没想到安装花了很长的时间。只好安慰自己,假如全新安装的话,加上安装办公软件+抗毒软件+以及小孩安装的各种软件,总的时间恐怕会更长。

安装完了,赶紧安装第一个补丁,KB961367,这修复了Media Player12的一个缺陷,因为它会在某些特别的情形下剪去某些mp3文件的前面几秒。呵,因为这是小孩的机器,我可不愿为坏掉了的mp3文件负责。

运行Norton的更新和扫描,居然蓝屏死机了,又试了一次,还是一样。

界面有了几个变动,譬如新的taskbar和Jump list,细节就不谈了。

用的时候感觉很平滑,响应比前面的那个版本要快,性能比以前好了。

【更新】微软刚公开了beta测试,任何人都可以在下面这个链接下载(但限于前面二百五十万位):

Welcome to the Windows 7 Beta Customer Preview Program
http://technet.microsoft.com/en-us/evalcenter/dd353205.aspx

【最新更新(1月11日)】因为下载的人太多,导致服务器运行缓慢,微软一度推迟了下载。在做了调整之后,现在恢复了下载。同时取消了二百五十万份下载数的限制,但只开放下载2个星期(到1月24日为止)。上面的链接依然有效,还有这个链接:

Download the Windows 7 Beta
http://www.microsoft.com/windows/windows-7/beta-download.aspx

 

posted on 2009-01-09 23:50:17 by saucer  评论(0) 阅读(5366)

 
2008年12月24日

想玩Windows 7很久了,而且从网上的评论来看,大家对Windows 7的评论都非常正面。但因为没有参加PDC,没能得到带Windows 7的碟子。最近MVP的邮件列单上说有多余碟子赠送,就去信说想要一份,没想到很快就寄来了。在这里,要感谢微软中国MVP部门,特别是MVP Lead Sisley。

拿到碟子后,迫不及待地在Virtual PC上装了一份,但没有硬件加速,感觉不是很过瘾。

小孩的Dell机器(04年购置的)不久前被一个病毒所扰,修复后很多软件都无法用了,一直吵着要重新安装系统。我想,反正是重装,就装Windows 7,让小孩做beta测试人员吧。

安装比较顺利,而且好像安装的时间也比较短。但可惜,系统居然不认识上面的声卡。小孩说,没audio,那怎么成?

该声卡是Creative Labs的,驱动认出是Multimedia Audio Controller,但Windows 7安装不了驱动。用了Dell原来带来的驱动软件,即使设置了以XP兼容模式运行,也无法安装。在网上查了无数的文章和贴子,试验了很多法子,都没用。被折磨了N个小时后,决定放弃,从一个很老的闲置机器上拔了声卡出来,看是否可用,哈,Window 7居然认出来,安装了适当的驱动程序!

安装的版本是6801,好像是比较早的版本,感觉比较慢。每次使用Windows Explorer访问局域网上的共享文件,如果连不上,Windows Explorer就会死在那里,用Task Manager也杀不死,log off也死在那里,最后只能关机。

从网上的消息看,好像有人已经拿到比较新的版本,反映不错。微软好像在明年一月会开放beta测试,非常期待。

posted on 2008-12-24 13:47:52 by saucer  评论(1) 阅读(8466)

 
2008年12月18日

微软模式和实践开发团队发布了《应用架构指引》(第二版)的最终版:

Application Architecture Guide 2.0
http://www.codeplex.com/AppArchGuide/Release/ProjectReleases.aspx?ReleaseId=20586

比原计划早了几乎一个月,看来我得加把劲了。

posted on 2008-12-18 07:51:45 by saucer  评论(0) 阅读(6673)

 
2008年11月27日

微软模式和实践开发团队发布了《应用架构指引》(第二版)的最终版:

Application Architecture Guide 2.0
http://www.codeplex.com/AppArchGuide/Release/ProjectReleases.aspx?ReleaseId=20586

比原计划早了几乎一个月,看来我得加把劲了。

posted on 2008-11-27 18:04:10 by saucer  评论(0) 阅读(8797)

 
2008年11月07日

在最近的PDC大会上,微软推出了与Oslo相关的重大技术。Oslo到底是什么?根据Oslo的FAQ

“Oslo是微软的模型驱动开发平台的代号名,Oslo的目标是,通过把模型驱动应用变成主流,提供十倍以上的生产力增益。这个平台的核心是特定领域(domain-specific )的模型,语言和工具:

•       一个名为M的语言,用文字的形式编写领域模型

•       一个名为Quadrant的工具,用图形的方式编写领域模型

•       一个用于管理领域模型的Repository

•       一个带有预制领域模型和语言的库

在一起,这些构件将使得一个团队能更有效地开发、实现和维护应用和服务。”

 

这里是一些相关的资源链接:

  • Oslo 开发者中心
    http://msdn.microsoft.com/en-us/oslo/default.aspx
  • Oslo SDK October 2008 CTP 下载地址
    http://code.msdn.microsoft.com/oslo/Release/ProjectReleases.aspx?ReleaseId=1707
  • PDC上与Oslo相关的讲座录像

    1. A Lap around "Oslo" (对Oslo相关技术的综合介绍)
      http://channel9.msdn.com/pdc2008/TL23/
    2. "Oslo": The Language (对M语言的介绍)
      http://channel9.msdn.com/pdc2008/TL27/
    3. "Oslo": Building Textual DSLs (对如何建造文字形式的DSL的介绍)
      http://channel9.msdn.com/pdc2008/TL31/
    4. "Oslo": Customizing and Extending the Visual Design Experience (对Quadrant工具的介绍)
      http://channel9.msdn.com/pdc2008/TL18/
    5. "Oslo": Repository and Models (对存储中心的介绍)
      http://channel9.msdn.com/pdc2008/TL28/
  • David Chappell的《Workflows, Services, and Models - A First Look at WF 4.0, “Dublin”, and “Oslo”》
    http://msdn.microsoft.com/en-us/library/dd200919.aspx

    对其中的一段的草译,“Oslo的主要目的是使模型成为跨越应用开发周期(创建、部署和管理)的一个基本部分。在Oslo中,模型是某种东西的抽象表现,譬如一个业务过程,一个应用,或一个工作流程(别把这里的“模型”的概念与其他场景(譬如UML)中的同名术语相混淆,两者并不等同)。不是把模型的概念局限于只在设计过程中使用的描述性图表,Oslo允许模型成为应用本身的一部分。例如,一个WF工作流程可以使用Quadrant来创建,并储存于repository之中。这个工作流程是个模型,该模型存在于repository之中,但它同时也是工作流程的实际逻辑。改变模型意味着改变工作流程本身,这意味着模型和这部分的应用逻辑总是同步的。Oslo repository不仅仅可以保存应用的模型,当然,一个应用的其他部分还可以居于repository之外。然而,把模型从只是描述一个应用变成实际应用本身的观念,对于Oslo来说,是至关重要的。”

posted on 2008-11-07 14:40:53 by saucer  评论(0) 阅读(6706)

 
2008年11月06日

在PDC上的一个讲座中,微软研究所展示了一个工具,叫Pex (Program EXploration - 程序探索):

Research: Contract Checking and Automated Test Generation with Pex
http://channel9.msdn.com/pdc2008/TL51/

 

Pex项目地址:
http://msdn.microsoft.com/en-us/devlabs/cc950525.aspx

(上面链接里的下载好像是针对VS 2010的,其他的版本可在这个地址http://research.microsoft.com/Pex/downloads.aspx下载)

 

Pex是个白盒测试生成工具,可以用于帮助理解.NET代码的行为,调试问题,以及,完全自动地,创建涵盖所有边界案例的全套测试。它提供了与VS的集成。

在安装之后,如果在自己的代码中点击右鼠标,然后在上下文菜单中选择运行Pex探索(“Run Pex Explorations”)的话,它会用不同的输入运行你的代码很多次。这些输入不是任意的,也不是所有可能输入的全部组合,而是根据你的代码,分析出其中的边界条件,选出有代表性的输入。简单地说,Pex会分析每一句代码,会琢磨出达到该语句的测试输入。如果代码中有条件性分支,Pex会做案例分析,即Pex会根据代码中条件分支的数目和可能组合生成对应的测试输入。

Pex是在一个反馈循环中运作的: Pex运行代码多次,通过监测控制和数据分流,了解程序的行为。每次运行之后,Pex会挑一个早先没有覆盖的分支,建造一个描述如何达到那个分支的约束系统,然后使用约束解算器(constraint solver,这个版本用了一个叫Z3的约束解算器)决定满足对应约束的新测试输入。然后用新的输入再次运行测试。。。这个过程会重复多次。每次运行,Pex也许会发现新的代码,深入代码实现之中。通过这个方式,Pex可以探索代码的行为。

在VS中,在运行Pex探索之后,在探索结果中选择某个输入,然后选择保存测试案例的话,Pex会为你的代码生成一个测试项目,在其中生成测试类以及相关测试方法。当然你也可以选择所有的输入场景,然后保存所有的测试案例,供你做regression测试之用。

Pex在探索代码、生成测试输入时也会跟踪代码覆盖率。但Pex只有局部的覆盖率知识(Pex称之为动态覆盖率),只有VS代码覆盖率收集器才能给你提供全局的覆盖率信息。

在Pex的新手起步网页上有个简短的代码挖掘教程http://research.microsoft.com/pex/articles/pexcodediggertutorial.pdf 

 

新手起步网页上,还有更深入的教程,原理概述,参考手册和例程等等。

posted on 2008-11-06 14:39:47 by saucer  评论(0) 阅读(6135)

 
2008年10月31日

了解动态语言Ruby的,大概都知道Ruby中有一个很有意思的实例方法叫method_missing。如果在代码中调用一个某个类中不存在的方法,就会调用这个方法。Ruby on Rails中的ActiveRecord利用这个方法,可以提供很方便的动态查询方法,譬如

find_by_name
find_by_username_and_password
find_all_by_first_name_and_last_name
....

细节请参考 http://www.gargoylesoftware.com/RailsDynamicFinders.pdf

当然,通过这个方法,你还可以做许多涉及metaprogramming的东西。

 

最近发布的C# 4.0 CTP版本引进了大量的动态语言的构造,我们终于也可以很方便地实现动态方法了,

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Scripting.Actions;

namespace ConsoleApplication1
{
    class Data
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public string Value { get;set;}
    }

       //这里使用了CTP版本中IDynamic Object Example例程中的IDOHelper项目中的Dynamic类以减少代码量
    class MyClass : System.Scripting.Actions.Dynamic
    {
        List<Data> data;
        public MyClass()
        {
            data = new List<Data>{ new Data {ID=1,Name="abc",Value="hello world" },
                new Data {ID=2,Name="def",Value="hello joycode"},
            };
        }

        public string Find(int id)
        {
            if (id <= 0 || id > 2)
                throw new ArgumentException("cannot handle this parameter:" + id.ToString());

            return Find(d => d.ID == id);
        }

        public override object Call(CallAction action, params object[] args)
        {
            if (action.Name == "Find" || action.Name == "FindByID")
                return Find((int)args[0]);
            else if (action.Name == "FindByName")
                return Find(d => d.Name == (string)args[0]);

            //当然,在这里,你可以发挥想象力,实现任何你能想到的方法

             throw new NotSupportedException();
        }

         private string Find(Predicate<Data> p)
        {
            Data d = data.Find(p);
            if (d == null)
                return String.Empty;
            return d.Value;
        }
    }

}

在客户端代码里,你可以这样

dynamic m = new MyClass();


Console.WriteLine(m.Find(1)); //Find方法在类中有声明,但其实还是通过Call方法来调用的

Console.WriteLine(m.FindByID(2));  //但FindByID则没有声明

Console.WriteLine(m.FindByName("def")); //FindByName也没有声明

这里把m声明为dynamic是关键,否则会出错。输出为

hello world
hello joycode
hello joycode

 

微软的Chris Burrows对此有更深入的讨论:

C# "dynamic," Part II
http://blogs.msdn.com/cburrows/archive/2008/10/28/c-dynamic-part-ii.aspx

posted on 2008-10-31 17:19:55 by saucer  评论(2) 阅读(6442)

 

一些讲座的录像已经上线:

http://channel9.msdn.com/tags/pdc2008/

 

哎,想看的太多了,可是每个都是一个多小时。。。。

对C# 4.0感兴趣的,一定要看一下的Anders Hejlsberg的

The Future of C#
http://channel9.msdn.com/pdc2008/TL16/

posted on 2008-10-31 00:01:25 by saucer  评论(0) 阅读(6182)

 
2008年10月29日

微软模式和实践团队已经推出了《应用架构指引》(第二版)的beta 1版本

patterns & practices Application Architecture Guide - v2.0 (Beta 1 Release)
http://www.codeplex.com/AppArchGuide

PDF版本可在此下载

http://www.codeplex.com/AppArchGuide/Release/ProjectReleases.aspx?ReleaseId=18834

 

在知道微软即将推出《应用架构指引》的第二版时,我即有意将该书翻译成中文。博文视点在了解情况后,很快与微软出版社取得联系,得到了该书的版权。在明知微软将免费提供该书英文版的情形下依然愿意出版该书,这样的胸襟非常难得,在此特向博文视点表示敬意。我计划从beta 1翻译起,希望在正式版(大概在1月中旬左右)推出不久,就能完成该书的翻译。

自四月份起,连续做了N个项目,忙得不行,到现在,虽然还有不少收尾工作,但终于有些空余时间了。适逢微软推出了无数新技术,其热闹程度,感觉象是到了春天一样,可玩的东西真是太多了,Silverlight 2, Visual Studio 2010, .NET 4, Windows 7, Windows Azure,  Windows Office Live, Oslo 和 M 语言,...Happy days are here again!

posted on 2008-10-29 05:44:02 by saucer  评论(1) 阅读(5215)

 
【第1页/共28页,413条】
首页
前页
1
...

Powered by: Joycode.MVC引擎 0.5.2.0