既然这个系列标题中都含有Entity Framework和POCO,这两者若到第三篇还不出现那就太奇怪了。本文将介绍如何使用Entity Framework和POCO来实现数据模型的创建以及数据存储。
接着上文,我们现在已经有了一个微博服务。但是,一旦网站重启或者IIS重启,我们就会丢失之前所有发布的微博,这样的服务显然不是我们所能接受的,我们接下来要构建服务的数据存储。使用Entity Framework的Model-First设计方式,可以大大简化这个过程。
我们首先创建一个类库项目WcfRestServiceDemo.Data,然后添加新项“ADO.NET Entity Data Model”:
选择数据源,如果有现成的数据源的话可以从数据库导入。我们选择Empty model, 新创建数据模型。
从工具箱中找到Entity,拖入设计器,参考之前创建的数据类型,修改得到:
到这里数据模型就创建完了。下面我们要把它同步到SQL Server。
到目前为止,模型还只存在C#项目中,虽然我们已经可以写代码来操纵数据,但实际上是无法运行的(或者说一运行就会挂掉的)。
要将模型同步到SQL Server,对设计器视图点右键,选择Generate Database from Model…
打开WcfRestServiceDemo.edmx.sql,右键选择Execute SQL,成功后模型就同步到SQL Server了。
现在数据模型和数据存储都做好了,我们要修改服务代码来使用新的数据模型。
删去Microblog.cs,添加对WcfRestServiceDemo.Data和System.Data.Entity的引用,并修改MicroblogService.cs代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.ServiceModel; using System.ServiceModel.Activation; using System.ServiceModel.Web; using WcfRestServiceDemo.Data; namespace WcfRestServiceDemo.Service { [ServiceContract(Namespace = "WcfRestServiceDemo")] [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] public class MicroblogService { [WebGet(UriTemplate = "")] public List<Microblog> GetCollection() { using(var container = new WcfRestServiceDemoContainer()) { return container.Microblogs.ToList(); } } [WebInvoke(UriTemplate = "", Method = "POST")] public Microblog Create(Microblog microblog) { microblog.PublishTime = DateTime.Now; using(var container = new WcfRestServiceDemoContainer()) { container.Microblogs.AddObject(microblog); container.SaveChanges(); } return microblog; } [WebGet(UriTemplate = "{id}")] public Microblog Get(string id) { using(var container = new WcfRestServiceDemoContainer()) { return container.Microblogs.FirstOrDefault(m => m.Id == int.Parse(id)); } } [WebInvoke(UriTemplate = "{id}", Method = "DELETE")] public void Delete(string id) { using(var container = new WcfRestServiceDemoContainer()) { container.Microblogs.DeleteObject( container.Microblogs.First(m => m.Id == int.Parse(id))); container.SaveChanges(); } } } }
要让Entity Framework正常工作,我们还需要把它生成的app.config文件中的ConnectionString复制到服务的web.config文件中:
<add name="WcfRestServiceDemoContainer" connectionString="metadata=res://*/WcfRestServiceDemo.csdl|res://*/WcfRestServiceDemo.ssdl|res://*/WcfRestServiceDemo.msl;provider=System.Data.SqlClient;provider connection string="Data Source=.;Initial Catalog=WcfRestServiceDemo;Integrated Security=True;MultipleActiveResultSets=True"" providerName="System.Data.EntityClient" />
这里附带一提,有的时候我们会遇到无法正确加载Model的情形,这时只要将上面那个连接字符串中的*替换成数据模型(*.edmx)所在的程序集名称(这里是WcfRestServiceDemo.Data),显式指定搜索路径即可。
好,都改好了,现在测试一下服务,我先手工往数据库中插入一条记录,然后访问
http://localhost:6421/microblogs/:
怎么回事?Content, Id, PublishTime这些属性倒是一个也不少,但是也多了很多其他的东西。这是因为与POCO不同,基于EntityObject的Entity Data Model支持更改提醒、关系管理等许多其他功能,所以会包含更多的属性,只不过在我们这个应用场景中,并不需要这些支持。另一个问题是,这时默认的命名空间变成了
xmlns=http://schemas.datacontract.org/2004/07/WcfRestServiceDemo.Data
而且由于Microblog这个DataModel是自动生成的,我没有办法改它的DataContractAttribute(改了下次生成又会变回来):
[EdmEntityTypeAttribute(NamespaceName="WcfRestServiceDemo", Name="Microblog")] [Serializable()] [DataContractAttribute(IsReference=true)] public partial class Microblog : EntityObject { … }
要解决上述问题,我们该请出下一位主角POCO了。
POCO全称Plain-Old CLR Objects(中文不知道怎么说)。以前有很多人手写POCO模型,不过现在已经有了一个非常强大的基于T4模板的模型生成器。
打开Visual Studio 2010的Extension Manager,找到ADO.NET C# POCO Entity Generator并安装(或者在这里下载):
然后打开WcfRestServiceDemo.edmx,右键选择Add Code Generation Item…:
Model.Context.tt和Model.tt分别是Container和Entity的T4模板。现在再运行一下服务,获取所有微博:
为什么会Empty Response? 调试一下服务,发现返回的类型不是Microblog, 而是System.Data.Entity.DynamicProxies.Microblog_
C60A574FC06ABEF4672858332CE687DDE70D188AD3BD73ED2731E8854D30C927,而WCF并不认识这个类型。这是因为默认的Entity Data Model启用了LazyLoading和Proxy机制的缘故。这两个特性我以后也许会详细讲,不过目前我们只要把它们关闭就好。虽然Model设计器提供了下面这个属性可以修改:
但是当我改成False然后对Model.Context.tt右键选择Run Custom Tool 重新生成Container代码,却没有任何效果,Container构造器代码依旧如下:
如果有哪位高手知道应该如何设置,请告诉我。我们现在先通过扩展Container类的方法来解决这个问题,下一篇讲如何通过修改T4模板来改变生成的Container。
在WcfRestServiceDemo.Data中创建WcfRestServiceDemoContainer.cs,代码如下:
using System; namespace WcfRestServiceDemo.Data { partial class WcfRestServiceDemoContainer { public static void Go(Action<WcfRestServiceDemoContainer> todo, bool isLazy = false) { Go<object>(entities => { todo(entities); return null; }, isLazy); } public static void GoAndSave(Action<WcfRestServiceDemoContainer> todo, bool isLazy = false) { Go<object>(entities => { todo(entities); entities.SaveChanges(); return null; }, isLazy); } public static T Go<T>(Func<WcfRestServiceDemoContainer, T> todo, bool isLazy = false) { using(var entities = new WcfRestServiceDemoContainer()) { entities.ContextOptions.LazyLoadingEnabled = isLazy; entities.ContextOptions.ProxyCreationEnabled = false; return todo(entities); } } public static T GoAndSave<T>(Func<WcfRestServiceDemoContainer, T> todo, bool isLazy = false) { return Go(entities => { var result = todo(entities); entities.SaveChanges(); return result; }, isLazy); } } }
这里我顺便写了几个帮助方法,因为每次使用Container时写using很麻烦,关键是我经常改完数据忘记调SaveChanges().
最后修改MicroblogService.cs代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.ServiceModel; using System.ServiceModel.Activation; using System.ServiceModel.Web; using WcfRestServiceDemo.Data; namespace WcfRestServiceDemo.Service { [ServiceContract(Namespace = "WcfRestServiceDemo")] [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] public class MicroblogService { [WebGet(UriTemplate = "")] public List<Microblog> GetCollection() { return WcfRestServiceDemoContainer.Go(container => container.Microblogs.ToList()); } [WebInvoke(UriTemplate = "", Method = "POST")] public Microblog Create(Microblog microblog) { microblog.PublishTime = DateTime.Now; WcfRestServiceDemoContainer.GoAndSave(container => container.Microblogs.AddObject(microblog)); return microblog; } [WebGet(UriTemplate = "{id}")] public Microblog Get(string id) { return WcfRestServiceDemoContainer.Go(container => container.Microblogs.FirstOrDefault(m => m.Id == int.Parse(id))); } [WebInvoke(UriTemplate = "{id}", Method = "DELETE")] public void Delete(string id) { WcfRestServiceDemoContainer.GoAndSave(container => container.Microblogs.DeleteObject( container.Microblogs.First(m => m.Id == int.Parse(id)))); } } }
再次访问http://localhost:6421/microblogs/:
啊哈,又回到最初干净的状态了。只是Namespace问题以及属性名称的大小写问题还是没有解决,但是我们已经离目标更近一步了,我将在下一章讨论这些。
本文介绍了: