zoukankan      html  css  js  c++  java
  • WCF4 Rest Service

    原文引自:http://www.uml.org.cn/net/2011482.asp

    (一)、创建一个基本的RESTful Service

    很久没有玩WCF了,年前和几个朋友做一个Restful的服务,惊讶于WCF4已经能如此完美实现REST(好吧,我承认不是完全完美)。期间遇到了许多难题,但还是一一解决,或者找到了workaround. 为了让以后想做同样事情的朋友不至于像我们这般抓狂,也为了给自己整理一下所学,我准备写一个系列,讲述如何从零开始创建一个完整的支持Token验证的REST服务,其内容将涉及如何控制返回内容格式,如何处理异常并返回自定义内容,如何建立基于token的身份验证,如何使用POCO,如何做service的单元测试……总之,总之,看了就知道了。鉴于文笔拙劣,技亦不精,不敢和大牛比肩,姑且自称为《我的WCF4 Rest Service及Entity Framework with POCO之旅》吧,还请各位高手多多指教。

    照理说,首先应介绍什么是REST,它是怎么出现的,有什么优势,什么历史意义。不过我不多想讲,也讲不清楚。简而言之,REST最核心的概念是“资源”,一个uri代表一个特定的资源,而客户端通过HTTP method GET, POST, PUT, DELETE来和资源交互(同时导致状态转移)。至于资源的表现形式,则以XML和JSON为主。为什么要用REST? 自己想去吧。

    创建项目

    闲话说完,现在开始创建项目。使用Online Templates中的WCF REST Service Template是一个比较容易的方法:

    项目创建好后,我们可以看到项目中已经有下面这些文件:

    Service1就是一个WCF Service,和以前的WCF Service不同,这里的Service既不是一个.svc文件,也没有专门定义一个接口作为ServiceContract。

    SampleItem就是我们以往所知的DataContract,不过现在即使不加DataContract attribute,也没有关系。

    比较特别的是Global.asax和Global.asax.cs。Global.asax.cs中定义了Routes:

    private void RegisterRoutes()

    { // Edit the base address of Service1 by replacing the "Service1" string below RouteTable.Routes.Add(new ServiceRoute("Service1", new WebServiceHostFactory(), typeof(Service1)));}

    表示所有访问HOST/Service1的请求都要由Service1这个类来处理。再看一下Service1的定义,其中有个方法是:

    [WebGet(UriTemplate = "{id}")]

    public SampleItem Get(string id)

    { // TODO: Return the instance of SampleItem with the given id throw new NotImplementedException();}

    这表示对于HOST/Service1/123的GET请求,将会用这个Get(string)方法来处理。服务运行时,WCF根据uri中的“Service1”根据前面指定的Route找到Service1这个服务类,再根据UriTemplate,将“123”赋给名为id的参数,其结果就是Get方法以参数“123”被调用了。

    现在我们修改一下这个方法,让它能够返回结果:

    [WebGet(UriTemplate = "{id}")]

    public SampleItem Get(string id)

    { return new SampleItem { Id = int.Parse(id), StringValue = string.Format("The id is {0}.", id) };}

    然后启动这个服务。在浏览器地址栏中输入“http://localhost:6421/Service1/123”(端口号根据实际情况而定),结果如下:

    如何,是不是很简单?

    不过现在的RESTful服务还只能读取资源,WebGet只支持GET方法,要支持PUT, POST和DELETE,需要使用WebInvoke attribute。而且,现在的服务看不出任何实际意义。

    既然掌握了基本技术,我们来创建一个稍微有点实际意义的服务

    创建一个简易微博服务
    (终于不是做图书管理系统示例了)

    首先,我们的微博服务真的很简单,简单到只能有一个用户自娱自乐,而且还只能发文字,是不是有点无聊?不过不要急,一步一步来。

    既然如此,所谓的微博服务,也就是一个微博管理系统了(…)。它应该有这些功能:

    发布微博

    查看已发布的微博

    删除一条微博

    照此删去原先那些示例代码,重新编写代码如下:

    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Linq;
    using System.ServiceModel;
    using System.ServiceModel.Activation;
    using System.ServiceModel.Web;
    using System.Threading;
     
    namespace WcfRestServiceDemo.Service
    {
          [ServiceContract(Namespace = "WcfRestServiceDemo")]
          [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
          [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
          public class MicroblogService
          {
              private static int _currentId;
              private static readonly ConcurrentDictionary<int, Microblog> _microblogs =
                  new ConcurrentDictionary<int, Microblog>();
    
              [WebGet(UriTemplate = "")]
              public List<Microblog> GetCollection()
              {
                  return _microblogs.Values.ToList();
              }
              [WebInvoke(UriTemplate = "", Method = "POST")]
              public Microblog Create(Microblog microblog)
              {
                  microblog.Id = Interlocked.Increment(ref _currentId);
                  microblog.PublishTime = DateTime.Now;
                  _microblogs.TryAdd(microblog.Id, microblog);
                  return microblog;
              }
              [WebGet(UriTemplate = "{id}")]
              public Microblog Get(string id)
              {
                  return _microblogs[int.Parse(id)];
              }
              [WebInvoke(UriTemplate = "{id}", Method = "DELETE")]
              public void Delete(string id)
              {
                  Microblog microblog;
                  _microblogs.TryRemove(int.Parse(id), out microblog);
              }
          }
    }
    using System;
    using System.Runtime.Serialization;
     
    namespace WcfRestServiceDemo.Service
    {
          [DataContract(Namespace = "WcfRestServiceDemo")]
          public class Microblog
          {
              [DataMember]
              public int Id { get; set; }
              [DataMember]
              public string Content { get; set; }
              [DataMember]
              public DateTime PublishTime { get; set; }
          }
    }

    Global.asax.cs
     
    using System;
    using System.ServiceModel.Activation;
    using System.Web;
    using System.Web.Routing;
     
    namespace WcfRestServiceDemo.Service
    {
          public class Global : HttpApplication
          {
              private void Application_Start(object sender, EventArgs e)
              {
                  RegisterRoutes();
              }
              private void RegisterRoutes()
              {
                  RouteTable.Routes.Add(new ServiceRoute("microblogs", new WebServiceHostFactory(),
    typeof (MicroblogService)));           }       } }
    编译并运行。

    理所当然什么都没有,一条微博都还没发呢。为了方便的发送请求并查看结果,祭出神器Fiddler2:

    在右边窗口可以看到请求返回的XML。

    接下来选择右边上面的Request Builder,来构造一个POST:

    (可以把左边那个请求拖入编辑窗口快速复制一个请求。)

    (注意要在上面的Headers窗口添加Content-Type: application/xml这一项,同时注意XML的namespace。)

    点击Execute:

    服务返回HTTP 200 OK,并且可以从Response的内容中看到成功创建的Microblog的Id以及PublishTime的值。

    再次获取全部:

    已经可以看到刚才创建的微博了。现在再试一下其他两个功能:

    获取单条微博:

    删除指定微博:

    再次查询全部,结果又是空空如也了。到这里,一个超级简单的“微博”服务已经搭建起来了。

    (二)——选择请求/返回格式

    REST被推崇,一个很重要的原因就是它的数据格式可以是XML,也可以是JSON,这使得Javascript可以轻松调用RESTful服务。WCF REST内置了JSON序列化器,可以轻易实现格式的转换,甚至可以根据请求动态选择返回的格式。

    先看看如何手动指定返回格式。WebGetAttribute和WebInvokeAttribute有RequesetFormat和ReponseFormat两个属性,可以指定请求/返回格式为XML或者JSON:

    但相比之下,动态选择格式还是更灵活也吸引人一些。打开之前创建的项目,在自动生成的web.config中,有这么一条配置:

    <standardEndpoint name="" helpEnabled="true" automaticFormatSelectionEnabled="true"/>设定标准Endpoint为打开服务帮助,并且打开自动格式选择。

    打开服务帮助就是当访问/microblogs/help时会看到如下的帮助界面:

    列举了服务的接口,每个接口还说明了其详细用法、示例,以及请求/返回数据的scheme。

    而自动格式选择,分为两部分:自动根据请求,选择请求内容的格式和回复内容的格式。这两者分别是通过HTTP请求头中的Content-Type和Accept这两项指定的。

    在Fiddler2中把Content-Type改成application/json, 表示请求的内容格式是JSON,再次发布一条微博:

    请求成功,可以看到请求内容是JSON格式,服务器成功处理了,但返回还是XML格式:

    现在再修改Accept头为application/json, 表示只接受JSON的返回格式,然后获取所有的微博:

    结果如下:

    可以看到,返回内容已经是JSON格式了。等等,那个时间的格式好像有点怪?另外,大写的field名称似乎也不合习惯。别急,我们以后会解决这些问题。

    (三)——用Entity Framework和POCO Template实现数据模型及存储

    既然这个系列标题中都含有Entity Framework和POCO,这两者若到第三篇还不出现那就太奇怪了。本文将介绍如何使用Entity Framework和POCO来实现数据模型的创建以及数据存储。

    接着上文,我们现在已经有了一个微博服务。但是,一旦网站重启或者IIS重启,我们就会丢失之前所有发布的微博,这样的服务显然不是我们所能接受的,我们接下来要构建服务的数据存储。使用Entity Framework的Model-First设计方式,可以大大简化这个过程。

    使用Entity Data Model创建数据模型

    我们首先创建一个类库项目WcfRestServiceDemo.Data,然后添加新项“ADO.NET Entity Data Model”:

    选择数据源,如果有现成的数据源的话可以从数据库导入。我们选择Empty model, 新创建数据模型。

    从工具箱中找到Entity,拖入设计器,参考之前创建的数据类型,修改得到:

    到这里数据模型就创建完了。下面我们要把它同步到SQL Server。

    由模型生成数据库

    到目前为止,模型还只存在C#项目中,虽然我们已经可以写代码来操纵数据,但实际上是无法运行的(或者说一运行就会挂掉的)。

    要将模型同步到SQL Server,对设计器视图点右键,选择Generate Database from Model…

    选择一个连接,没有的话就创建一个:

    然后对应的SQL脚本就自动生成好了:

    打开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=
    &quot;Data Source=.;Initial Catalog=WcfRestServiceDemo;Integrated Security=True;
    MultipleActiveResultSets=True
    &quot;"  providerName="System.Data.EntityClient"
    />

    这里附带一提,有的时候我们会遇到无法正确加载Model的情形,这时只要将上面那个连接字符串中的*替换成数据模型(*.edmx)所在的程序集名称(这里是WcfRestServiceDemo.Data),显式指定搜索路径即可。

    怎么回事?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了。

    使用ADO.NET C# POCO Entity Generator生成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 = isLazy;
                    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代码如下:

    usingstyle="FONT-SIZE: 10pt" 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))));
            }
        }
    
    

    啊哈,又回到最初干净的状态了。只是Namespace问题以及属性名称的大小写问题还是没有解决,但是我们已经离目标更近一步了

  • 相关阅读:
    可变性编程 不可变性编程 可变性变量 不可变性变量 并发编程 命令式编程 函数式编程
    hashable
    优先采用面向表达式编程
    内存转储文件 Memory.dmp
    windows update 文件 路径
    tmp
    查询局域网内全部电脑IP和mac地址等信息
    iptraf 网卡 ip 端口 监控 netstat 关闭端口方法
    Error 99 connecting to 192.168.3.212:6379. Cannot assign requested address
    t
  • 原文地址:https://www.cnblogs.com/xiarifeixue/p/restful.html
Copyright © 2011-2022 走看看