[转载]一个很好的php分词类库 - 拾贝壳的小男孩 - 博客园

mikel阅读(794)

[转载]一个很好的php分词类库 – 拾贝壳的小男孩 – 博客园.

最近做实验,苦于php分词的问题,从网上找了很多,但都不行,导致试验结果不是很好,今天早晨抱着不放弃的努力,又试着重网上找开源的php分词词库,终于功夫不负有心人。

发现了一个很好的分词类库phpanalysis2.0。

原文连接地址:http://www.phpbone.com/phpanalysis/

        分 词系统简介:PHPAnalysis分词程序使用居于unicode的词库,使用反向匹配模式分词,理论上兼容编码更广泛,并且对utf-8编码尤为方 便。 由于PHPAnalysis是无组件的系统,因此速度会比有组件的稍慢,不过在大量分词中,由于边分词边完成词库载入,因此内容越多,反而会觉得速度越 快,这是正常现象,PHPAnalysis的词库是用一种类似哈希(Hash)的数据结构进行存储的,因此对于比较短的字符串分词,只需要占极小的资源, 比那种一次性载入所有词条的实际性要高得多,并且词库容量大小不会影响分词执行的速度。
      PHPAnalysis分词系统是基于字符串匹配的分词方法进行分词的,这种方法又叫做机械分词方法,它是按照一定的策略将待分析的汉字串与 一个“充分大的”机器词典中的词条进行配,若在词典中找到某个字符串,则匹配成功(识别出一个词)。按照扫描方向的不同,串匹配分词方法可以分为正向匹配 和逆向匹配;按照不同长度优先匹配的情况,可以分为最大(最长)匹配和最小(最短)匹配;按照是否与词性标注过程相结合,又可以分为单纯分词方法和分词与 标注相结合的一体化方法。常用的几种机械分词方法如下:
1)正向最大匹配法(由左到右的方向);
2)逆向最大匹配法(由右到左的方向);
3)最少切分(使每一句中切出的词数最小)。
      还可以将上述各种方法相互组合,例如,可以将正向最大匹配方法和逆向最大匹配方法结合起来构成双向匹配法。由于汉语单字成词的特点,正向最小匹配和逆向 最小匹配一般很少使用。一般说来,逆向匹配的切分精度略高于正向匹配,遇到的歧义现象也较少。统计结果表明,单纯使用正向最大匹配的错误率为1/169, 单纯使用逆向最大匹配的错误率为1/245。但这种精度还远远不能满足实际的需要。实际使用的分词系统,都是把机械分词作为一种初分手段,还需通过利用各 种其它的语言信息来进一步提高切分的准确率。另一种方法是改进扫描方式,称为特征扫描或标志切分,优先在待分析字符串中识别和切分出一些带有明 显特征的词,以这些词作为断点,可将原字符串分为较小的串再来进机械分词,从而减少匹配的错误率。另一种方法是将分词和词类标注结合起来,利用丰富的词类 信息对分词决策提供帮助,并且在标注过程中又反过来对分词结果进行检验、调整,从而极大地提高切分的准确率。
     PHPAnalysis分词先对需要分词的词进行粗分,然后对粗分的短句子进行二次逆向最大匹配法(RMM)的方法进行分词,分词后对分词结果进行优化,然后才得到最终的分词结果。


PHPAnalysis类API文档

一、比较重要的成员变量
$resultType   = 1        生成的分词结果数据类型(1 为全部, 2为 词典词汇及单个中日韩简繁字符及英文, 3 为词典词汇及英文)
                                    这个变量一般用 SetResultType( $rstype ) 这方法进行设置。
$notSplitLen  = 5        切分句子最短长度
$toLower      = false    把英文单词全部转小写
$differMax    = false    使用最大切分模式对二元词进行消岐
$unitWord     = true     尝试合并单字(即是新词识别)
$differFreq   = false    使用热门词优先模式进行消岐
二、主要成员函数列表
1、public function __construct($source_charset='utf-8', $target_charset='utf-8', $load_all=true, $source='') 
函数说明:构造函数
参数列表:
$source_charset      源字符串编码
$target_charset      目录字符串编码
$load_all            是否完全加载词典(此参数已经作废)
$source              源字符串
如果输入输出都是utf-8,实际上可以不必使用任何参数进行初始化,而是通过 SetSource 方法设置要操作的文本
2、public function SetSource( $source, $source_charset='utf-8', $target_charset='utf-8' )
函数说明:设置源字符串
参数列表:
$source              源字符串
$source_charset      源字符串编码
$target_charset      目录字符串编码
返回值:bool
3、public function StartAnalysis($optimize=true)
函数说明:开始执行分词操作
参数列表:
$optimize            分词后是否尝试优化结果
返回值:void
一个基本的分词过程:
//////////////////////////////////////
$pa = new PhpAnalysis();

$pa->SetSource('需要进行分词的字符串');

//设置分词属性
$pa->resultType = 2;
$pa->differMax  = true;

$pa->StartAnalysis();

//获取你想要的结果
$pa->GetFinallyIndex();
////////////////////////////////////////
4、public function SetResultType( $rstype )
函数说明:设置返回结果的类型
实际是对成员变量$resultType的操作
参数 $rstype 值为:
1 为全部, 2为 词典词汇及单个中日韩简繁字符及英文, 3 为词典词汇及英文
返回值:void
5、public function GetFinallyKeywords( $num = 10 )
函数说明:获取出现频率最高的指定词条数(通常用于提取文档关键字)
参数列表:
$num = 10  返回词条个数
返回值:用","分隔的关键字列表
6、public function GetFinallyResult($spword=' ')
函数说明:获得最终分词结果
参数列表:
$spword    词条之间的分隔符
返回值:string
7、public function GetSimpleResult()
函数说明:获得粗分结果
返回值:array
8、public function GetSimpleResultAll()
函数说明:获得包含属性信息的粗分结果
属性(1中文词句、2 ANSI词汇(包括全角),3 ANSI标点符号(包括全角),4数字(包括全角),5 中文标点或无法识别字符)
返回值:array
9、public function GetFinallyIndex()
函数说明:获取hash索引数组
返回值:array('word'=>count,...) 按出现频率排序
10、public function MakeDict( $source_file, $target_file='' )
函数说明:把文本文件词库编译成词典
参数列表:
$source_file   源文本文件
$target_file   目标文件(如果不指定,则为当前词典)
返回值:void
11、public function ExportDict( $targetfile )
函数说明:导出当前词典全部词条为文本文件
参数列表:
$targetfile  目标文件
返回值:void

测试代码


<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
test

<!--?php   require_once 'phpanalysis2.0/phpanalysis.class.php';   $pa=new PhpAnalysis();   $pa--->SetSource("PHPAnalysis分词系统是基于字符串匹配的分词方法进行分词的,这种方法又叫做机械分词方法,它是按照一定的策略将待分析的汉字串与 一个“充分大的”机器词典中的词条进行配,若在词典中找到某个字符串,则匹配成功(识别出一个词)。按照扫描方向的不同,串匹配分词方法可以分为正向匹配 和逆向匹配;按照不同长度优先匹配的情况,可以分为最大(最长)匹配和最小(最短)匹配;按照是否与词性标注过程相结合,又可以分为单纯分词方法和分词与 标注相结合的一体化方法。常用的几种机械分词方法如下: ");
$pa-&gt;resultType=2;
$pa-&gt;differMax=true;
$pa-&gt;StartAnalysis();
$arr=$pa-&gt;GetFinallyIndex();
echo "
<pre>";
  print_r($arr);
  echo "</pre>
";

?&gt;

效果如下:

 

[转载]ACCESS数据库转换MYSQL数据库的软件-月光博客

mikel阅读(1004)

[转载]ACCESS数据库转换MYSQL数据库的软件-月光博客.

一、 软件介绍

DB2MYSQL是一个可以自动将ACCESS数据库文件转化为对应的SQL代码的软件。可广泛应用于ACCESS数据库转换为MYSQL或其他类型的SQL数据库的软件。

DB2MYSQL能够实现可视化操作数据库,转换界面非常简单明了,可轻松实现ACCESS数据库转换为MYSQL数据库。如果想要将SQL SERVER的数据库转换到MYSQL,也可以使用这个软件,具体操作是先使用SQL SERVER的“导入和导出数据”将SQL SERVER数据导出到ACCESS文件,然后再将ACCESS文件导出为MYSQL语句。

运行转换功能后,系统会生成两个输出,一个是在屏幕界面上将转换后的SQL语句输出,另外还会在输出目录下自动生成一个后缀为.SQL的文本文件,里面会记录生成的SQL语句。

使用本程序需要对数据库结构原理、SQL语言有一些背景知识。

二、操作简介

DB2MySQL的界面

  本软件为英文界面,纯绿色软件。不过需要先安装微软的MDAC(Microsoft Data Access Components)才能使用。首先解压缩全部文件到你想要的文件夹下。然后运行 DB2MYSQL.EXE 文件。

运行本软件后,在开始界面里选择所要使用的数据库,表和字段。 具体操作:

1、先打开需要使用的ACESS数据库,数据库上的表将列出在数据表列表框里。 点击数据表即可列出当前数据表的所有字段。

2、点击所需的表进行表和字段选择。

3、确定数据表后即可点击[Start]按钮。就可以自动生成SQL语句了。

4、运行PHPMYSQL,先创建一个和ACCESS表完全相同的表,然后导入自动生成的SQL语句,即可完成ACCESS数据转换到MYSQL数据了。

转换后的SQL语句不仅仅在MYSQL下可以执行,在Microsoft SQL Server和ACCESS中同样可以执行。

三、授权方式

本软件的授权方式是自由软件,即,软件使用者有使用、复制、散布、研究、改写、再利用该软件的自由。更精确地说,自由软件赋予使用者四种自由:

不论目的为何,有使用该软件的自由。

有研究该软件如何运作的自由,并且得以改写该软件来符合使用者自身的需求。取得该软件之源码为达成此目的之前提。

有重新散布该软件的自由,所以每个人都可以藉由散布自由软件来敦亲睦邻。

有改善再利用该软件的自由,并且可以发表改写版供公众使用,如此一来,整个社群都可以受惠。如前项,取得该软件之源码为达成此目的之前提。

使用者必须能够自由地、以不收费或是收取合理的散布费用的方式、在任何时间再散布该软件的原版或是改写版在任何地方给任何人使用。

四、下载地址

软件下载地址:ACCESS数据库转换MYSQL数据库的软件

五、联系方式

软件开发:月光博客

博客地址:http://www.williamlong.info

六、英文版

英文版介绍地址:Free Access to MySQL Database Converter

[转载]ASP.NET Web API标准的“管道式”设计 - Artech - 博客园

mikel阅读(926)

[转载]ASP.NET Web API标准的“管道式”设计 – Artech – 博客园.

ASP.NET Web API的核心框架是一个消息处理管道,这个管道是一组HttpMessageHandler的有序组合。这是一个双工管道,请求消息从一端流入并依次经过 所有HttpMessageHandler的处理。在另一端,目标HttpController被激活,Action方法被执行,响应消息随之被生成。响 应消息逆向流入此管道,同样会经过逐个HttpMessageHandler的处理。这是一个独立于寄宿环境的抽象管道,如何实现对请求的监听与接收,以 及将接收的请求传入消息处理管道进行处理并将管道生成的响应通过网络回传给客户端,这就是Web API寄宿需要解决的问题。

目录
一、HttpMessageHandler
二、DelegatingHandler
三、HttpServer
四、HttpRoutingDispatcher
五、HttpControllerDispatcher

一、HttpMessageHandler

ASP.NET Web API的消息处理管道由一组HttpMessageHandler经过“首尾相连”而成,ASP.NET Web API之所以具有较高的可扩展性,主要源于采用的管道式设计。虽然ASP.NET Web API框架旨在实现针对请求的处理和响应的回复,但是采用的处理策略因具体的场景而不同。

我们不可能也没有必要创建一个“万能”的处理 器来满足所有的请求处理需求,倒不如让某个处理器只负责某个单一的消息处理功能。在具体的应用场景中,我们可以根据具体的消息处理需求来选择所需的处理器 并组成一个完整的消息处理管道。在这里这个用于完成某个单一消息处理功能的处理器就是HttpMessageHandler。

这里的“消息处理”具有两个层面的含义,既包括针对请求消息的处理,还包括针对响应消息的 处理。HttpMessageHandler直接或者间接继承自具有如下定义的抽象类型HttpMessageHandler,该类型定义在命名空间 “System.Net.Http”下。ASP.NET Web API通过类型HttpRequestMessage和HttpResponseMessage来表示管道处理的请求消息和响应消息,所以对 HttpMessageHandler的定义就很好理解了。

   1: public abstract class HttpMessageHandler : IDisposable
   2: {
   3:     public void Dispose();
   4:     protected virtual void Dispose(bool disposing);
   5:     protected abstract Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
   6: }

 

如上面的代码片断所示,抽象类HttpMessageHandler定义了一个受保护的抽象方法SendAsync,这是一个采用针对Task的“并行编 程模式”的异步方法,在后续的章节中我们会看到ASP.NET Web API的应用程序接口基本上都采用这样的定义方式。对于这个SendAsync方法来说,request参数表示传递给当前 HttpMessageHandler进行处理的请求,这是一个HttpRequestMessage对象。另一个参数 cancellationToken是一个用于发送取消操作信号的CancellationToken对象,如果读者对.NET中的并行编程具有基本了解 的话,相信对这个类型不会感到陌生。针对请求消息和响应消息的处理均体现在这个SendAsync方法上。具体来说,针对请求消息的处理直接实现在SendAsync方法中,而针对响 应消息的处理则通过其返回的Task<HttpResponseMessage>对象来完成。由HttpMessageHandler组成的 消息处理管道以及请求消息和响应消息在管道中的“流动”基本上可以通过右图来体现。

抽象类HttpMessageHandler实现了IDisposable接口,它按照“标准”的方式实现Dispose方法。如下面的代码片断所 示,当我们调用Dispose方法的时候,HttpMessageHandler并没有执行任何资源回收操作。当我们通过继承这个抽象类自定义 HttpMessagHandler的时候,可以将资源回收操作实现在重写的Dispose方法中。

   1: public abstract class HttpMessageHandler : IDisposable
   2: {
   3:     //其他操作
   4:     public void Dispose()
   5:     {
   6:         this.Dispose(true);
   7:         GC.SuppressFinalize(this);
   8:     }
   9:
  10:     protected virtual void Dispose(bool disposing)
  11:     {}
  12: }

二、DelegatingHandler

我们说ASP.NET Web API消息处理管道是通过一组有序的HttpMessagHandler“首尾相连”而成,具体实现“管道串联”是通过DelegatingHandler这个类型来完成的。顾名思义,DelegatingHandler 具有委托功能,当它自己负责的消息处理任务完成之后可以委托另一个HttpMessagHandler进行后续的处理。如果这个被委托的也是一个 DelegatingHandler对象,不就可以组成一个委托链了吗?而这个委托链不就是由一个个DelegatingHandler组成的消息处理管 道吗?

如下面的代码片断所示,DelegatingHandler是一个继承自HttpMessageHandler类的抽象类。上面我们所说的这个被委 托的HttpMessagHandler由它的属性InnerHandler表示。DelegatingHandler重写了定义在其类的抽象方法 SendAsync来调用InnerHandler属性的同名方法。

   1: public abstract class DelegatingHandler : HttpMessageHandler
   2: {
   3:     protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
   4:     public HttpMessageHandler InnerHandler get;  set; }
   5: }

正如上面所说,如果ASP.NET Web API的消息处理管道均由DelegatingHandler组成(位于管道尾端的HttpMessageHandler除外),我们就可以根据其 InnerHandler获得对被委托的HttpMessageHandler对象的引用,由此便构成具有如上图所示的链式结构。组成ASP.NET Web API核心框架的消息处理管道就这么简单。

三、HttpServer

一般来说,对于构成ASP.NET Web API消息处理管道的所有HttpMessageHandler来说,除了处于尾端的那一个之外,其余的均为DelegatingHandler,那么通 过InnerHandler属性维持着“下一个” HttpMessageHandler。作为这个HttpMessageHandler链“龙头”的是一个HttpServer对象,该类型定义在命名空 间“System.Web.Http”下。

如下面的代码片断所示,HttpServer直接继承自DelegatingHandler。它具有两个重要的只读属性 (Configuration和Dispatcher),我们可以通过前者得到用于配置整个消息处理管道的HttpConfiguration对象,另外 一个属性Dispatcher返回的是处于整个消息处理管道“尾端”的HttpMessageHandler。

   1: public class HttpServer : DelegatingHandler
   2: {
   3:     public HttpConfiguration     Configuration { get; }
   4:     public HttpMessageHandler    Dispatcher { get; }
   5:
   6:     public HttpServer();
   7:     public HttpServer(HttpMessageHandler dispatcher);
   8:     public HttpServer(HttpConfiguration configuration);
   9:     public HttpServer(HttpConfiguration configuration, HttpMessageHandler dispatcher);
  10:
  11:     protected override void Dispose(bool disposing);
  12:     protected virtual void Initialize();
  13:     protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
  14: }

HttpServer的Configuration和Dispatcher属性均可以在相应的构造函数中初始化。如果在构造HttpServer的 时候没有显式指定这两个属性的值(调用默认的无参构造函数创建HttpServer),在默认情况下会创建一个HttpConfiguration作为 Configuration的属性值,而作为Dispatcher属性值的则是一个HttpRoutingDispatcher对象,该类型定义在命名空 间“System.Web.Http.Dispatcher”下。除此之外。由于HttpConfiguration类型实现了IDisposable接口,所以HttpServer重写了虚方法Dispose并在该方法中完成了对HttpConfiguration对象的释放。

当HttpServer对象被成功创建之后,对应的消息处理管道的“一头一尾”已经确定下来。一头指的就是HttpServer对象本身,一尾指的 自然就是通过Dispatcher属性引用的HttpMessageHandler对象了。ASP.NET Web API框架最大的扩展性就在于我们可以根据具体的消息处理需求来“定制”这个消息处理管道,它允许我们将自定义的HttpMessageHandler按 照如左图所示的方式“安装”到这一头一尾之间,但是这些处于“中间位置”的HttpMessageHandler是如何注册呢?

既然整个管道都是由HttpConfiguration进行配置,那么自定义HttpMessageHandler的注册自然也可以利用它来完成。 如下面的代码片断所示,HttpConfiguration具有一个只读的集合类型的MessageHandlers,需要注册的 HttpMessageHandler需要添加到此集合之中。由于这是一个元素类型为DelegatingHandler的集合,所以我们自定义的 HttpMessageHandler必须继承自DelegatingHandler。

   1: public class HttpConfiguration : IDisposable
   2: {
   3:     //其他成员    
   4:     public Collection<DelegatingHandler> MessageHandlers { get; }
   5: }

通过上面的给出的HttpServer类型定义我们可以看到它具有一个受保护的Initialize方法,该方法最终完成了对整个消息处理管道的构 建。在重写的SendAsync方法中,如果自身尚未被初始化,该Initialize方法会自动被调用以确保整个消息处理管道已经被成功构建。

四、HttpRoutingDispatcher

在默认情况下,作为消息处理管道“龙头”的HttpServer的Dispatcher属性返回一个HttpRoutingDispatcher对 象,它可以视为这个消息处理管道的最后一个HttpMessageHandler。Web API调用请求一般都是针对定义在某个HttpController中的某个Action方法,所以消息处理管道最终需要激活相应的 HttpController并执行对应的Action方法,HttpRoutingDispatcher完成了目标HttpController的激活 和Action方法的执行。

如下面的代码片断所示,HttpRoutingDispatcher不再是DelegatingHandler的继承者,它的直接基类是抽象类 HttpMessageHandler。我们在构建一个HttpRoutingDispatcher对象的时候需要指定一个 HttpConfiguration对象,而通过参数defaultHandler指定的HttpMessageHandler对于创建的 HttpRoutingDispatcher对象来说具有重要的意义,因为HttpController的激活、Action方法的选择与执行等后续操作 实际上是由它来完成的。

   1: public class HttpRoutingDispatcher : HttpMessageHandler
   2: {
   3:     public HttpRoutingDispatcher(HttpConfiguration configuration);
   4:     public HttpRoutingDispatcher(HttpConfiguration configuration, HttpMessageHandler defaultHandler);
   5:
   6:     protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
   7: }

虽然ASP.NET Web API消息处理管道不具有一个类似于HttpContext的对象来保存基于当前请求的上下文信息,但是表示请求消息的 HttpRequestMessage对象具有一个通过Properties属性表示的属性字典,我们可以利用它来作为上下文数据的存放容器。

通过上面对HttpServer的介绍我们知道它会将当前SynchronizationContext和HttpConfiguration添加 到表示当前请求的HttpRequestMessage对象的属性字典中。与之类似,通过路由系统生成的HttpRouteData也以同样的方式保存在 HttpRequestMessage的属性字典之中,我们可以直接调用HttpRequestMessage的如下两个扩展方法 GetRouteData和SetRouteData进行HttpRouteData的获取和设置。

   1: public static class HttpRequestMessageExtensions
   2: {
   3:     //其他成员
   4:     public static IHttpRouteData GetRouteData(this HttpRequestMessage request);
   5:     public static void SetRouteData(this HttpRequestMessage request, IHttpRouteData routeData);
   6: }

HttpRoutingDispatcher的SendAsync方法被执行时,它会判断作为参数的HttpRequestMessage对象的属 性字典中是否具有这样一个HttpRouteData对象。如果此HttpRouteData对象存在,它会直接将请求交付给创建时指定的 HttpMessageHandler进行处理。这样的情况会发生在Web Host寄宿模式下。

如果封装路由数据的HttpRouteData对象尚未添加到表示被处理请求的HttpRequestMessage对象的属性字典中,意味着针对 请求的路由尚未发生,这种情况会发生在Self Host寄宿模式下。在这种情况下,HttpRoutingDispatcher会直接通过当前HttpConfiguration的Routes属性得 到全局路由表,并将HttpRequestMessage对象作为参数调用其GetRouteData方法以实现针对当前请求的路由解析。

如果执行路由表的GetRouteData方法返回一个具体的HttpRouteData对象,意味着当前请求与注册的某个HttpRoute相匹 配,HttpRoutingDispatcher会将这个HttpRouteData对象添加到HttpRequestMessage对象的属性字典中。 在这之后,ASP.NET Web API会将请求交付给创建时指定的HttpMessageHandler进行后续处理。如果执行GetRouteData方法返回Null,意味着当前请 求与注册的路由规则不匹配,客户端会得到一个状态为“404, Not Found”的响应。

五、HttpControllerDispatcher

我们从类型命名可以看出HttpRoutingDispatcher具有两个基本的职能,即“路由(Routing)”和“消息分发 (Dispatching)”。对于前者,它会调用当前路由表对请求消息实施路由解析进而生成用于封装路由数据的HttpRouteData(如果这样的 HttpRouteData不存在于当前请求的属性字典中)。对于后者,它会将请求直接分发给在创建时指定的HttpMessageHandler来完成 进一步处理。

如果在构建HttpRoutingDispatcher对象的时候没有通过参数defaultHandler显式指定这么一个 HttpMessageHandler对象,默认情况下从它手中接管请求的HttpMessageHandler是一个具有如下定义的 HttpControllerDispatcher的对象,该类型定义在命名空间“System.Web.Http.Dispatcher”下。 HttpControllerDispatcher在整个消息处理管道中显得尤为重要,因为目标HttpController的激活、Action方法的 执行和响应生成均是由HttpControllerDispatcher来完成的。

   1: public class HttpControllerDispatcher : HttpMessageHandler
   2: {
   3:     public HttpControllerDispatcher(HttpConfiguration configuration);
   4:     protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
   5:
   6:     public HttpConfiguration Configuration { get; }
   7: }

在我们引入HttpControllerDispatcher对象之后,ASP.NET Web API的消息处理管道将具有如右图所示的结构。从这个结构来看,貌似HttpControllerDispatcher才是整个消息处理管道的最后一个 HttpMessageHandler。这种说法没有错,但我个人还是倾向于将HttpControllerDispatcher视为“隶属于” HttpRoutingDispatcher 的“内部”HttpMessageHandler,所以仍将这个“包含” HttpControllerDispatcher的HttpRoutingDispatcher视为组成消息处理管道的最后一个 HttpMessageHandler。

除此之外,“N个DelegagingHandler + 1个HttpMessageHander”这样的链式结构也刚好与基于DelegagingHandler的委托链相匹配。对于读者朋友来说,具体倾向于 哪种说法并不重要,重要的是能够深刻了解整个消息处理管道的是如何构成的。

[转载]八一八android开发规范(一种建议) - laozhu1124 - 博客园

mikel阅读(879)

[转载]八一八android开发规范(一种建议) – laozhu1124 – 博客园.

开发规范重不重要了,不言而喻。这里就给大家说一故事把——据《圣经·旧约·创世记》第11章记载,是当时人类联合起来兴建,希望能通往天堂的高塔。为了阻止人类的计划,上帝让人类说不同的语言,使人类相互之间不能沟通,计划因此失败,人类自此各散东西。此故事试图为世上出现不同语言和种族提供解释。好的开发团队应该是同样的语言,同样的理念,同样的规范去做同一个事情。因此,这里说的开发规范不仅仅在Android开发界重要,而且在所有的程序界的重要性也是不言而喻的。

这里说的开发规范分成目录规范,项目和报名的命名规范,类,方法,变量和常量的命名规范这几种。

1.目录规范

目录规范——你在开发中整体文件夹组织结构。 我这里推荐一种文件夹的组织结构。

Requirement——需求文档文件夹

Design——设计文档文件夹

Test——集成测试,系统测试,测试报告,测试清单文件夹

Deployment——发布部署的文件夹

Study——预研,学习资料的文件夹

Src——源码文件夹

Help——帮助文档文件夹

这么组织文件有什么好处,就是你一个项目做完以后,您的所有的资料就也完成了。 就像这样:

这样的规范,结构一目了然。

在介绍命名规范之前,先介绍一下常见的命名方法。

  1. 匈牙利命名法。该命名法是在每个变量名的前面加上若干表示数据类型的字符。基本原则是:变量名=属性+类型+对象描述。如i表示int,所有i开头的变量命都表示int类型。s表示String,所有变量命以s开头的都表示String类型变量。
  2. 骆驼命名法。正如它的名称所表示的那样,是指混合使用大小写字母来构成变量和函数的名字。驼峰命名法跟帕斯卡命名法相似,只是首字母为小写,如userName。因为看上去像驼峰,因此而得名。
  3. 帕斯卡命名法 即pascal命名法。做法是首字母大写,如UserName,常用在类的变量命名中。
  4. 下划线命名法,下划线法是随着C语言的出现流行起来的,在UNIX/LIUNX这样的环境,以及GNU代码中使用非常普遍。

2.项目和报名命名规范

对于项目和包名命名规范是

包名一律小写少用缩写和长名;

采用以下规则:

[基本包].[项目名].[模块名]

包名一般不要超过三级,级别多了费脑子

不得将类直接定义在基本包下,所有项目中的类、接口等都应当定义在各自的项目和模块包中;

例如:

package com.routemap _information.util;

这样子的规范,能够提高项目组织性,从而便于更好的协同开发。

 3.类,接口,方法,常量和变量命名规则

Ⅰ类和接口的命名

类或接口名是个一名词,采用大小写混合的方式,每个单词的首字母大写。尽量使你的类名简洁而富于描述。使用完整单词,避免用缩写词(除非该缩写词被更广泛使用,像URLHTML)

例如:

class Raster;

class ImageSprite;

interface RasterDelegate;

interface Storing;

命名采用单词组合取名,单词首字母为大写,单词之间可采用“_”下划线进行区分,也可不采用。

 根据定义类型首字母加以区分:

Interface:命名首字母加大写的“I;

Abstract class:命名首字母加大写“A;

Class:无需加

 根据功能类型结尾加上功能描述字符串:

页面类:“Page”,例如“LoginPage

处理类:“Handle”,例如“LogicHandle

工厂实现类:“Impl”,例如“FactoryImpl”

动作事件定义类:“Action”,例如“LoginAction

网络事件定义类:“Net”,例如“LoginNet

数据定义类:“Data”,例如“LoginData

消息处理类:“Msg”,例如“LoginRequestMsg

资源管理类:“Manager”,例如“ImageManager

缓存类:“Cache”,例如“UserCache

参数传递类:“Param”,例如“LoginParam

功能提供类:“Util”,例如“MathUtil

数据输入输出类:“Steam”,例如“CacheOutStream

 类命名不能使用中文字符,不能在命名字符串中出现“0-9”的数值描述和除下划线以外的其他字符描述,命名的字母组合尽量能够在本身的文字意义上初步了解类的大体功能。

好的类的命名是,不见注释见名知意。

采用大小写混合的方式,第一个单词的首字母小写,其后单词的首字母大写;

Ⅱ变量命名方法

变量名不应以下划线或美元符号开头;

尽量避免单个字符的变量名,除非是一次性的临时变量。临时变量通常被取名为ijkmn,它们一般用于整型;cde,它们一般用于字符型

不建议采用匈牙利命名法则,对不易清楚识别出该变量类型的变量应使用类型名或类型名缩写作其后缀,例如:

Thread animationThread;

String responseStr;

组件或部件变量使用其类型名或类型名缩写作其后缀,例如:

Command backCommand;

Image barImage

TextField passwordField;

Player dogSoundPlayer;

集合类型变量,例如数组和矢量,应采用复数命名或使用表示该集合的名词做后缀:

Image[] images;

Vector requestQueue;

Ⅲ常量命名

全部采用大写,单词间用下划线隔开:

static final int MIN_WIDTH = 4; 

static final int MAX_WIDTH = 999; 

static final int GET_THE_CPU = 1;

方法命名

方法名是一个动词,采用大小写混合的方式,第一个单词的首字母小写,其后单词的首字母大写;取值类可使用get前缀,设值类可使用set前缀,判断类可使用is(has)前缀。

getName();

setSarry();

isLogon();

能够遵循这样的命名规范,相信你的开发会规范许多。

对于方法中一定要加上适当的非空判断,与try catch 语句等等程序健壮性的判断。

最后说一下注释,注释,是程序维护的灵魂。对于注释我给出这样的建议:

原则——对已经不推荐使用的类和方法需要注明@Deprecated,并说明替代的类或者方法;对于针对集合、开关的方法,要在方法注释中表明是否多线程安全。

 Ⅰ文件注释

所有的源文件都应该在开头有一个注释,其中列出文件的版权声明、文件名、功能描述以及创建、修改记录:

/*

 * Copyright (C) 2009-2014 ROUTEMAP Inc.All Rights Reserved.

 * 

 * FileNameKQQMIDlet.java

 *

 * Description:简要描述本文件的内容

 * 

 * History

版本号 作者 日期       简要介绍相关操作

*  1.0   Iron 2009-01-21  Create

 *  1.1   Iron 2009-01-30  Add Logo Screen

**/

Ⅱ类或接口注释

采用JavaDoc文档注释,在类、接口定义之前应当对其进行注释,包括类、接口的描述、最新修改者、版本号、参考链接等:

/** 

 * 描述

 * @author 作者(最新修改者)

 * @version 版本号(最新版本号)

 * @see 参考的JavaDoc

 */

class Window extends BaseWindow 

{

   …

}

注:JavaDoc文档注释:描述Java的类、接口、构造方法、方法、以及字段。每个文档注释都会被置于注释定界符/**…*/之中,一个注释对应一个类、接口或成员。该注释应位于声明之前。文档注释的第一行(/**)不需缩进,随后的文档注释每行都缩进1(使星号纵向对齐)

Ⅱ方法注释

采用JavaDoc文档注释,在方法定义之前当对其进行注释,包括方法的描述、输入、输出及返回值说明、抛出异常说明、参考链接等:

  /**

     * 描述

     * @param 参数说明:每个参数一行,注明其取值范围等

     * @return 返回值:注释出失败、错误、异常时的返回情况

     * @exception 异常:注释出什么条件下会引发什么样的异常

     * @see 参考的JavaDoc

     */

public char charAt(int index)

{

       …

    }

Ⅲ其它注释(JavaDoc文档注释)

1单行代码注释一律使用注释界定符”//”,例如:

// explain what this means

if(bar > 1)

{

……

}

 

int isShow = 0;// 是否显示

2.多行注释使用注释界定符“/*…*/”,例如:

/*

     * Here is a block comment with 

     * multiple lines for text comments.

     */

这些命名规范和注释,看似是微不足道一小步,却是我们通往专业程序员的一大步。

[转载]Asp.Net MVC 上传图片到数据库 - stoneniqiu - 博客园

mikel阅读(1510)

[转载]【读书笔记】Asp.Net MVC 上传图片到数据库(会的绕行) – stoneniqiu – 博客园.

之前上传图片的做法都是上传到服务器上的文件夹中,再将url保存到数据库。其实在MVC中将图片上传到数据库很便捷的事情,而且不用去存url了。而且 这种方式支持ie6(ie6不支持JQuery自动提交form,认为其不安全,这里form是主动点击提交的,所以就没有这个问题,而 uploadify自动提交是flash的方式)。

一、建立模型

      场景是假设我们需要给一个Product编辑一张图片。在模型中,先定义好两个属性,ImageData和ImageType

public class Product {

[HiddenInput(DisplayValue=false)]
public int ProductID { get; set; }

[Required(ErrorMessage = "Please enter a product name")]
public string Name { get; set; }

[Required(ErrorMessage = "Please enter a description")]
[DataType(DataType.MultilineText)]// 在前台会渲染成Textarea
public string Description { get; set; }

[Required]
[Range(0.01, double.MaxValue, ErrorMessage = "Please enter a positive price")]
public decimal Price { get; set; }

[Required(ErrorMessage = "Please specify a category")]
public string Category { get; set; }
public byte[] ImageData { get; set; }
[HiddenInput(DisplayValue = false)]//会让改属性在编辑的时候不显示出来。
public string ImageType{ get; set; }
}

二、存取方法

那在我们的控制器中,这样定义Edit方法。MVC强大的模型绑定机制会自动的将前台form中的数据根据name转换成我们需要的C#对象。当然后台代码这里只是简单的实现,文件大小和类型的判断先略过。

[HttpPost] //保存
public ActionResult Edit(Product product, HttpPostedFileBase image) {

if (ModelState.IsValid) {
if (image != null) {
product.ImageType = image.ContentType;//获取图片类型
product.ImageData = new byte[image.ContentLength];//新建一个长度等于图片大小的二进制地址
image.InputStream.Read(product.ImageData, 0, image.ContentLength);//将image读取到ImageData中
}
// save the product
repository.UpdateProduct(product);//更新一下 保存模型。
// add a message to the viewbag
TempData["message"] = string.Format("{0} has been saved", product.Name);
// return the user to the list
return RedirectToAction("Index");
} else {
// there is something wrong with the data values
return View(product);
}
}
HttpPostedFileBase.inputStream 获取一个Stream 对象,该对象指向一个上载文件,以准备读取该文件的内容。
 

然后通过ID,将二进制转化为图片。

public FileContentResult GetImage(int productId) {
Product prod = repository.Products.FirstOrDefault(p =&gt; p.ProductID == productId);
if (prod != null) {
return File(prod.ImageData, prod.ImageMimeType);//File方法直接将二进制转化为指定类型了。
} else {
return null;
}
}

FileContentResult 最适合将二进制数据转换成文件,同类型还有FileStreamResult,FilePathResult,这三个都是继承与FileResult。

更详细的解释大家可以参考Artech大神的博客  :了解ASP.Net MVC几种ActionResult的本质

三、前台代码

@using (Html.BeginForm("Edit", "Admin", FormMethod.Post,
new { enctype = "multipart/form-data" })) {

@Html.EditorForModel()
<div class="editor-label">Image</div>
<div class="editor-field">
<div>Upload new image: <input type="file" name="Image" /></div>
</div>
<input type="submit" value="Save" />
@Html.ActionLink("Cancel and return to List", "Index")
}
 @Html.EditorForModel() 会根据我们的模型自动渲染出编辑界面,比如是bool型,就会生成checkbox,而不用一条一条去写。不是太复杂的模型可以用这种较为快捷的方式

自动渲染的效果如下(勿喷,未处理样式)

那再获取图片的时候,就可以通过调用GetImage方法来获取。 和获取验证码的方式一样。 这种方式比之前上传写一堆脚本要好很多。

<div class="item">@if (Model.ImageData != null) {
<div style="float: left; margin-right: 20px;"><img alt="" src="@Url.Action(" width="75" height="75" /></div>
}
.......
<h4>@Model.Price.ToString("c")</h4>
</div>

希望这次分享对你有帮助~

源代码参考SportShop

参考书籍:Pro ASP.NET MVC3 Framwork

[转载]依赖 IDE 做开发,会让你成为一名糟糕的程序员么? - SegmentFault

mikel阅读(988)

[转载]依赖 IDE 做开发,会让你成为一名糟糕的程序员么? – SegmentFault.

使用Notepad++编程比使用Visual Studio让你感觉更好?

请输入图片描述

那晚我正读着Rob Conery写的一篇文章,其中谈到了他对职业生涯的选择,对编程语言的选择以及这过程中留有的一些遗憾。很多地方让我觉得吃惊,但其中提到的关 于.net开发的一点引起了我的深思。他发现的不好之处是:比起PHP开发人员,.Net开发人员会变得越来越依赖IDE。

作为一名.NET开发人员,我已经习惯了周围各种关于IDE依赖现象的说法,也习惯了各种PHP、JavaScript或Ruby开发者对微软产品 Visual Studio的抨击。通常来说,基于C语言的开发者们在这个话题上更有发言权,这就像使用编程语言和开发工具更多的人会更了解程序的运行过程一样。

事实上,一款优秀的IDE会让你的工作更有效率:优质的框架加快了项目进度,自动编译和IDE重构工具让编码进行得更快,集成单元测试让你的应用程 序易于维护,部署工具、web服务器、代码分析器或编译时绑定等简化了项目工作流程,除此之外,它还标准化了程序员的开发经验,这既有利于开发者本身也有 利于项目的进行。团队内的开发者们使用相同的工具一起工作效率更高,而企业也受益于老员工与新员工之间的代沟会变小。

以上所说的优点也只是冰山一角而已,并且在所有可用的IDE中,Visual Studio可以说是最好的,它不仅功能齐全,而且连发布版本也都是免费的(尽管缺少一些中看不中用的组件)。可能你不会被视为那种能在Emacs上用 bash写纸牌的“精英”,但是我认为很多人讨厌IDE的原因是因为他们所用的IDE不适合他们所用的编程语言。其中大部分都是脚本语言,因为一个IDE 很难为一种解释型语言做出点贡献。不能因为一个开发者更喜欢使用notepad++来写PHP而否认他们能从IDE中获益,相反,这反而是一种固执的表 现。

依赖IDE不好的第二大原因是,它会让你对编程语言本身越来越模糊。如果你不需要记住语法规则,也不需要记住那些要调用的方法,因为IDE总会智能 的提醒你,久而久之,你就会很难在没有IDE的情况下写好一个程序。但在我看来,对于一个优秀的开发者,最重要的不是去记住那些语法细节,而是知道解决问 题的有效方法并能有效利用最好的技术或者工具来解决它。使用IDE能够让你将更多的精力投入到真正重要的问题上,也就是解决方案,而不是纠结于哈希表逆序 的函数是哪一个这种问题。

而Rob Conery认为过于依赖IDE不好的原因在于,他的主要工作是在windows平台下使用Visual Studio中的拖放控件来开发web表单。这从某种意义上来说确实是不好的,但同时我也对他竟然花费如此多的时间那样使用.NET感到惊讶。我已经有将 近8年没有使用过VS系列的可视化开发工具了,但是我仍然认为没有任何IDE能够比得上它。

我曾经多年见到Xcode就发慌,Eclipse很适合用来做Java开发可是我不喜欢用它来开发PHP,刚用Aptana的时候会觉得不错但是过 不了多久就会让你有在使用Notepad++的错觉,诸如此类的IDE还有很多。而由于JetBrains公司(IntelliJ IDEA集成开发环境所属公司)产品的缺乏,使得它远不能与Visual Studio相提并论。

当然这仅仅是我个人的想法。你认为依赖IDE对你的编程能力或者职业生涯有没有影响呢?你觉得如果一个团队的开发者都使用Sublime Text或者Notepad++会和使用IDE一样高效吗?

部分微博评论

@xxiuu :写代码是个手艺活,拿着钉子锤子,锯子墨线,一块木头,半天功夫,一把仿古的凳子就出来了,在来个雕花,上个老漆,真是个绝活。在看看那边,电闸一啦,车床呼啦呼啦的,这边几块木料下去,那边一对零件出来了,打个包装加个说明书,一套原木环保宜家家具就出来了。

@扬仔360: 老么前儿学J2EE那会儿,一老师说我太过于依赖Eclipse,用多了代码补全,问:“单单让你用TXT,能写出来吗?”其实当时J2EE勉勉强强还能 用txt写出来,现在用XCode用多了,要说用txt写O-B,我是死活写不出来。现在我才明白很多事情,抨击IDE的便捷性就是抨击咱人类的发展过 程,不定个反人类罪都是轻的。


原文 Does relying on an IDE for development make you a bad programmer?
翻译 伯乐在线Licorice

[转载]PHP遍历数组的几种方法_php技巧_脚本之家

mikel阅读(877)

[转载]PHP遍历数组的几种方法_php技巧_脚本之家.

PHP中遍历数组有三种常用的方法:
一、使用for语句循环遍历数组;
二、使用foreach语句遍历数组;
三、联合使用list()、each()和while循环遍历数组。
这三种方法中效率最高的是使用foreach语句遍历数组。从PHP4开始就引入了foreach结构,是PHP中专门为遍历数组而设计的语句,推荐大家使用。先分别介绍这几种方法。

一、使用for语句循环遍历数组
值得大家注意的是使用for语句循环遍历数组要求遍历的数组必须是索引数组。PHP中不仅有关联数组而且还有索引数组,所以PHP中很少用for语句循环遍历数组。
实例代码如下:

复制代码 代码如下:
<?php
$arr = array(‘http://www.jb51.net’,’脚本之家’,’PHP教程’);
$num = count($arr);
for($i=0;$i<$num;++$i){
echo $arr[$i].'<br />’;
}
?>

注 释:上例代码中我们先计算出数组$arr中元素的个数,然后才使用在for语句,这样做很高效的。因为如果是for($i=0;$i< count($arr);++$i)的话,每次循环都会计算数组$arr中元素的个数,而使用上面的方式可以减去这种开销。使用++$i也是为了提高效 率,前面有文章我们提到过,建议大家再看看。
上面代码的输出结果为:
http://www.jb51.net
脚本之家
PHP教程

二、使用foreach语句遍历数组
使用foreach语句循环遍历数组有二种方式,我们使用的最多的还是第一种方式。介绍如下:
第一种方式:
foreach(array_expression as $value){
//循环体
}
实例代码:

复制代码 代码如下:
<?php
$arr = array(‘http://www.jb51.net’,’脚本之家’,’PHP教程’);
foreach($arr as $value){
echo $value.'<br />’;
}
?>

每次循环中,当前元素的值被赋给变量$value,并且把数组内部的指针向后移动一步。所以下一次循环中会得到数组的下一个元素,直到数组的结尾才停止循环,结束数组的遍历。

第二种方式:
foreach(array_expression as $key=>$value){
//循环体
}
实例代码:

复制代码 代码如下:
<?php
//定义数组
$arr = array(‘http://www.jb51.net’,’脚本之家’,’PHP教程’);
foreach($arr as $k=>$v){
echo $k.”=>”.$v.”<br />”;
}
?>

三、联合使用list()、each()和while循环遍历数组
each()函数需要传递一个数组作为一个参数,返回数组中当前元素的键/值对,并向后移动数组指针到下一个元素的位置。
list()函数,这不是一个真正的函数,是PHP的一个语言结构。list()用一步操作给一组变量进行赋值。

实例代码:

复制代码 代码如下:
<?php
//定义循环的数组
$arr = array(‘website’=>’http://www.jb51.net’,’webname’=>’脚本之家’)
while(list($k,$v) = each($arr)){
echo $k.’=>’.$v.'<br />’;
}
?> jb51.net

输出结果为:
website=>http://www.jb51.net
webname=>PHP程序员

总结:上述三种循环遍历数组的方法中建议大家使用foreach语句循环遍历数组,效率更高。

[转载]解析移动游戏运营数据分析指标 - kobeshow - 博客园

mikel阅读(943)

[转载]解析移动游戏运营数据分析指标 – kobeshow – 博客园.

       在平常的工作中,经常会有一些马虎的数据分析师,接到业务方提到的需求后,大致扫一遍然后就吭叽吭叽做起来,最后出的分析结果报告交给业务方后没多久就 打回来,说不是他想要结果,仔细一讨论发现双方的指标定义不一致,从而导致了一顿白忙活,造成“十动仍拒”的下场。所以指标定义的清晰性是开始分析工作的 前提。 OK,本文要讲述的也是有关指标定义,是由talking data推出的移动游戏运营数据分析指标白皮书,目的是统一移动游戏数据指标的定义,主要涉及宏观层面的定义,下面分模块来看
 
一,用户获取
日新登用户数(daily new users)
定义:每日注册并登陆游戏的用户数
解决的问题:1)渠道贡献新用户份额 (新用户的来源渠道分析); 2) 宏观走势,是否需要进行投放(产品用户量的走势,更好分配资源);3) 是否存在渠道作弊行为;
其他的:周/月新登陆用户数,细分自然增长用户跟推广用户(感觉还可以根据其他的一些维度如地域,手机型号等结合分析)
 
日一次会话用户数(daily one session users)
定义:一次会话用户,即锌等用户中只有一次会话,且会话时长低于规定阀值(场景:1) 产品不感兴趣用户;2) 无限刷号用户 3) 推广渠道质量)
解决的问题:1)渠道渠道是否刷量作弊; 2) 渠道推广质量是否合格(推广质量参考的一个维度);3) 用户导入是否存在障碍点,如网络状况,加载时间;
其他的:周/月一次会话用户数,游戏引导分析点之一,有助于分析新登用户质量,进一步分析则需要定义活跃用户的月一次会话用户数
 
用户获取成本(customer acquisition cost)
定义:推广成本/有效新登录用户(具体怎么定义有效新登录用户,需要看场景?)
解决的问题:1)获取有效新登录用户的成本; 2) 优化推广渠道
其他的:把用户获取的成本细分到渠道里面,也就是说每个渠道获取有效新登录用户的成本
 
二,用户活跃
日活跃用户数(daily active users)
定义:每日登陆游戏的用户数
解 决的问题:1) 游戏的核心用户规模; 2) 游戏产品周期变化趋势衡量(端游有端游的生命周期,手游有手游的生命周期,同一类型里面的不用主题的游戏又有不同的生命周期);3) 游戏产品新老用户分析; 4) 渠道活跃用户生存周期(类似营销渠道跟常规渠道的区别);5) 游戏产品的粘性如何(需要与MAU结合,更细的可以看游戏时长等维度)
其他的:周/月活跃用户数,指的注意的是月活跃用户数能反映用户规模的稳定性(天活跃极度容易受活动效应/节日效应影响),进而观察不同游戏产品间的对比,同一游戏产品版本迭代的情况,当然这三个指标都是要用户去重的
 
日参与次数(daily engagement count)
定义:用户对移动游戏的使用记为一次参与,日参与次数,就是一天的总和(这相当于游戏登陆次数?)
解决的问题:1) 衡量用户的粘性; 2) 什么渠道,什么用户参与频率较高;3) 用户对产品参与频率的衡量
其他的:周/月参与次数 ,日平均参与次数(日参与次数/日参与用户数,跟arpu值计算逻辑一样),一般建议30内重复开启记录为一次完整使用,不单独计量(类似页游里面的时长心跳上报)
 
日均使用时长(daily avg  online_time)
定义:活跃用户平均每日在线时长=日总在线时长/日活跃用户数
解决的问题:1) 用户参与怎样; 2) 渠道优化的参考维度;3) 用户质量参考维度;4)用户持续游戏的能力(特别对新用户来说)
其他的:周/月均时长 ,当然也可以使用用户单次平均使用时长
 
用户活跃度(DAU、MAU)
解决的问题:1) 用户游戏参与怎样; 2) 游戏质量人气等;3) 用户活跃天数如何
其他的:DAU/MAU的理论值不低于0.2,也就是一个月登陆次数不低于6次
三,留存跟流失
通常的术语:新登用户的次日、3日、7日、月留存率
解决的问题:1) 每个渠道好坏的参考维度; 2) 用户对游戏的粘性;3) 用户生命周期;
其他:留存率一定意义代表游戏的质量,关注用户流失的节点,根据流失率改进产品,优化游戏的设计
 
用户的流失
关于这个话题需要确定2个定义,用户最近的活跃定义,跟未来的流失定义,例如用户最近7天是活跃的,未来7天不登陆,我就算流失等等,这个是做游戏预警模型的重中之重
 
四,充值
对于移动游戏创造营收有三种:1)付费下载  2)应用内广告  3)应用内付费,这里主要关注第三种
 
月付费率(monthly payment ratio)
定义:统计区间内,付费用户占活跃用户的比例
解决的问题:1) 游戏产品的付费引导是否合理; 2) 用户是否有充值意愿;
活跃付费用户(active payment account)
解决的问题:1) 游戏付费用户规模如何; 2) 付费转化率如何;3)游戏充值人群构成(大R,中R,小R比例)
 
平均每用户收入(ARPU)
定义:总收入/活跃人数
解决的问题:1) 游戏收益如何; 2) 渠道价值如何;
 
生命周期(life time)
定义:用户从第一次参与游戏,到最后一次参与游戏的时间差,
解决的问题:1) 游戏收益如何; 2) 渠道价值如何;
最后的最高同时在线人数(PCU),平均同时在线人数(ACU),新用户转化率(点击-安装-注册-登陆-付费-流失)
K因子 =感染率*转化率,感染率(每个用户发出的邀请数量),转化率(转为用户的比率)
k>1 游戏用户增长快,<1 增长慢,达到瓶颈
 
数据报表
 
日报表
1)监控重点数据,是否存在异常;2) 了解渠道数据,实时了解渠道表现
关注指标
用户层面:日活,新用户注册/登陆,次日留存,在线时长,新老用户付费人数/金额,新老用户价值
游戏层面:日活,新用户注册/登陆,次日留存,在线时长,新老用户付费人数/金额,新老用户价值
渠道层面:每日充值总量,新老用户占比,次日留存情况,渠道价值
 
周报表
1)了解周期性变化,制定策略;2) 重点渠道、用户跟踪、分析转化;3)周期行数据分析总结,制定下一个周期计划
关注指标
用户层面:日报表里面的周汇总数据,然后加环比、同比
游戏层面:同上
渠道层面:同上
 
总结:上面的报表是一个参考,具体的当然需要结合业务场景来进行增添删减
白皮书的最后介绍了下他们的AARRR模型简称(2A3R吧,呵呵),具体的就是获取用户(acquisition)、提高活跃度(activation)、提高留存率(retention)、获取收入(revenue)、自传播(refer)

[转载]在一个空ASP.NET Web项目上创建一个ASP.NET Web API 2.0应用 - Artech - 博客园

mikel阅读(1080)

[转载]在一个空ASP.NET Web项目上创建一个ASP.NET Web API 2.0应用 – Artech – 博客园.

由于ASP.NET Web API具有与ASP.NET MVC类似的编程方式,再加上目前市面上专门介绍ASP.NET Web API 的书籍少之又少(我们看到的相关内容往往是某本介绍ASP.NET MVC的书籍“额外奉送”的),以至于很多人会觉得ASP.NET Web API仅仅是ASP.NET MVC的一个小小的扩展而已,自身并没有太多“大书特书”的地方。而真实的情况下是:ASP.NET Web API不仅仅具有一个完全独立的消息处理管道,而且这个管道比为ASP.NET MVC设计的管道更为复杂,功能也更为强大。虽然被命名为“ASP.NET Web API”,但是这个消息处理管道却是独立于ASP.NET平台的,这也是为什么ASP.NET Web API支持多种寄宿方式的根源所在。

为 了让读者朋友们先对ASP.NET Web API具有一个感性认识,接下来我们以实例演示的形式创建一个简单的ASP.NET Web API应用。这是一个用于实现“联系人管理”的单页Web应用,我们以Ajax的形式调用Web API实现针对联系人的CRUD操作。[源代码从这里下载]

目录
构建解决方案
定义Web API
以Web Host方式寄宿Web API
以Self Host方式寄宿Web API
利用HttpClient调用Web API
创建一个“联系人管理器”应用

一、构建解决方案

Visual Studio为我们提供了专门用于创建ASP.NET Web API应用的项目模板,借助于此项目模板提供的向导,我们可以“一键式”创建一个完整的ASP.NET Web API项目。在项目创建过程中,Visual Studio会自动为我们添加必要的程序集引用和配置,甚至会为我们自动生成相关的代码,总之一句话:这种通过向导生成的项目在被创建之后其本身就是一个 可执行的应用。

对于IDE提供的这种旨在提高生产效率的自动化机制,我个人自然是推崇的,但是我更推荐读者朋友们去了解一下这些自动化机制具体为我们做了什么?做这些的目的何在?哪些是必需的,哪些又是不必要的?正是基于这样的目的,在接下来演示的实例中,我们将摒弃Visual Studio为我们提供的向导,完全在创建的空项目中编写我们的程序。这些空项目体现在如右图所示的解决方案结构中。

如右图所示,整个解决方案一共包含6个项目,上面介绍的作为“联系人管理器”的单页Web应用对应着项目WebApp,下面的列表给出了包括它在内的所有项目的类型和扮演的角色。

  • ·Common:这是一个空的类库项目,仅仅定义了表示联系人的数据类型而已。之所以将数据类型定义在独立的项目中,只要是考虑到它会被多个项目(WebApi和ConsoleApp)所使用。
  • WebApi:这是一个空的类库项目,表现为HttpController类型的Web API就定义在此项目中,它具有对Common的项目引用。
  • WebHost:这是一个空的ASP.NET Web应用,它实现了针对ASP.NET Web API的Web Host寄宿,该项目具有针对WebApi的项目引用。
  • SelfHost:这是一个空的控制台应用,旨在模拟ASP.NET Web API的Self Host寄宿模式,它同样具有针对WebApi的项目引用。
  • WebApp:这是一个空的ASP.NET Web应用,代表“联系人管理器”的网页就存在于该项目之中,至于具体的联系人管理功能,自然通过以Ajax的形式调用Web API来完成。
  • ConsoleApp:这是一个空的控制台应用,我们用它来模拟如何利用客户端代理来实现对Web API的远程调用,它具有针对Common的项目引用。

二、定义Web API

在正式定义Web API之前,我们需要在项目Common中定义代表联系人的数据类型Contact。简单起见,我们仅仅为Contact定义了如下几个简单的属性,它们分别代表联系人的ID、姓名、联系电话、电子邮箱和联系地址。

   1: public class Contact
   2: {
   3:     public string Id { get; set; }
   4:     public string Name { get; set; }
   5:     public string PhoneNo { get; set; }
   6:     public string EmailAddress { get; set; }
   7:     public string Address { get; set; }
   8: }

表现为HttpController的Web API定义在WebApi项目之中,我们一般将ApiController作为继承的基类。ApiController定义在 “System.Web.Http.dll”程序集中,我们可以在目录“%ProgramFiles%\Microsoft ASP.NET\ASP.NET Web Stack 5\Packages\”中找到这个程序集。具体来说,该程序集存在于子目录“Microsoft.AspNet.WebApi.Core.5.0.0 \lib\net45”中。

Web API体现在如下所示的ContactsController类型中。在该类型中,我们定义了Get、Post、Put和Delete这4个Action 方法,它们分别实现了针对联系人的查询、添加、修改和删除操作。Action方法Get具有一个表示联系人ID的可缺省参数,如果该参数存在则返回对应的 联系人,否则返回整个联系人列表。由于ASP.NET Web API默认实现了Action方法与HTTP方法的映射,所以方法名也体现了它们各自所能处理请求必须采用的HTTP方法。

   1: public class ContactsController: ApiController
   2: {
   3:     static List<Contact> contacts;
   4: static int counter = 2;
   5:
   6:     static ContactsController()
   7:     {
   8:         contacts = new List<Contact>();
   9:         contacts.Add(new Contact { Id = "001", Name = "张三",
  10:             PhoneNo = "0512-12345678", EmailAddress = "zhangsan@gmail.com",
  11:             Address = "江苏省苏州市星湖街328号" });
  12:         contacts.Add(new Contact { Id = "002", Name = "李四",
  13:             PhoneNo = "0512-23456789", EmailAddress = "lisi@gmail.com",
  14:             Address = "江苏省苏州市金鸡湖大道328号" });
  15: }
  16:
  17:     public IEnumerable<Contact> Get(string id = null)
  18:     {
  19:         return from contact in contacts
  20:             where contact.Id == id || string.IsNullOrEmpty(id)
  21:             select contact;
  22: }
  23:
  24:     public void Post(Contact contact)
  25:     {
  26:         Interlocked.Increment(ref counter);
  27:         contact.Id = counter.ToString("D3");
  28:         contacts.Add(contact);
  29:     }
  30:
  31:     public void Put(Contact contact)
  32:     {
  33:         contacts.Remove(contacts.First(c => c.Id == contact.Id));
  34:         contacts.Add(contact);
  35:     }
  36:
  37:     public void Delete(string id)
  38:     {
  39:         contacts.Remove(contacts.First(c => c.Id == id));
  40:     }
  41: }

简单起见,我们利用一个静态字段(contacts)表示存储的联系人列表。当ContactsController类型被加载的时候,我们添加了 两个ID分别为“001”和“002”的联系人记录。至于实现联系人CRUD操作的Action方法,我们也省略了必要的验证,对于本书后续的演示的实 例,我们基本上也会采用这种“简写”的风格。

三、以Web Host方式寄宿Web API

我们在上面已经提到过了,虽然被命名为ASP.NET Web API,但是其核心的消息处理管道却是独立于ASP.NET平台的,所以我们可以对相同的Web API实施不同的寄宿方式。寄宿的本质就是利用一个具体的应用程序为Web API提供一个运行的环境,并最终解决“请求的接收和响应的回复”问题。作为寄宿的一种主要形式,Web Host就是创建一个ASP.NET Web应用作为Web API的宿主。

采用Web Host方式寄宿Web API的宿主程序WebHost是一个空的ASP.NET应用。除了让它引用定义ContactsController的WebApi项目之外,我们还需 要为其添加如下这些必需的程序集引用。除了程序集“System.Net.Http.dll”(它属于.NET Framework 原生的程序集)之外,其余3个均可以在目录“%ProgramFiles%\Microsoft ASP.NET\ASP.NET Web Stack 5\Packages\”中找到。

  • System.Web.Http.dll(\ Microsoft.AspNet.WebApi.Core.5.0.0\lib\net45\)
  • System.Net.Formatting.Http.dll(\Microsoft.AspNet.WebApi.Client.5.0.0\lib\net45\)
  • System.Web.Http.WebHost.dll(\Microsoft.AspNet.WebApi.WebHost.5.0.0\lib\net45\)
  • System.Net.Http.dll

与ASP.NET MVC一样,如果采用Web Host的方式来寄宿Web API,ASP.NET自身的路由系统会成为接收请求的第一道屏障。在将请求递交给ASP.NET Web API自己的消息处理管道之前,路由系统会解析出当前请求访问的目标HttpController和Action的名称。我们需要做的就是根据需求注册相 应的路由,这也是采用Web Host寄宿方式所需的唯一操作。

我们在WebHost项目中添加一个Global.asax文件,并按照如下的形式在其Application_Start方法中注册了一个模板为 “api/{controller}/{id}”的路由。此模板由3部分组成,静态文本“api”表示其前缀,后面是两个路由参数。前者 ({controller})表示目标HttpController的名称,后者({id})可以映射为目标Action方法的同名参数(比如 ContractsController的Get方法的参数id),这是一个可以缺省的路由参数(RouteParameter.Optional)。

   1: public class Global : System.Web.HttpApplication
   2: {
   3:     protected void Application_Start(object sender, EventArgs e)
   4:     {
   5:         GlobalConfiguration.Configuration.Routes.MapHttpRoute(
   6:             Name            : "DefaultApi",
   7:             routeTemplate   : "api/{controller}/{id}",
   8:             defaults        : new { id = RouteParameter.Optional });
   9:     }
  10: }

如上面的代码片断所示,路由注册是通过调用代表全局路由表的HttpRouteCollection对象的扩展方法MapHttpRoute来完成 的。GlobalConfiguration的静态属性Configuration返回一个代表当前配置的HttpConfiguration对象,全局 路由表就注册在它的Routes属性上。

如果你了解ASP.NET MVC的路由注册,可能觉得奇怪:注册路由的模板中并没有表示目标Action的路由参数,ASP .NET Web API如何根据请求确定哪个Action方法应该被调用呢?答案其实很简单:它能根据请求采用HTTP方法来确定目标Action方法。当然,在注册路由模板中提供代表Action名称的路由参数({action})也是支持的。

在默认情况下,通过Visual Studio(VS 2012或者VS 2013,本书采用的是后者)创建的Web应用总是使用IIS Express作为服务器,它会自动为我们指定一个可用的端口号。为了更好地模拟真实发布环境,同时避免“跨域资源共享”带来的困扰,我们采用本地IIS 作为服务器。如下图所示,WebHost项目在IIS中映射的Web应用采用的URL为“http://localhost/webhost”。

实际上到此为止,Web API的Web Host寄宿工作就已经完成,我们可以利用浏览器来调用寄宿的Web API来判断寄宿工作是否成功。由于浏览器在默认情况下访问我们在地址栏中输入的地址总是采用HTTP-GET请求,所以我们只能利用它来调用支持 HTTP-GET的Action方法,即定义在ContactsController中的Get方法。

根据我们注册的路由,如果我们访问目标地址“http://localhost/webhost/api/contacts”可以获得所有联系人列 表;如果目标地址为“http://localhost/webhost/api/contacts/001”,则可以得到ID为“001”的联系人信 息,右图证实了这一点。

从右图可以看到,我们采用的浏览器为Chrome,获取的联系人列表总是表示为XML,这是为什么呢?在前面介绍REST的时候,我们曾经提及一种 旨在识别客户端期望的资源表示形式并被称为“内容协商”的机制,它可以根据请求携带的相关信息来判断客户端所期望的响应资源表现形式。

对于ASP.NET Web API来说,它会优先利用请求报头“Accept”携带的媒体类型来确定响应内容采用的表现形式。如下所示的是Chrome访问 “http://localhost/webhost/api/contacts/001”发送请求的内容,它之所以会得到以XML表示的响应是因为 “Accept”报头指定的媒体类型列表中只有“application/xml”被ASP.NET Web API支持。如果我们采用IE,请求的“Accept”报头将携带不同的媒体类型列表,我们实际上会得到以JSON格式表示的响应结果。

   1: GET http://localhost/webhost/api/contacts/001 HTTP/1.1
   2: Host: localhost
   3: Connection: keep-alive
   4: Cache-Control: max-age=0
   5: Accept: text/html,application/xhtml+xml,application/xml ;q=0.9,image/webp,*/*;q=0.8
   6: User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36
   7: Accept-Encoding: gzip,deflate,sdch
   8: Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh-TW;q=0.4

为了进一步验证并演示ASP.NET Web API的内容协商机制,我们现在改用Fiddler来发送调用Web API的HTTP请求。如左图所示,我们利用Fiddler发送了一个针对目标地址“http://localhost/webhost/api /contacts/001”的HTTP-GET请求,并添加了一个值为“application/json”的“Accept”报头,请求发送之后确实 得到了以JSON格式表示的联系人列表。

支持PUT和DELETE请求

在定义ContactsController的时候,我们严格按照RESTful Web API关于“使用标准的HTTP方法”的指导方针,分别采用GET、POST、PUT和DELETE作为获取、创建、修改和删除联系人的操作所支持的 HTTP方法。但是IIS在默认情况下并不提供针对 PUT和DELETE请求的支持。

如右图所示,我们利用Fiddler发送了一个针对地址“http://localhost/webhost/api/contacts/001” 的HTTP-DELETE请求,旨在删除ID为“001”的联系人。但是遗憾的是,我们得到了一个状态为“405,Method Not Allowed”的响应,意味着服务端并不支持HTTP-DELETE方法。

IIS拒绝PUT和DELETE请求是由默认注册的一个名为“WebDAVModule”的自定义HttpModule导致的。WebDAV的全称 为“Web-based Distributed Authoring and Versioning”,它是一个在多用户之间辅助协同编辑和管理在线文档的HTTP扩展。该扩展使应用程序可以直接将文件写到 Web Server 上,同时支持文件的加锁和版本控制。

微软是推动WebDAV成为一个标准的主导力量,它自己利用自定义的HttpModule实现了IIS针对WebDAV的支持。但是这个默认注册 (注册名称为“WebDAVModule”)会拒绝HTTP方法为PUT和DELETE的请求,如果我们的站点不需要提供针对WebDAV的支持,解决这 个问题最为直接的方式就是利用如下的配置将注册的HttpModule移除。

   1: <configuration>
   2:   ...
   3:   <system.webServer>
   4:     <modules runAllManagedModulesForAllRequests="true">
   5:       <remove name="WebDAVModule" />
   6:     </modules>
   7:   </system.webServer>
   8: </configuration>

 

四、 以Self Host方式寄宿Web API

与WCF类似,寄宿Web API不一定需要IIS的支持,我们可以采用Self Host的方式使用任意类型的应用程序(控制台、Windows Forms应用、WPF应用甚至是Windows Service)作为宿主。对于我们演示的实例来说,项目SelfHost代表的控制台程序就是一个采用Self Host寄宿模式的宿主。

对于SelfHost这么一个空的控制台应用来说,除了需要添加针对WebApi的项目引用之外,还需要添加如下4个程序集引用。除了程序集 “System.Net.Http.dll”(它属于.NET Framework 原生的程序集)之外,其余3个均可以在目录“%ProgramFiles%\Microsoft ASP.NET\ASP.NET Web Stack 5\Packages\”中找到。

  • System.Web.Http.dll(\ Microsoft.AspNet.WebApi.Core.5.0.0\lib\net45\)
  • System.Net.Formatting.Http.dll(\Microsoft.AspNet.WebApi.Client.5.0.0\lib\net45\)
  • System.Web.Http.SelfHost.dll(\Microsoft.AspNet.WebApi.SelfHost.5.0.0\lib\net45\)
  • System.Net.Http.dll

通过上面的介绍我们可以看到以Web Host的方式寄宿Web API需要做的唯一一件事情是路由注册。但是对于Self Host来说,除了必需的路由注册外,我们还需要完成额外的一件事情,即手工加载定义了HttpController类型的程序集。整个寄宿工作通过如下 几行简单的代码就可以实现。

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         Assembly.Load("WebApi, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null");
   6:
   7:         HttpSelfHostConfiguration configuration = new HttpSelfHostConfiguration("http://localhost/selfhost");
   8:         using (HttpSelfHostServer httpServer = new HttpSelfHostServer(configuration))
   9:         {
  10:             httpServer.Configuration.Routes.MapHttpRoute(
  11:                 name            : "DefaultApi",
  12:                 routeTemplate   : "api/{controller}/{id}",
  13:                 defaults        : new { id = RouteParameter.Optional });
  14:
  15:             httpServer.OpenAsync();
  16:             Console.Read();
  17:         }
  18:     }
  19: }

ASP.NET Web API的Self Host寄宿方式通过HttpSelfHostServer来完成。如上面的代码片断所示,在手工加载了定义ContactsController类型的 程序集“WebApi.dll”之后,我们根据指定的基地址(“http://localhost/selfhost”),注册路由的URL模板将是以此 作为基地址的相对地址)创建了一个HttpSelfHostConfiguration对象,HttpSelfHostServer由该对象创建。接下 来,我们利用创建的HttpSelfHostConfiguration对象(对应着HttpSelfHostServer的Configuration 属性)的Routes得到全局路由表,并调用扩展方法MapHttpRoute注册了与Web Host寄宿方式一样的路由。当我们调用OpenAsync方法成功开启HttpSelfHostServer之后,服务器开始监听来自网络的调用请求。

如果读者朋友们对WCF比较熟悉的话,应该清楚在进行WCF服务寄宿的时候我们必须指定寄宿服务的类型,但是对于ASP.NET Web API的寄宿来说,不论是Web Host还是Self Host,我们都无需指定HttpController的类型。换句话说,WCF服务寄宿是针对具体某个服务类型的,而ASP.NET Web API的寄宿则是批量进行的。

ASP.NET Web API的批量寄宿源自它对HttpController类型的智能解析,它会从“提供的”的程序集列表中解析出所有HttpController类型(所 有实现了IHttpController接口的类型)。对于Web Host来说,它会利用BuildManager获得当前项目直接或者间接引用的程序集,但是对于Self Host来说,HttpController类型的解析在默认情况下只会针对加载到当前应用程序域中的程序集列表,这也是我们为何需要手工加载定义了 ContactsController类型的程序集的原因所在。

如果现在运行这个作为宿主的控制台程序,我们依然可以对寄宿其中的Web API发起调用。同样采用浏览器作为测试工具,在分别访问目标地址“http://localhost/selfhost/api/contacts”和 “http://localhost/selfhost/api/contacts/001”后,我们依然会得到上面的结果。

五、利用HttpClient调用Web API

对于一个.NET客户端程序,它可以利用HttpClient来进行Web API的调用。由于Web API的调用本质上就是一次普通的发送请求/接收响应的过程,所以HttpClient其实可以作为一般意义上发送HTTP请求的工具。在 ConsoleApp代表的控制台应用中,我们利用HttpClient来调用以Self Host方式寄宿的Web API。

由于我们需要使用到代表联系人的数据类型Contact,所以需要为该项目添加针对Common的项目引用。HttpClient定义在程序集 “System.Net.Http.dll”中,所以针对该程序集的引用也是必需的。除此之外,我们还需要添加针对程序集 “System.Net.Formatting.Http.dll”的引用,因为序列化请求和反序列化响应的相关类型定义在此程序集中。

如下所示的是整个Web API调用程序的定义,我们利用HttpClient调用Web API实现了针对联系人的获取、添加、修改和删除。由于HttpClient提供的大部分方法都采用针对Task的异步编程形式,所以我们将所有的操作定 义在一个标记为“async”的静态方法Process中,以便我们可以使用“await”关键字编写同步代码。

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         Process();
   6:         Console.Read();
   7:     }
   8:
   9:     private async static void Process()
  10:     {
  11:         //获取当前联系人列表
  12:         HttpClient httpClient = new HttpClient();
  13:         HttpResponseMessage response = await httpClient.GetAsync("http://localhost/selfhost/api/contacts");
  14:         IEnumerable<Contact> contacts = await response.Content.ReadAsAsync<IEnumerable<Contact>>();
  15:         Console.WriteLine("当前联系人列表:");
  16:         ListContacts(contacts);
  17:
  18:         //添加新的联系人
  19:         Contact contact = new Contact { Name = "王五", PhoneNo = "0512-34567890", EmailAddress = "wangwu@gmail.com" };
  20:         await httpClient.PostAsJsonAsync<Contact>("http://localhost/selfhost/api/contacts", contact);
  21:         Console.WriteLine("添加新联系人“王五”:");
  22:         response = await httpClient.GetAsync("http://localhost/selfhost/api/contacts");
  23:         contacts = await response.Content.ReadAsAsync<IEnumerable<Contact>>();
  24:         ListContacts(contacts);
  25:
  26:         //修改现有的某个联系人
  27:         response = await httpClient.GetAsync("http://localhost/selfhost/api/contacts/001");
  28:         contact = (await response.Content.ReadAsAsync<IEnumerable<Contact>>()).First();
  29:         contact.Name         = "赵六";
  30:         contact.EmailAddress     = "zhaoliu@gmail.com";
  31:         await httpClient.PutAsJsonAsync<Contact>("http://localhost/selfhost/api/contacts/001", contact);
  32:         Console.WriteLine("修改联系人“001”信息:");
  33:         response = await httpClient.GetAsync("http://localhost/selfhost/api/contacts");
  34:         contacts = await response.Content.ReadAsAsync<IEnumerable<Contact>>();
  35:         ListContacts(contacts);
  36:
  37:         //删除现有的某个联系人
  38:         await httpClient.DeleteAsync("http://localhost/selfhost/api/contacts/002");
  39:         Console.WriteLine("删除联系人“002”:");
  40:         response = await httpClient.GetAsync("http://localhost/selfhost/api/contacts");
  41:         contacts = await response.Content.ReadAsAsync<IEnumerable<Contact>>();
  42:         ListContacts(contacts);
  43: }
  44:
  45:     private static void ListContacts(IEnumerable<Contact> contacts)
  46:     {
  47:         foreach (Contact contact in contacts)
  48:         {
  49:             Console.WriteLine("{0,-6}{1,-6}{2,-20}{3,-10}", contact.Id, contact.Name, contact.EmailAddress, contact.PhoneNo);
  50:         }
  51:         Console.WriteLine();
  52:     }
  53: }

如上面的代码片段所示,我们创建了一个HttpClient对象并调用其GetAsync方法向目标地址“http://localhost /selfhost/api/contacts”发送了一个GET请求,返回的对象HttpResponseMessage表示接收到的响应。该 HttpResponseMessage对象的Content属性返回一个表示响应主体内容的HttpContent对象,我们调用其 ReadAsAsync<T>方法读取响应主体内容并将其反序列化成一个Contact集合。我们将表示当前联系人列表的Contact集合 输出在控制台上。

我们接下来调用HttpClient的PostAsJsonAsync<T>方法向目标地址“http://localhost /selfhost/api/contacts”发送一个POST请求以添加一个新的联系人。正如方法名称所体现的,作为参数的Contact对象将以 JSON格式被写入请求的主体部分。请求被正常发送并接收到响应之后,我们会打印出当前联系人列表。

在此之后,我们向目标地址“http://localhost/selfhost/api/contacts/001”发送一个GET请求以获取 ID为“001”的联系人。在修改了联系人的姓名(“赵六”)和电子邮箱(“zhaoliu@gmail.com”)之后,我们将其作为参数调用 HttpClient的PutAsJsonAsync<T>方法,以此向目标地址“http://localhost/selfhost /api/contacts/001”发送一个PUT请求以更新对应联系人的相关信息。联系人信息是否正常更新同样通过输出当前所有联系人列表来证实。

我们最后调用HttpClient的DeleteAsync方法向地址“http://localhost/selfhost/api /contacts/002”发送一个DELETE请求以删除ID为“002”的联系人并通过输出当前所有联系人列表来证实删除参数是否成功完成。

我们在运行宿主程序SelfHost之后启动此ConsoleApp程序,会在控制台上得到下所示的输出结果,由此可以看出通过调用 HttpClient的GetAsync、PostAsJsonAsync、PutAsJsonAsync和DeleteAsync方法帮助我们成功完成 了针对联系人的获取、添加、修改和删除。

   1: 当前联系人列表:
   2: 001   张三    zhangsan@gmail.com  0512-12345678
   3: 002   李四    lisi@gmail.com      0512-23456789
   4:
   5: 添加新联系人“王五”:
   6: 001   张三    zhangsan@gmail.com  0512-12345678
   7: 002   李四    lisi@gmail.com      0512-23456789
   8: 003   王五    wangwu@gmail.com    0512-34567890
   9:
  10: 修改联系人“001”信息:
  11: 002   李四    lisi@gmail.com      0512-23456789
  12: 003   王五    wangwu@gmail.com    0512-34567890
  13: 001   赵六    zhaoliu@gmail.com   0512-12345678
  14:
  15: 删除联系人“002”:
  16: 003   王五    wangwu@gmail.com    0512-34567890
  17: 001   赵六    zhaoliu@gmail.com   0512-12345678

 

六、创建一个“联系人管理器”应用

我们最后来创建一个叫做“联系人管理器”的Web应用。这是一个单网页应用,我们采用Ajax的请求的形式调用以Web Host模式寄宿的Web API实现针对联系人的CRUD操作。在正式介绍编程实现之前,我们不妨来看看该应用运行起来的效果。

如右图所示,当页面被加载之后,当前联系人列表会以表格的形式呈现出来。我们可以利用每条联系人记录右侧的“修改”和“删除”链接实现针对当前联系人的编辑和删除。除此之外,我们还可以点击左下方的“添加联系人”按钮添加一个新的联系人。

如果我们点击“删除”链接,当前联系人会直接被删除。如果我们点击了“修改”链接或者“添加联系人”按钮,被修改或者添加的联系人信息会显示在如左 图所示的一个弹出的“模态”对话框中。在我们输入联系人相关资料后点击“保存”按钮,联系人会被成功修改或者添加。被修改的现有联系人信息或者被添加的联 系人会立即体现在列表之中。

虽然这仅仅是一个简单的Web应用,但是我刻意使用了3个主流的Web前端开发框架,它们分别是JQuery、Bootstrap和KnockOut,这三个框架的使用体现在页面引用的CSS和JavaScript文件上。

   1: <!DOCTYPE html>
   2: <html xmlns="http://www.w3.org/1999/xhtml">
   3: <head>
   4:     <title>联系人管理器</title>
   5:     <link href="css/bootstrap.min.css" rel="stylesheet">
   6: </head>
   7: <body>
   8:     ...
   9:     <script src="Scripts/jquery-1.10.2.min.js"></script>
   1:
   2:     <script src="Scripts/bootstrap.min.js"></script>
   2:     <script src="Scripts/knockout-3.0.0.js"></script>
   2:     <script src="Scripts/viewmodel.js"></script>
  10: </body>
  11: </html>

JQuery,这个“地球人都知道”的JavaScript框架,我们无须对它作任何介绍了。Bootstrap 是集 HTML、CSS 和 JavaScript 于一体,是由微博的先驱 Twitter 在2011年8月开源的整套前端解决方案,Web 开发人员利用它能够轻松搭建出具有清爽风格的界面以及实现良好的交互效果的Web应用。Bootstrap是ASP.NET MVC 5默认支持的框架,当我们利用Visual Stduio创建一个ASP.NET MVC项目时,项目目录下就包含了Bootstrap相关的CSS和JavaScript文件。

在本例中,我们主要利用jQuery来实现以Ajax方式调用Web API,同时它也是其他两个框架(Bootstrap和KnockOut)的基础框架。至于Bootstrap,我们则主要使用它的页面布局功能和它提供 的CSS。除此之外,“编辑联系人”对话框就是利用Bootstrap提供的JavaScript组件实现的。

MVVM与Knockout

考虑到可能有人对Knockout(以下简称KO)这个JavaScript框架不太熟悉,在这里我们对它作一下概括性的介绍。KO是微软将应用于 WPF/Silverlight的MVVM模式在Web上的尝试,这是一个非常有用的JavaScript框架。对于面向数据的Web应用来说,MVVM 模式是一项不错的选择,它借助框架提供的“绑定”机制使我们无需过多关注UI(HTML)的细节,只需要操作绑定的数据源。MVVM最早被微软应用于 WPF/SL的开发,所以针对Web的MVVM框架来说,Knockout(以下简称KO)无疑是“根正苗红”。

MVVM可以看成是MVC模式的一个变体,Controller被View Model取代,但两者具有不同的职能,三元素之间的交互也不相同。以通过KO实现的MVVM为例,其核心是“绑定”,我个人又将其分为“数据的绑定”和 “行为的绑定”。所谓数据的绑定,就是将View Model定义的数据绑定到View中的UI元素(HTML元素)上,KO同时支持单向和双向绑定。行为绑定体现为事件注册,即View中UI元素的事件 (比如某个<button>元素的click事件)与View Model定义的方法(function)进行绑定。

如右图所示,用户行为(比如某个用户点击了页面上的某个按钮)首先触发View的某个事件,与之绑定的定义在View Model中的EventHandler(View Model的某个方法成员)被自动执行。它可以执行Model,并修改自身维护的数据,如果View和View Model的数据绑定是双向的,用户在界面上输入的数据可以被View Model捕获,View Model对数据的更新可以自动反映在View上。这样的好处显而易见:我们在通过JavaScript定义UI处理逻辑的时候,无需关注View的细节 (View上的HTML),只需要对自身的数据进行操作即可。

我们通过一个简单的例子来说明两种绑定在KO中的实现。假设我们需要设计如左图所示的“地址编辑器页面”,在页面加载的时候它会将默认的地址信息绑 定到表示省、市、区和街道的文本框和显示完整地址信息的<span>元素上,当用户在文本框中输入新的值并点击“确认”按钮后,显示的完整地 址会相应的变化。

我们可以利用KO按照如下的方式来实现地址信息的绑定和处理用户提交的编辑确认请求。我们首先需要通过一个函数来创建表示View Model的“类”,需要绑定的数据和函数将作为该类的成员,组成View的HTML元素则通过内联的“data-bind”属性实现数据绑定和事件注 册。我们最终需要创建View Model对象,并将其作为参数调用ko.applyBindings方法将绑定应用到当前页面。

   1: <div>
   2:     <div><label>省:</label><input data-bind="value: province" /></div>
   3:     <div><label>市:</label><input data-bind="value: city" /></div>
   4:     <div><label>区:</label><input data-bind="value: district" /></div>
   5:     <div><label>街道:</label><input data-bind="value: street"/>
   6:     <div><label>地址:</label><span data-bind="text: address"></span></div>
   7:     <div><input type="button" data-bind="click: format" value="确定"/></div>
   8: </div>
   9:
  10: <script type="text/javascript" >
   1:
   2:     function AddressModel() {
   3:         var self = this;
   4:         self.province     = ko.observable("江苏省");
   5:         self.city         = ko.observable("苏州市");
   6:         self.district     = ko.observable("工业园区");
   7:         self.street       = ko.observable("星湖街328号");
   8:         self.address      = ko.observable();
   9:
  10:         self.format     = function () {
  11:             if (self.province() && self.city() && self.district() && self.street()){
  12:                 var address = self.province() + " " + self.city() + " " + self.district() + " " + self.street();
  13:                 self.address(address);
  14:             }
  15:             else {
  16:                 alert("请提供完整的地址信息");
  17:             }
  18:     };
  19:
  20:         self.format();
  21: }
  22:
  23:     ko.applyBindings(new AddressModel());
</script>

如上面的代码片段所示,我们定义了一个名为AddressModel的类作为整个“地址编辑”页面的View Model,AddressModel的五个数据成员(province、city、district、street和address)表示地址的四个组 成部分和格式化的地址。它们都是基于双向绑定的Observable类型成员,意味着用户的输入能够即时改变绑定的数据源,而数据源的改变也能即时地反映 在绑定的HTML元素上。Observable数据成员是一个通过调用ko.observable方法创建的函数,方法调用指定的参数表示更新的数据。

AddressModel的另一个成员format是一个自定义的函数,该函数进行地址格式化并用格式化的地址更新address字段。由于address字段是一个Observable成员,一旦它的值发生改变,被绑定的HTML元素的值将会自动更新。

AddressModel的六个字段分别绑定在六个HTML元素上,其中province、city、district和street字段绑定到代 表对应文本框的Value属性上(data-bind=”value: {成员名称}”),而address字段则绑定到用于显示格式化地址的<span>元素的Text属性上(data-bind=”text: {成员名称}”),用于格式化地址的format字段则与“确定”按钮的click事件进行绑定(data-bind=”click: {成员名称}”)。真正的绑定工作发生在ko.applyBindings方法被调用的时候。

ViewModel

接下来我们来看看“联系人管理器”这个Web页面究竟如何来定义。具体来说,该页面的内容包含两个部分,HTML标签和JavaScript代码。 对于后者,其主要体现在具有如下定义的View Model上,我们将它定义在独立的JavaScript文件(viewmodel.js)中。

   1: function ViewModel() {
   2:     self             = this;
   3:     self.contacts    = ko.observableArray(); //当前联系人列表
   4:     self.contact     = ko.observable(); //当前编辑联系人
   5:
   6:     //获取当前联系人列表
   7:     self.load = function () {
   8:         $.ajax({
   9:             url        : "http://localhost/webhost/api/contacts",
  10:             type       : "GET",
  11:             success    : function (result) {
  12:                 self.contacts(result);
  13:             }
  14:         });
  15:     };
  16:
  17:     //弹出编辑联系人对话框
  18:     self.showDialog = function (data) {
  19:         //通过Id判断"添加/修改"操作
  20:         if (!data.Id) {
  21:             data = { ID: "", Name: "", PhoneNo: "", EmailAddress: "",
  22:                 Address: "" }
  23:         }
  24:         self.contact(data);
  25:         $(".modal").modal('show');
  26:     };
  27:
  28:     //调用Web API添加/修改联系人信息
  29:     self.save = function () {
  30:         $(".modal").modal('hide');
  31:         if (self.contact().Id) {
  32:             $.ajax({
  33:                 url    : "http://localhost/webhost/api/contacts/" + self.contact.Id,
  34:                 type    : "PUT",
  35:                 data    : self.contact(),
  36:                 success    : function () {self.load();}
  37:             });
  38:         }
  39:         else {
  40:             $.ajax({
  41:                 url   : "http://localhost/webhost/api/contacts",
  42:                 type    : "POST",
  43:                 data    : self.contact(),
  44:                 success : function () {self.load();}
  45:             });
  46:         }
  47:     };
  48:
  49:     //删除现有联系人
  50:     self.delete = function (data) {
  51:         $.ajax({
  52:             url        : "http://localhost/webhost/api/contacts/" + data.Id,
  53:             type       : "DELETE",
  54:             success    : function () {self.load();}
  55:         });
  56:     };
  57:
  58:     self.load();
  59: }
  60:
  61: $(function () {
  62:     ko.applyBindings(new ViewModel());
  63: });

对于上面定义的作为整个页面View Model的“类型”(ViewModel)来说,它具有两个“数据”成员(其实是函数)contacts和contact,前者表示当前联系人列表,后 者则表示当前修改或者添加的联系人。contacts和contact分别通过调用方法observableArray和observable创建,所以 它们均支持双向绑定。这两个数据成员分别被绑定到呈现当前联系人的表格和用于编辑联系人信息的对话框中。除了这两个数据成员之外,我们还定义了4个方法成 员。

  • load:发送Ajax请求调用Web API以获取当前联系人列表,并将得到的联系人列表“赋值”给contacts属性。
  • showDialog:弹出“编辑联系人信息”对话框。我们通过指定的联系人对象是否具有Id来判断当前操作是“修改”还是“添加”。对于后 者,我们会创建一个新的对象作为添加的联系人对象。被修改或者添加的联系人对象被“赋值”给contact属性。对话框的弹出通过调用表示对话框 的<div>的modal方法实现,该方法是由Bootstrap提供的。
  • save:发送Ajax请求调用Web API以添加新的联系人或者修改现有某个联系人的信息。contact属性作为提交的数据,至于“添加”还是“修改”,同样是通过它是否具有相应的Id来 决定。联系人成功添加或者修改之后,load方法被调用以刷新当前联系人列表。
  • delete:发送Ajax请求调用Web API以删除指定的联系人。联系人成功删除之后,load方法被调用以刷新当前联系人列表。

HTML

如下所示的是页面主体部分包含的HTML,ViewModel的相关成员会绑定到相应HTML元素上。整个内容大体包含两个部分,第一部分用于呈现当前联系人列表,第二部分在用于定义弹出的对话框。

   1: <!--当前联系人列表-->
   2: <div id="content">
   3:     <table class="table table-striped">
   4:         <thead>
   5:             <tr>
   6:                 <th>姓名</th>
   7:                 <th>联系电话</th>
   8:                 <th>电子邮件</th>
   9:                 <th></th>
  10:             </tr>
  11:         </thead>
  12:         <tbody data-bind="foreach: contacts">
  13:             <tr>
  14:                 <td data-bind="text: Name"></td>
  15:                 <td data-bind="text: PhoneNo"></td>
  16:                 <td data-bind="text: EmailAddress"></td>
  17:                 <td>
  18:                     <a href="#" data-bind="click: $root.showDialog">修改</a>
  19:                     <a href="#" data-bind="click: $root.delete">删除</a>
  20:                 </td>
  21:             </tr>
  22:         </tbody>
  23:     </table>
  24:     <a href="#" class="btn btn-primary" data-bind="click: showDialog">添加新联系人</a>
  25: </div>
  26:
  27: <!--添加/修改联系人对话框-->
  28: <div class="modal fade">
  29:     <div class="modal-dialog">
  30:         <div class="modal-content">
  31:             <div class="modal-header">
  32:                 <button type="button" class="close" data-dismiss="modal"  aria-hidden="true">&times;</button>
  33:                 <h4 class="modal-title">编辑联系人信息</h4>
  34:             </div>
  35:             <div class="modal-body form-horizontal" data-bind="with: contact">
  36:                 <div class="form-group">
  37:                     <label for="name" class="col-sm-2 control-label">姓名:</label>
  38:                     <div class="col-sm-10">
  39:                         <input type="text" class="form-control" id="name"  placeholder="姓名" data-bind="value:Name">
  40:                     </div>
  41:                 </div>
  42:                 <div class="form-group">
  43:                     <label for="phoneNo" class="col-sm-2 control-label"> 联系电话:</label>
  44:                     <div class="col-sm-10">
  45:                         <input type="text" class="form-control" id="phoneNo"  placeholder="联系电话" data-bind="value:PhoneNo">
  46:                     </div>
  47:                 </div>
  48:                 <div class="form-group">
  49:                     <label for="emailAddress" class="col-sm-2 control-label"> 电子邮箱:</label>
  50:                     <div class="col-sm-10">
  51:                         <input type="text" class="form-control"  id="emailAddress" placeholder="电子邮箱"  data-bind="value:EmailAddress">
  52:                     </div>
  53:                 </div>
  54:                 <div class="form-group">
  55:                     <label for="address" class="col-sm-2 control-label"> 地址:</label>
  56:                     <div class="col-sm-10">
  57:                         <input type="text" class="form-control" id="address"  placeholder="地址" data-bind="value:Address">
  58:                     </div>
  59:                 </div>
  60:             </div>
  61:             <div class="modal-footer">
  62:               <a href="#" class="btn btn-default" data-dismiss="modal">关闭</a>
  63:               <a href="#" class="btn btn-primary" data-bind="click: save">保存</a>
  64:             </div>
  65:         </div>
  66:     </div>
  67: </div>

第一部分的核心是呈现联系人列表的<table>元素,其主体具有一个针对contacts成员的foreach绑定 (<tbody data-bind=”foreach: contacts”>),该绑定利用内嵌的<tr>元素绑定列表中的每个联系人。至于联系人的具体某个属性,则对应着相应 的<td>元素,两者之间是一个text绑定(<td data-bind=”text: Name”></td>)。

表格中的每行右侧的“修改”和“删除”链接各自具有一个针对showDialog 和delete方法成员的click绑定(<a href=”#” data-bind=”click: $root.showDialog”>修改</a>)。之所以需要在成员名称前面添加“$root”前缀,是因为KO总是会从当前绑定 上下文中去获取绑定的成员。由于这两个链接HTML内嵌于foreach绑定之中,所以当前绑定上下文实际上是contacts属性中某个联系人对象。 “$root”前缀的目的在于告诉KO绑定的是ViewModel自身的成员。值得一提的是,当绑定的方法被执行时,KO会将当前绑定上下文作为参数。

在表示“编辑联系人信息”对话框的主体部分,我们通过一个with绑定(<div data-bind=”with: contact”>)将绑定上下文设定为ViewModel的contact属性,内嵌其中的4个文本框分别利用一个value绑定(比 如<input type=”text” data-bind=”value:Name”>)与对应的成员进行关联。ViewModel中最终用于添加/修改联系人的方法save则通过一个 click绑定(<三data-bind=”click: save”>保存</a>)与“保存”按钮关联在一起。

[转载]Getting started with ASP.NET Web API 2.2 for OData v4.0 - .NET Web Development and Tools Blog - Site Home - MSDN Blogs

mikel阅读(1008)

[转载]Getting started with ASP.NET Web API 2.2 for OData v4.0 – .NET Web Development and Tools Blog – Site Home – MSDN Blogs.

A few weeks ago we started publishing nightly builds for our initial support in ASP.NET Web API for the OData v4.0 protocol. Our OData v4.0 support is based on the OData Library for OData v4.0 that has been released in the past few months. The OData v4.0 protocol introduces a lot of changes and new features that allow more flexibility in the way to model services and improvements over many features from the past versions of the protocol.

In addition to this, the OData protocol has been recently ratified as an OASIS standard which will help bolster the adoption of the protocol by many companies and services all over the internet. If you want to know more about OData you can check the official site at www.odata.org where you can find the complete specification of the protocol and the features, the different formats supported and information about existing OData clients you can use in your apps. If you want to take a sneak peak at the new features and changes in the v4.0 version, you can do it here.

During the past few months, the Web API team has been working on the initial support for the v4.0 version. Many of the existing changes in the current nightly build deal with protocol and format changes from the v3.0 to the v4.0 version, but we have managed to add some interesting features to our current OData support. This list of features include:

1. OData attribute routing: This feature allows you to define the routes in your controllers and actions using attributes.

2. Support for functions: This feature allows you to define functions in your OData model and bind them to actions in your controller that implement them.

3. Model aliasing: This feature allows to change the names of the types and properties in your OData model to be different than the ones in your CLR types.

4. Support for limiting allowed queries: This feature allows the service to define limitations on the properties of the model that can be filtered, sorted, expanded or navigated across.

5. Support for ETags: This feature allows to generate an @odata.etag annotation based on some properties of the entity that can be used in IfMatch and IfNoneMatch headers in following requests.

6. Support for Enums: We’ve improved our support for Enums and now we support them as OData enumerations.

7. Support for $format: We’ve also added support for $format, so clients are able to specify the desired format of the response in the URL.

Important changes in this version

The OData v4.0 protocol includes a lot of new features and many changes to existing ones that improve the protocol and the modeling capabilities for the services implementers, but at the same time, those changes make difficult to support multiple versions of the protocol in a single implementation.

For that reason, we have decided to create a new assembly to support the v4.0 version of the protocol while maintaining the current assembly for those people who want to implement services based on previous versions.

One of the important goals with this new implementation has been to support the side by side scenario where customers can have v3 and v4 services running on the same application. To that effect, we had to make some changes in the current naming of some classes and methods to allow for a reasonable user experience. Here are the most important changes:

1. The package ID for the v4.0 is Microsoft.AspNet.OData.

2. The assembly name and the root namespace are now System.Web.OData instead of System.Web.Http.OData.

3. All the extension methods have been moved to System.Web.OData.Extensions.

4. We have removed all the extension methods that used to exist for HttpRequestMessage like GetODataPath or GetEdmModel and we have added a single extension method, ODataProperties that returns an object containing the common OData properties that were accessible by the old extension methods, like the IEdmModel of the service or the ODataPath of the request.

5. MapODataRoute has been changed to MapODataServiceRoute.

6. QueryableAttribute has been changed to EnableQueryAttribute.

For the sake of consistency between versions, we have done the same set of changes in the Microsoft.AspNet.WebApi.OData to achieve a similar development experience. Only the namespace remains System.Web.Http.OData in this version. The current methods and class names can still be used with the System.Web.Http.OData (OData v3.0), but we have marked them as obsolete, and they are not available in the new assembly.

Enough talking, let’s write an OData v4.0 service!

We’ll start our new OData v4.0 service by creating a simple web application that we’ll call ODataV4Service. We’ll chose to use the Web API template that will install the default Web API packages required for our application.

Once the basic application has been created, the first thing we need to do is update the existing Web API packages to use the nightly versions hosted on MyGet. In order to do that, right click on “References” in the project we have just created on the solution explorer, click on “Manage Nuget Packages” and expand the Updates section on the left.

image

Check that there is a source for WebStack Nightly, and if not, just proceed to add it by clicking the Settings button on the left bottom corner of the window and adding the source in the windows that appears after clicking, as shown in the following figure.

clip_image004

As you can see from the image, the URL for the nightly ASP.NET packages is http://www.myget.org/f/aspnetwebstacknightly/ and you can see all the different published packages on https://www.myget.org/gallery/aspnetwebstacknightly.

Now that we have setup our nightly package source we can go and update the Web API packages. In order to do that, we need to select the Include Prerelease option on the dropdown menu on the top of the window. Then we just need to click Update All.

Before leaving the Nuget Package Manager we need to install the Web API 2.2 for OData v4.0 package, in order to do that, we expand the Online tab, select the WebStack Nightly Source and the Include Prerelease option and then search for Microsoft.AspNet.OData.

clip_image006

After installing this package, we can exit the Nuget Package Manager and try running our application by pressing F5. The default page should appear in the browser.

At this point we have our application running on the latest 5.2 assemblies and we are ready to create our OData service. The first step is to create a model, for that we create a couple of C# classes representing entities as follow:

public class Player
{

    public virtual int Id { get; set; }

    public virtual int TeamId { get; set; }

    public virtual string Name { get; set; }
}

public class Team
{
    public virtual int Id { get; set; }

    public virtual string Name { get; set; }

    public virtual double Rate { get; set; }

    public virtual int Version { get; set; }

    public virtual ICollection<Player> Players { get; set; }

    public Category Category { get; set; }
}

We are going to need some data to use, so we are going to use Entity Framework for that, in order to do that, we install the Entity Framework package from Nuget in the same way we have done with the OData package, except this time we pick the nuget.org package source and a stable version of the package. Then we create a context and include an initializer to seed the database with some data, as shown here:

 

public class LeagueContext : DbContext
{
    public DbSet<Team> Teams { get; set; }
    public DbSet<Player> Players { get; set; }

    static LeagueContext()
    {
        Database.SetInitializer<LeagueContext>(new LeagueContextInitializer());
    }

    private class LeagueContextInitializer : DropCreateDatabaseAlways<LeagueContext>
    {
        protected override void Seed(LeagueContext context)
        {
            context.Teams.AddRange(Enumerable.Range(1, 30).Select(i =>
                new Team
                {
                    Id = i,
                    Name = "Team " + i,
                    Rate = i * Math.PI / 10,
                    Players = Enumerable.Range(1, 11).Select(j =>
                        new Player
                        {
                            Id = 11 * (i - 1) + j,
                            TeamId = i,
                            Name = string.Format("Team {0} Player {1}", i, j)
                        }).ToList()
                }
            ));
        }
    }
}

The next step is creating our OData model. We are going to create it in the WebApiConfig.cs file as the next figure shows:

 

public static IEdmModel GetModel()
{
    ODataModelBuilder builder = new ODataConventionModelBuilder();

    builder.EntitySet<Team>("Teams");
    builder.EntitySet<Player>("Players");

    return builder.GetEdmModel();
}

OData attribute routing

Now that we have created our model, we need to define the route for the OData service. We are going to use OData Attribute Routing to define the routes in our service. In order to do that, we need to open the WebApiConfig.cs file under our App_Start folder and add the System.Web.OData.Extensions and System.Web.OData.Routing namespaces to the list of our usings. Then, we need to modify our Register method to add the following lines:

ODataRoute route = config.Routes.MapODataServiceRoute("odata", "odata",GetModel());
route.MapODataRouteAttributes(config);

At this point we have successfully configured our OData service, but we haven’t defined yet any controller to handle the incoming requests. Ideally we would use scaffolding for this, but we are still working on getting the OData v4.0 scaffolders ready for preview (the existing scaffolders only support OData v3.0 services). So we have to create our controllers by hand, but we’ll see that with attribute routing it’s not difficult at all.

In previous versions of our Web API OData support, we had a very tight restriction on the names of the controllers, actions and even parameter names of our actions. With attribute routing, all those restrictions go away. We can define a controller or an action using whatever name we want as the following fragment of code shows:

 

[ODataRoutePrefix("Teams")]
public class TeamsEntitySetController : ODataController
{
    private readonly LeageContext _leage = new LeageContext();

    [EnableQuery]
    [ODataRoute]
    public IHttpActionResult GetFeed()
    {
        return Ok(_leage.Teams);
    }
    [ODataRoute("({id})")]
    [EnableQuery]
    public IHttpActionResult GetEntity(int id)
    {
        return Ok(SingleResult.Create<Team>(_leage.Teams.Where(t => t.Id == id)));
    }
}

As we can see on the figure above, we can use ODataRoutePrefixAttribute to specify a prefix for all the routes in the actions on the controller, and we can use ODataRouteAttribute to specify further segments that will get combined with the ones in the prefix. That way, the GetFeed action, represents the route /Teams and the GetEntity action represents routes like Teams(1), Teams(2), etc.

Support for Functions

Now that we have a basic service up and running, we are going to introduce some business logic. For that, we are going to define a function that will give us the teams whose rating is around a certain threshold with a given tolerance.

Obviously, we could achieve the same result with a query, but in that case, the clients of our service are ones responsible for defining the query and might make mistakes. However, if we give them a function, they only need to care about sending the right parameters.

In order to define a function that represents the business logic that we have specified, we can modify our GetModel function as follows:

 

public static IEdmModel GetModel()
{
    ODataModelBuilder builder = new ODataConventionModelBuilder();

    EntitySetConfiguration<Team> teams = builder.EntitySet<Team>("Teams");
    builder.EntitySet<Player>("Players");

    FunctionConfiguration withScore = teams
        .EntityType
        .Collection
        .Function("WithScore");
    withScore.Parameter<double>("threshold");
    withScore.Parameter<double>("tolerance");
    withScore.ReturnsCollectionFromEntitySet<Team>("Teams");

    return builder.GetEdmModel();
}

Functions can be defined at the service level (unbounded), at the collection level (bounded to collection) or at the entity level (bounded to the entity). In this case, we have defined a function bounded to the collection, but similar methods exist on the ODataModelBuilder class (to define service level functions) and on the EntityConfiguration class (to define entity level functions).

Now, the last step is to define an action that implements the function, in order to do that, we are going to take advantage of attribute routing. The action in the figure below shows the implementation:

 

[ODataRoute("Default.WithScore(threshold={threshold},tolerance={tolerance})")]
[EnableQuery]
public IHttpActionResult GetTeamsWithScore(double threshold, double tolerance)
{
    return Ok(_league.Teams.Where(t =>
        (t.Rate < (threshold + tolerance)) &&
        (t.Rate > (threshold - tolerance))));
}

As you can see, the way we call the function is by using it’s fully qualified name after the entity set on which we want to call it. We use attribute routing to define the parameters of the function and bind them to the parameters of the action in a very elegant way. In this case a sample call to the function would use the following URL /odata/Teams/Default.WithScore(threshold=3, tolerance=2)

Important note: If you try this in IIS, you’ll probably get a 404 response. This is because IIS doesn’t like the dot in the URL on the last segment (IIS thinks it´s a file). One possible way to fix this is to add piece of configuration on you web.config to ensure IIS runs the routing module on all the requests.

<system.webServer>
  <modules runAllManagedModulesForAllRequests="true"></modules>
</system.webServer>

Model aliasing

So far we’ve seen attribute routing and functions, now we are going to show another very interesting feature, model aliasing. Many times we want to expose some data from our domain, but we want to change things like the names of the domain entities or the names of some properties. In order to do that, we can use model aliasing.

There are two ways to configure model aliasing in our model, we can do it directly through the model builder by setting the name property of the types and the properties of the types, or we can annotate our types with DataContract and DataMember attribute. For example, we can change our model using data contract in the following way:

[DataContract(Name = "Member")]
public class Player
{
    [DataMember]
    public virtual int Id { get; set; }
    [DataMember(Name = "Team")]
    public virtual int TeamId { get; set; }
    [DataMember]
    public virtual string Name { get; set; }
}

Support for limiting the set of allowed queries

As we said above, query limitations allow a service to limit the types of queries that users can issue to our service by imposing limitations on the properties of the types of the model. A service can decide to limit the ability to sort, filter, expand or navigate any property of any type on the model.

In order to do that, there are two options, we can use attributes like Unsortable, NonFilterable, NotExpandable or NotNavigable on the properties of the types in our model, or we can configure this explicitly in the model builder. In this case, we’ll do it though attributes.

 

public class Team
{
    public virtual int Id { get; set; }
    [Unsortable]
    public virtual string Name { get; set; }
    [NonFilterable]
    public virtual double Rate { get; set; }
    [NotExpandable]
    [NotNavigable]
    public virtual ICollection<Player> Players { get; set; }
}

The meaning of Unsortable, NonFilterable and NotExpandable is self-explanatory, as for NotNavigable it is a shortcut for specifying that a property is Unsortable and NonFilterable. When a client issues a query that involves a limited property, the server will answer with a 400 status code and will indicate the limited property that is causing the request to fail.

Support for ETags

The next feature we are going to see is ETags. This feature allows a service to define what fields of an entity are part of the concurrency check for the entity. Those fields will be used to generate an @odata.etag annotation that will be sent to the clients when returning the entity, either as part of a feed or just the single entity.

The client can use this ETag value in the If-Match and If-None-Match headers to implement optimistic concurrency updates and efficient resource caching. In order to mark a field as part of the concurrency check, we can use the ConcurrencyCheck attribute or the Timestamp attribute.

It’s important to note that we should use one or another, but not both at the same time. The difference strives in that ConcurrencyCheck is applicable to multiple fields of the entity and Timestamp is meant to be applied to a single field.

The individual properties can also be marked as part of the concurrency check explicitly using the model builder. In this case, we’ll do it through attributes. For example, we have modified the Team entity to add a Version property and mark it as part of the ETag for the entity. The result is shown in the next figure:

public class Team
{
    public virtual int Id { get; set; }
    [Unsortable]
    public virtual string Name { get; set; }
    [NonFilterable]
    public virtual double Rate { get; set; }
    [ConcurrencyCheck]
    public int Version { get; set; }
    [NotExpandable]
    [NotNavigable]
    public virtual ICollection<Player> Players { get; set; }
}

Now, we will serialize the ETag of the entity when we retrieve it through a GET request, but we still need to take advantage of the ETag on the actions of our service. In order to do that, we are going to add a Put action and we’ll bind ODataQueryOptions<Team> in order to use the ETag.

[ODataRoute("({id})")]
public IHttpActionResult Put(int id, Team team, ODataQueryOptions<Team> options)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    if (id != team.Id)
    {
        return BadRequest("The key on the team must match the key on the url");
    }

    if (options.IfMatch != null &&
        !(options.IfMatch.ApplyTo(_leage.Teams.Where(t => t.Id == id))
        as IQueryable<Team>).Any())
    {
        return StatusCode(HttpStatusCode.PreconditionFailed);
    }
    else
    {
        _leage.Entry(team).State = EntityState.Modified;
        _leage.SaveChanges();
        return Updated(team);
    }
}

As we can see, we can take advantage of the ETag by binding ODataQueryOptions as a parameter and using the IfMatch or IfNoneMatch properties in that object in order to apply the ETag value to a given query.

In the above example, we check if the ETag on the IfMatch header exists and if so, if it doesn’t the value of the Team with the id represented by the URL to return a Precondition Failed status in that case.

Support for Enums

We already had support for Enums in Web API OData v3.0 by serializing them as strings, but the new version of the protocol has added full support for them, so we have upgraded our Enum support accordingly. In order to use Enums you just need to define a property with an Enum type and we’ll represent it as an Enum in the $metadata and the clients will able to use Enum query operators in $filter clauses. There are also specific overloads on the model builder in case we want to configure the enumeration explicitly. Defining an OData Enum property in your type is as simple as this:

public enum Category
{
    Amateur,
    Professional
}

public class Team
{
    public virtual int Id { get; set; }

    [Unsortable]
    public virtual string Name { get; set; }

    [NonFilterable]
    public virtual double Rate { get; set; }

    [ConcurrencyCheck]
    public virtual int Version { get; set; }

    [NotExpandable]
    [NotNavigable]
    public virtual ICollection<Player> Players { get; set; }

    public Category Category { get; set; }
}

Support for $format

This feature allows a client to specify the format they want in the query string of the URL bypassing any value set by the accept header. For example, the user can issue queries like this one to get all the metadata in the response, instead of just the minimal ammount (which is the default):

http://localhost:12345/odata/Teams?$format=application/json;odata.metadata=full

The above query uses a MIME media type and includes parameters in order to ask for a specific JSON version.

http://localhost:12345/odata/Teams?$format=json

The above query uses an alias to refer to a specific MIME media type, application/json which in the case of OData is equivalent to application/json;odata.metadata=minimal

Using the .NET OData client to query the v4.0 service

The OData client for .NET has been released this week, the following blog post contains the instructions on how to use it to generate a client that can be used to query Web API for OData v4.0 services.

Note: If you plan to use $batch it won’t work properly with the client that gets generated by default. This is caused due to the fact that we are still using the beta version of OData Lib (we plan to update to the RTM version in the near future) and the client uses the RTM version of OData Lib. In order to workaround this issue, you can do the following:

Open the Nuget Package Console and downgrade the OData Client package to beta1 doing:

1. Uninstall-package Microsoft.OData.Client -RemoveDependencies -project <ProjectName>

2. Install-package Microsoft.OData.Client -version 6.0.0-beta1 -pre -project <ProjectName>

Perform the following changes on the T4 template mentioned on the blog:

1. Replace Microsoft.OData.Client.Key with Microsoft.OData.Service.Common.DataServiceKeyAttribute

2. Replace Microsoft.OData.Client.ODataProtocolVersion with Microsoft.OData.Service.Common.DataServiceProtocolVersion

Samples

Along with the nightly build we have published samples with the new features and new samples showing off common scenarios that target OData v4 in the ASP.NET codeplex site.

Conclusion

We have started publishing nightly builds for our OData v4.0 support that it’s built on top of the ODataLib support that has already shipped. Our nightly builds can be found on http://www.myget.org/f/aspnetwebstacknightly. The package ID to find them is Microsoft.AspNet.OData (Remember to select IncludePrerelease in the Nuget Package Manager).

We’ve also seen a brief introduction to all the new features in this version, like OData attribute routing, functions, model aliasing, query limitations, ETags, enums and $format. You can find samples for all the new features in https://aspnet.codeplex.com/SourceControl/latest#Samples/WebApi/OData/v4/.

Enjoy!