Visual Studio 2010 中的实体框架 4.0 和 WCF 数据服务 4.0
Elisa Flasko
在诸多新改进之中,Visual Studio 2010 引入了用户期待已久的实体框架 4.0 和 WCF 数据服务 4.0(以前称为 ADO.NET 数据服务),这两项功能综合起来,简化了您建立数据模型、使用数据和生成数据的方式。
实体框架 4.0 (EF 4.0) 致力于启用和简化两种主要方案:以域为中心的应用程序开发和传统以数据为中心的“基于数据的窗体设计”。它引入了诸如模型优先开发等功能(该功能允许您创建模型并为您生成自定义 T-SQL);对持久化透明的支持;外键;延迟加载以及实体的自定义代码生成。
WCF 数据服务 4.0 致力于对开放数据协议 (odata.org) 及其新功能的更新,其中包括 Windows Presentation Foundation (WPF) 和 Silverlight 的双向数据绑定、行计数、服务器驱动的分页、增强的二进制大对象支持以及对投影的支持。
我将使用一个简单的网络日志应用程序 (MyBlog) 来探讨 EF 和 WCF 数据服务中的新功能,并说明这些技术如何协同工作来简化建立数据模型和使用数据的方式。此示例应用程序将包含一个提供博客文章只读视图的 ASP.NET Web 应用程序,以及一个允许博客所有者编辑文章的 Silverlight 博客管理员客户端。在应用程序开头,我将首先使用模型创建一个实体数据模型 (EDM),然后生成数据库以及用于与该数据库交互的代码。此示例还将使用 Silverlight 3 CTP 3 的 ADO.NET 数据服务更新。
EF 4.0 入门
我将先探讨 ASP.NET Web 应用程序项目。(我的应用程序名为 BlogModel;您可以在 code.msdn.microsoft.com/mag201004VSData 下载随附的代码。)为了开始使用 EF,我使用“添加新项”向导来添加 ADO.NET EDM,并选择一个我同样也称为 BlogModel 的空模型。通过右键单击空设计器图面并选择“属性”,您可以看到默认实体容器名称,在本例中为 BlogModelContainer。首先,我将该名称更改为 BlogContext,然后将创建模型。
MyBlog 需要三个实体,我分别将其命名为 Blog、Post 和 Tag,如图 1 中所示。为了创建这些实体,我将一个实体从工具箱拖到设计图面,然后单击鼠标右键并选择“属性”以编辑实体属性。在其中每个实体上,我还将需要一些标量属性(右键单击实体并选择“添加”|“标量属性”)。
图 1 Blog、Post 和 Tag 实体以及关联的属性设置
EF 4.0 中的外键支持
接下来,我将添加这些实体之间的关系。右键单击设计图面,并选择“添加”|“关联”,如图 2 中所示。EF 现在支持外键,从而允许在实体上包括外键属性。请注意,添加关系的操作向 Post 实体中添加了 BlogBlogID 属性(即外键)。
图 2 Blog、Post 和 Tag 实体之间的关联
通过在实体上包括外键属性,将可简化许多键编码模式,其中包括数据绑定、动态数据、并发控制和 n 层开发。例如,如果我正在对显示产品的网格进行数据绑定操作,并且在网格中有 CategoryID(一个外键值),但没有对应的 Category 对象,则 EF 中的外键支持意味着我不再需要进行查询来单独地拉回 Category 对象。
EF 4.0 的模型优先
既然生成了模型(请参见图 3),那么应用程序将需要数据库。在本例中,MyBlog 是一个新应用程序,尚没有数据库。我不想自己创建数据库;而是希望让程序为我创建 – 而我能够做到。利用 EF 4.0 中的模型优先,Visual Studio 现在不仅能为实体生成自定义代码,而且能基于刚刚创建的模型来生成数据库。
图 3 Blog 模型
首先,我需要创建将向其应用所生成的架构的空数据库。为此,我打开服务器资源管理器,右键单击“数据连接”节点,并选择“创建新的 SQL Server 数据库”(请参见图 4)。创建空数据库后,我右键单击模型设计图面,并选择“从模型中生成数据库”。完成“生成数据库”向导的各个步骤后,将会创建 BlogModel.edmx.sql 文件。在此新文件处于打开状态时,只需右键单击该文件并执行 SQL 脚本即可为我的数据库创建架构。
图 4 创建新的空数据库并从 EDM 中生成数据库架构
EF 4.0 的自定义代码生成
此时,可以进行多个后续步骤,其中一个步骤就是使用 T4 模板自定义 EF 所生成的代码。在 Visual Studio 2008 SP1 中,尽管 EF 提供了一些用于自定义代码生成的挂钩,但使用起来却相对不灵活和困难。EF 4.0 现在利用 T4 模板,从而提供了更简单、更灵活并且更强大的方式来自定义生成的代码。
若要将新的 T4 模板添加到项目,请右键单击实体设计器图面,并选择“添加代码生成项”。从此处选择任何当前已安装的模板作为起点,或查看联机库中可用的模板。为了使用默认 EF 模板作为此项目的起点,我将选择 ADO.NET EntityObject 生成器模板;默认情况下,该模板名为 Model1.tt。通过采用这种方式添加代码生成模板,EF 将自动为模型禁用默认代码生成。生成的代码被从 BlogModel.Designer.cs 中删除,现在存在于 Model1.cs 中。此时,可以编辑模板以自定义它将生成的实体,并且每次保存 .tt 文件时,都将重新生成相关代码。有关编辑和使用 EF 4.0 的 T4 模板的详细信息,请访问 ADO.NET 团队博客,网址为 blogs.msdn.com/adonet。
将 POCO 实体用于 EF 4.0
Visual Studio 2008 SP1 对实体类有很多限制,这至少可以说,会使生成真正没有持久性问题的类非常困难。EF 4.0 中最热门的功能之一是:能够为使用 EF 的实体创建 Plain Old CLR Object (POCO) 类型,并且没有 Visual Studio 2008 SP1 中的那种限制。
我们再回到 MyBlog 示例。我将为三个实体(Blog、Post 和 Tag)创建 POCO 对象。首先,需要禁用代码生成,并且我需要删除在上一节中添加的 .tt 文件。若要查看模型的属性,请右键单击实体设计器图面。如图 5 中所示,有一个名为“Code Generation Strategy”的属性,该属性需要设置为“None”以禁用代码生成。
图 5**“Code Generation Strategy”属性**
请注意,当您将“代码生成项”(T4 模板)添加到项目时,此属性将自动设置为“None”。如果项目当前包括 .tt 文件,您将需要在使用 POCO 对象之前将该文件删除。从此处可以添加 POCO 对象的类(Blog.cs、Post.cs 和 Tag.cs),如图 6、图 7 和图 8 中所示。
图 6 Blog 实体的 POCO 对象
public class Blog
{
public intBlogID
{
get;
set;
}
public string Name
{
get;
set;
}
public string Owner
{
get;
set;
}
public List<Post> Posts
{
get { return _posts; }
set { _posts = value; }
}
List<Post> _posts = new List<Post>();
}
图 7 Tag 实体的 POCO 对象
public class Tag
{
public int TagID
{
get;
set;
}
public string Name
{
get;
set;
}
public List<Post> Posts
{
get { return _posts; }
set { _posts = value; }
}
List<Post> _posts = new List<Post>();
}
图 8 Post 实体的 POCO 对象
public class Post
{
public int PostID
{
get;
set;
}
public DateTime CreatedDate
{
get;
set;
}
public DateTime ModifiedDate
{
get;
set;
}
public string Title
{
get;
set;
}
public string PostContent
{
get;
set;
}
public Blog Blog
{
get;
set;
}
public int BlogBlogID
{
get;
set;
}
public Boolean Public
{
get;
set;
}
public List<Tag> Tags
{
get { return _tags; }
set { _tags = value; }
}
private List<Tag> _tags = new List<Tag>();
}
最后,我需要创建上下文类,该类与通过默认代码生成而生成的 ObjectContext 实现非常类似,但我将其称为 BlogContext。该类将从 ObjectContext 类继承。上下文是持久性调用类。它将允许查询复合、将实体具体化,并将更改保存回数据库(请参见图 9)。
图 9 BlogContext
public class BlogContext : ObjectContext
{
public BlogContext()
: base("name=BlogContext", "BlogContext")
{
}
public ObjectSet<Blog> Blogs
{
get
{
if (_Blogs == null)
{
_Blogs =
base.CreateObjectSet<Blog>("Blogs");
}
return _Blogs;
}
}
private ObjectSet<Blog> _Blogs;
public ObjectSet<Post> Posts
{
get
{
if (_Posts == null)
{
_Posts =
base.CreateObjectSet<Post>("Posts");
}
return _Posts;
}
}
private ObjectSet<Post> _Posts;
public ObjectSet<Tag> Tags
{
get
{
if (_Tags == null)
{
_Tags = base.CreateObjectSet<Tag>("Tags");
}
return _Tags;
}
}
private ObjectSet<Tag> _Tags;
}
延迟加载
在 Visual Studio 2008 SP1 中,EF 支持两种基本的相关实体加载方式:使用 Load 方法显式加载相关实体,或使用 Include 方法在查询内自愿加载相关实体,这两种方式都能确保只有在明确指示应用程序命中数据库时,应用程序才会命中数据库。EF 4.0 中现在另一个最热门的功能是延迟加载。如果不需要执行显式加载,您可以使用延迟加载(也称为延期加载)在首次访问导航属性时加载相关实体。在 Visual Studio 2010 中,这一点是通过使导航属性成为虚拟属性而实现的。
在 MyBlog 示例中,Blog.cs 和 Tag.cs 中的公共 List<Post> Posts 属性都将成为公共虚拟 List<Post> Posts,而 Post.cs 中的公共 List<Tag> Tags 属性将成为公共虚拟 List<Tag> Tags。EF 随后将在运行时创建一个知道如何执行加载的代理类型,这样将无需另外更改代码。但是,由于 MyBlog 示例使用 WCF 数据服务通过开放数据协议 (OData) 服务公开实体,因此应用程序不使用延迟加载。
在 Visual Studio 2010 中创建 WCF 数据服务
MyBlog 利用 WCF 数据服务提供的接近于完整的解决方案通过 EDM 提供 OData 服务,并包括一个使用 OData 服务的 Silverlight 博客管理员客户端。开放数据协议是一种数据共享标准,针对数据使用者(客户端)和生成者(服务),该标准打破了信息孤立的情况,并形成了一个强大、可互操作的生态系统,从而使更多应用程序能够利用更广泛的数据集。
在设置了 EDM 和数据库后,向应用程序中添加新的 WCF 数据服务将很简单;我使用“添加新项”向导添加了一个 WCF 数据服务(我将其称为 BlogService)。这将生成 BlogService.svc 文件,该文件表示服务的框架,并将通过随之前创建的上下文一起提供而指向 EDM。由于默认情况下服务处于完全锁定状态,因此必须使用 config.SetEntitySetAccessRule 明确允许通过服务访问实体集。为此,将为提供的每个 EntitySet 设置一条规则,如图 10 中所示。
图 10 BlogService.svc
public class BlogService : DataService<BlogContext>
{
// This method is called only once to initialize service-wide policies.
public static void InitializeService(DataServiceConfiguration config)
{
// TODO: set rules to indicate which entity sets and service
// operations are visible, updatable, etc.
// Examples:
config.SetEntitySetAccessRule("Blogs", EntitySetRights.All);
config.SetEntitySetAccessRule("Posts", EntitySetRights.All);
config.SetEntitySetAccessRule("Tags", EntitySetRights.All);
// config.SetServiceOperationAccessRule("MyServiceOperation",
// ServiceOperationRights.All);
config.DataServiceBehavior.MaxProtocolVersion =
DataServiceProtocolVersion.V2;
}
}
(注意:如果您下载本文的示例代码,您将注意到,该代码使用非常简单的 Forms 身份验证方案来保护网站的安全,而其余示例也将使用该方案来基于当前登录的用户筛选数据。不过,由于实现 Forms 身份验证超出了本文范围,因此不在此处详细论述。)
在服务已启动并正在运行的情况下,下一步是基于当前登录的用户筛选结果,以便只会返回该用户拥有的博客。可通过添加查询侦听器(如图 11 中所示)来限制查询返回的实体,从而实现这一点。
图 11 查询侦听器
// returns only public posts and posts owned by the current user
[QueryInterceptor("Posts")]
public Expression<Func<Post, bool>>OnPostQuery()
{
return p =>p.Public == true ||
p.Blog.Owner.Equals(HttpContext.Current.User.Identity.Name);
}
// returns only the blogs the currently logged in user owns
[QueryInterceptor("Blogs")]
public Expression<Func<Blog, bool>>OnBlogQuery()
{
return b =>
b.Owner.Equals(HttpContext.Current.User.Identity.Name);
}
在 Silverlight 中使用 WCF 数据服务
有关构建 Silverlight UI 的详细信息超出了本文范围,因此我将忽略其中部分详细信息。但在深入探讨如何将数据服务与 Silverlight 应用程序挂接之前,我将向项目中添加一个新的 Silverlight 应用程序,其中包含默认 Silverlight 页面 MainPage.xaml。我将向其中添加基本的 DataGrid、ComboBox、Button 和几个标签。在框架 Silverlight 应用程序准备就绪后(请参见图 12),我们可以挂接数据服务。
图 12 MyBlog SilverlightAdministrator 应用程序的基本布局
首先,Silverlight 应用程序需要一些对象,这些对象表示数据服务定义的每个实体。为此,可使用 Visual Studio 中的“添加服务引用”向导为数据服务自动生成客户端类。(请注意,为了使用“添加服务引用”,我需要暂时禁用在服务上实现的授权检查,以便“添加服务引用”拥有服务的完全访问权限。我将“添加服务引用”向导指向服务的基本 URI,在 MyBlog 中为 localhost:48009/BlogService.svc)。
WCF 数据服务 4.0 中的数据绑定
WCF 数据服务 4.0 中改进的数据绑定支持向客户端库中添加了一种新集合类型 DataServiceCollection,从而扩展了 ObservableCollection。但是,在 Silverlight 3 中,向项目中添加服务引用时,默认情况下数据绑定处于禁用状态。因此,若要利用 WCF 数据服务中新的数据绑定功能,将需要启用数据绑定,并需要更新服务引用。从解决方案资源管理器中,单击“显示所有文件”按钮,并展开“服务引用”节点下的“BlogService”项。双击 Reference.datasvc 映射文件,并将 Parameters 元素替换为如下所示的 XML 片段:
<Parameters>
<Parameter Name="UseDataServiceCollection" Value="true" />
<Parameter Name="Version" Value="2.0" />
</Parameters>
如果将 UseDataServiceCollection 参数设置为 true,则会自动生成实现 INotifyPropertyChanged 和 INotifyCollectionChanged 接口的客户端类型。这意味着,对 DataServiceCollection 的内容或集合中的实体所做的任何更改都会反映在客户端上下文上。它还意味着,如果重新查询集合中的某个实体,则对该实体所做的任何更改都会反映在 DataServiceCollection 内的实体中。并且它意味着,由于 DataServiceCollection 实现标准绑定接口,因此它可作为 DataSource 绑定到大多数 WPF 和 Silverlight 控件。
返回到 MyBlog 示例,下一步是通过创建新的 DataServiceContext 来创建与服务的连接,并使用该连接来查询服务。图 13 包括 MainPage.xaml 和 MainPage.xaml.cs 并显示新的 DataServiceContext 的创建,同时在服务中查询所有博客(在本例中,服务将返回登录用户拥有的所有博客),并将这些博客绑定到 Silverlight 应用程序上的 ComboBox。
图 13 MainPage.xaml 和 MainPage.xaml.cs
MainPage.xaml
<Grid x:Name="LayoutRoot" Background="White" Width="618">
<data:DataGrid Name="grdPosts" AutoGenerateColumns="False"
Height="206" HorizontalAlignment="Left"Margin="17,48,0,0"
VerticalAlignment="Top" Width="363" ItemsSource="{Binding Posts}">
<data:DataGrid.Columns>
<data:DataGridTextColumn Header="Title" Binding="{Binding Title}"/>
<data:DataGridCheckBoxColumn Header="Public"
Binding="{Binding Public}"/>
<data:DataGridTextColumn Header="Text"
Binding="{Binding PostContent}"/>
</data:DataGrid.Columns>
</data:DataGrid>
<Button Content="Save" Height="23" HorizontalAlignment="Left"
Margin="275,263,0,0" Name="btnSave" VerticalAlignment="Top"
Width="75" Click="btnSave_Click_1" />
<ComboBox Height="23" HorizontalAlignment="Left"
Margin="86,11,0,0" Name="cboBlogs" VerticalAlignment="Top"
Width="199" ItemsSource="{Binding}" DisplayMemberPath="Name"
SelectionChanged="cboBlogs_SelectionChanged" />
<dataInput:Label Height="50" HorizontalAlignment="Left"
Margin="36,15,0,0" Name="label1"
VerticalAlignment="Top"Width="100" Content="Blogs:" />
<dataInput:Label Height="17" HorizontalAlignment="Left"
Margin="17,263,0,0" Name="lblCount" VerticalAlignment="Top"
Width="200" Content="Showing 0 of 0 posts"/>
<Button Content="Load More Posts" Height="23" HorizontalAlignment="Left" Margin="165,263,0,0" Name="btnMorePosts"
VerticalAlignment="Top" Width="100" Click="btnMorePosts_Click" />
</Grid>
MainPage.xaml.cs
public MainPage()
{
InitializeComponent();
svc = new BlogContext(new Uri("/BlogService.svc", UriKind.Relative));
blogs = new DataServiceCollection<Blog>(svc);
this.LayoutRoot.DataContext = blogs;
blogs.LoadCompleted +=
new EventHandler<LoadCompletedEventArgs>(blogs_LoadCompleted);
var q = svc.Blogs.Expand("Posts");
blogs.LoadAsync(q);
}
void blogs_LoadCompleted(object sender, LoadCompletedEventArgs e)
{
if (e.Error == null)
{
if (blogs.Count> 0)
{
cboBlogs.SelectedIndex = 0;
}
}
}
为了绑定 DataGrid,添加了一个 cboBlogs_SelectionChanged() 方法:
private void cboBlogs_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
this.grdPosts.DataContext = ((Blog)cboBlogs.SelectedItem);
}
ComboBox 中的当前选定项每次发生变化时,都将调用此方法。
要挂接到 Silverlight 应用程序上的最后一项是“Save”按钮,该按钮是通过添加对 DataServiceContext 调用 SaveChanges 的 btnSave_Click 方法来启用的,如图 14 中所示。
图 14 将更改保存回数据库
private void btnSave_Click_1(object sender, RoutedEventArgs e)
{
svc.BeginSaveChanges(SaveChangesOptions.Batch, OnChangesSaved, svc);
}
private void OnChangesSaved(IAsyncResult result)
{
var q = result.AsyncState as BlogContext;
try
{
// Complete the save changes operation
q.EndSaveChanges(result);
}
catch (Exception ex)
{
// Display the error from the response.
MessageBox.Show(ex.Message);
}
}
服务器驱动的分页
通常需要限制服务器针对给定查询将返回的结果总数,以免应用程序意外地拉回数量过大的数据。利用 WCF 数据服务 4.0 中服务器驱动的分页,服务作者能够在 InitializeService 方法中针对每个实体集合设置 SetEntitySetPageSize 属性,从而按集合限制服务为每个请求返回的实体总数。除了限制为每个请求返回的实体数之外,数据服务还将为客户端提供“下一个链接”- 一个指定客户端要如何检索集合中的下一组实体的 URI,采用 AtomPub<link rel=”next”> 元素的形式。
返回到 MyBlog 示例,我将在我的服务上为 Posts EntitySet 将 SetEntitySetPageSize 属性设置为五个结果:
config.SetEntitySetPageSize("Posts", 5);
在服务中查询 Posts 时,这会限制返回的实体数。我在此处将 SetEntitySetPageSize 属性设置为一个小数字来阐释该功能的工作方式;通常应用程序会设置一个大多数客户端将不会达到的限制(实际上,客户端会使用 $top 和 $skip 来随时控制请求的数据量)。
我还将向应用程序中添加一个新按钮,以允许用户从服务中请求 Posts 的下一页。以下代码段显示访问下一组 Posts 的 btnMorePosts_Click 方法:
private void btnMorePosts_Click(object sender, RoutedEventArgs e)
{
Blog curBlog = cboBlogs.SelectedItem as Blog;
curBlog.Posts.LoadCompleted += new
EventHandler<LoadCompletedEventArgs>(Posts_LoadCompleted);
curBlog.Posts.LoadNextPartialSetAsync();
}
行计数
Visual Studio 2008 SP1 中的 ADO.NET 数据服务发布之后,其中一个最热门的功能是能够确定集合中的实体总数,而无需从数据库中拉回所有这些实体。在 WCF 数据服务 4.0 中,我们增加了“行计数”功能来实现此目的。
在客户端上创建查询时,可以调用 IncludeTotalCount 方法以在响应中包括计数标记。随后可以使用 QueryOperationResponse 对象上的 TotalCount 属性来访问值,如图 15 中所示。
图 15 使用行计数
private void cboBlogs_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Blog curBlog = this.cboBlogs.SelectedItem as Blog;
this.grdPosts.DataContext = curBlog;
var q = (from p in svc.Posts.IncludeTotalCount()
where p.BlogBlogID == curBlog.ID
select p) as DataServiceQuery<Post>;
curBlog.Posts.LoadCompleted += new
EventHandler<LoadCompletedEventArgs>(Posts_LoadCompleted);
curBlog.Posts.LoadAsync(q);
}
void Posts_LoadCompleted(object sender, LoadCompletedEventArgs e)
{
if (e.Error == null)
{
Blog curBlog = cboBlogs.SelectedItem as Blog;
totalPostCount = e.QueryOperationResponse.TotalCount;
string postsCount = string.Format("Displaying {0} of {1} posts",
curBlog.Posts.Count, totalPostCount);
this.lblCount.Content = postsCount;
curBlog.Posts.LoadCompleted -= Posts_LoadCompleted;
}
}
投影
WCF 数据服务 4.0 中另一项最热门的功能是“投影”,即能够指定要从查询中返回的实体属性的子集,从而允许应用程序针对带宽消耗和内存占用进行优化。在 Visual Studio 2010 中,数据服务 URI 格式已扩展为包括 $select 查询选项,从而使客户端能够指定要由查询返回的属性的子集。例如,对于 MyBlog,我可以使用以下 URI 查询所有 Posts 并仅投射 Title 和 PostContent:BlogService.svc/Posts?$select=Title,PostContent。在客户端上,您现在也可以使用 LINQ 进行包含投影的查询。
了解更多
尽管本文所含内容已足以让您对 Visual Studio 2010 中的实体框架 4.0 和 WCF 数据服务 4.0 有初步了解,但您感兴趣的其他主题和新功能可能仍然有很多。有关详细信息,请访问 MSDN 数据开发中心,网址为 msdn.microsoft.com/data。
Elisa Flasko 是 Microsoft 数据可编程性团队的一名项目经理,主要从事 ADO.NET 实体框架、WCF 数据服务、M、Quadrant 和 SQL Server 建模服务技术的研究。可通过 blogs.msdn.com/elisaj 与她取得联系。
衷心感谢以下技术专家,感谢他们审阅了本文:Jeff Derstadt 和 Mike Flasko