大多数人,都低估了编程学习的难度,而高估了自己的学习能力和毅力。
当前系列: ASP.NET 修改讲义
复习:

新建项目

Framework和Core都可以生成WebApi项目,(因为刚学完ASP.NET Framework MVC)我们首先以Framework进行演示:

项目和文件结构

基本上我们就可以把WebApi当成一个:没有View的MVC(所以配合View存在的Model也不是必须的)

所有和View相关的内容,都不需要学习:严格意义上(RESTful)的WebApi项目不应该返回HTML页面内容。


启动

WebApi本身是没有前端界面的。

GET请求可以通过浏览器地址栏输入发送(演示:Ctrl+F5运行得到JSON格式数据

但POST呢?PUT呢,DELETE呢?所以最常用的,还是Postman(复习)

PS:其他两种方式见WebApi Core


route

WebApi route的endpoint是还是Controller和Action。

默认配置

但是Controller里面的

Action方法名

只是用于匹配HTTP请求的method的。

换言之,url里面的path,只能决定controller,能决定action;action是有HTTP请求的method决定的!

所以请求同一个url,但使用不同的Http谓语(verb/method) ,服务器端应予以不同的响应。

Postman演示:同一个url,进入不同的Get()/Post()方法

按Restful风格,controller一般是名称,配合作为method的名词,进行表意,比如URL:api/user:

  • 用GET发起,是获取user
  • 用POST/PUT发起,是新建/更改user
  • 用DELETE发起,是删除user
  • ……

Action方法参数

URL是可以带url参数的,而且url参数可以“绑定”到Action方法参数。

演示:localhost:64031/api/register?uname=fg

public IEnumerable<string> Get(string uname)
{
    return new string[] { "value1", "value2", uname };

Action方法参数还可以从route data(比如{id})中取值。

注意:非可空的、非可选URL参数,也参与到route匹配中!(对比MVC报参数异常,演示WebApi报route错误)

自定义

演示:Global.asax - Application_Start() - WebApiConfig.Register

  • convention的默认规则,定义在(和MVC类似,但是,忽略action):
    config.Routes.MapHttpRoute(
  • 显式开启,使用特性([])标记route的功能:
    config.MapHttpAttributeRoutes();
    PS:这种方式其实也适用于MVC,但建议

演示:localhost:64031/user/by-name/fg route到:

[RoutePrefix("user")]
public class UserController : ApiController
{
    [Route("byName/{name:alpha}")]
    public IEnumerable<string> Get(string name)

说明:

  • [RoutePrefix]适用于整个controller中所有用[route]标记过的Action
  • [Route]作用于单个Action,构造函数template同Routes.MapHttpRoute()中的template,可以有path也可以有{}标记的route data
  • [Route]和Action要匹配,比如[Route]中有name参数,Action中也一定要有name参数才行
  • {name}是route data键值对的“键名”,后面冒号引导的是“约束”,详见下表:
    Constraint Description Example
    alpha Matches uppercase or lowercase Latin alphabet characters (a-z, A-Z) {x:alpha}
    bool Matches a Boolean value. {x:bool}
    datetime Matches a DateTime value. {x:datetime}
    decimal Matches a decimal value. {x:decimal}
    double Matches a 64-bit floating-point value. {x:double}
    float Matches a 32-bit floating-point value. {x:float}
    guid Matches a GUID value. {x:guid}
    int Matches a 32-bit integer value. {x:int}
    length Matches a string with the specified length or within a specified range of lengths. {x:length(6)}
    {x:length(1,20)}
    long Matches a 64-bit integer value. {x:long}
    max Matches an integer with a maximum value. {x:max(10)}
    maxlength Matches a string with a maximum length. {x:maxlength(10)}
    min Matches an integer with a minimum value. {x:min(10)}
    minlength Matches a string with a minimum length. {x:minlength(10)}
    range Matches an integer within a range of values. {x:range(10,50)}
    regex Matches a regular expression. {x:regex(^\d{3}-\d{3}-\d{4}$)}
  • 无论[RoutePrefix]还是[Route],其template都不能以“/”或“~/”开头

另因为优先级问题,配置了Order属性。

另:当一个url可以适用两个route规则的时候,

[Route("by/{name}")]
public IEnumerable<string> Get(string name)

[Route("by/{id}")]
public string Get(int id)
直接报错:

Multiple actions were found that match the request

[Http<method>]

MVC里面出现的:

[HttpPost]
[HttpPut]

等在WebApi中一样适用,且优先级高于Action方法名识别。

PS:对前后端分离的项目来说,用url参数其实更能充分表意比较

自定义route的使用,更多是为了url对于“普通用户”更友好……


Action相关

参数[FromBody]

默认是[FromUri],数据源是从uri中(url参数或route data)获得的,这种参数要参与route匹配。

标记为[FromBody]的参数,数据源是Ajax请求的Body(即$.ajax()的data),这种参数不会参与route匹配。

Postman演示:Body中设置数据,Send到后台……

注意选择正确/合适的Content-Type!当Action参数

  • 仅仅是一个基本类型(比如string)时,
    public void Put(int id, [FromBody] string value)
    最好选择raw-JSON,即:application/json
  • 是自定义类型时,
    public void Put(int id, [FromBody] User user)
    可以使用raw-JSON或urlencoded:
  • 含二进制文件时,选择form-data,即:

如果Content-Type不对,要么报错要么绑定失败(演示)

复杂类型和集合

比如Article中的Keywords:

    public class Article : BaseEntity
    {
        public string Title { get; set; }
        public virtual IList<Keyword> keywords { get; set; }

前端如何组织数据格式?一般使用对象数组:

{ 
    "Title":"垃圾JavaScript", 
    "Keywords":[
        {
            "Id":1,"Name":"csharp"
        }
    ]
}


返回值

默认是XML格式的,可以在Global.asax - Application_Start() 中:

GlobalConfiguration.Configuration.Formatters
    .XmlFormatter.SupportedMediaTypes.Clear();
清除掉对XML格式化的支持——这样默认就返回JSON数据了。

WebApi会自动将所有C#对象转换成JSON数据。


ApiController

因为继承的是ApiController,所以有些属性和方法就和MVC中的Controller不同:(演示)

  • HttpRequestMessage Request
  • HttpRequestContext RequestContext
  • 么得Response,但WebApi中有一个ResponseMessage类,可以new出来用……
如果你不愿使用上述对象的话,也可以很方便的利用

HttpContext.Current

获取所有你熟悉的对象:Request/Response/Session/Server……

但是,如果你的项目是DDD的,就不要这样做,因为:

HttpContext.Current点出来的对象

  • 是“具体”的,而不是“抽象”的;
  • 很多属性是只读的,而不是可写的

public HttpRequest Request { get; }
public sealed class HttpRequest{
这就给单元测试时“模拟”一个Http请求造成了很多问题。

对比:无论MVC的Controller,还是ApiController,他们的属性类型都是“抽象”的

public HttpRequestMessage Request { get; set; }
public class HttpRequestMessage : IDisposable

#体会#:抽象的作用……


Model验证

验证没有问题,和MVC一模一样,但问题是:验证失败的结果怎么办?

标准的Restful风格WebApi,Action返回值是确定的C#类型,比如string/int/IEnumerable……,这些都是“正常”情形下的返回。

如何能让这些数据“包含”额外的错误信息?

除了抛异常/Filter截断(后文详述),就只有重新设计Action的返回值了:

public IHttpActionResult Post([FromBody] User user)
{
    if (ModelState.IsValid)
    {
        //return Ok(user);
        //return Json(user);
        return Content(HttpStatusCode.OK, 20);
    }
    else
    {
        return BadRequest(ModelState);
public class User
{
    [Required(ErrorMessage = "* 用户名必填")]
    public string Name { get; set; }

Postman演示:Body中有Age但没有Name(如果Body完全为空,user=null,不会触发Model验证)

说明:
  1. Content()和BadRequest(),以及其他一系列的方法(Ok()/Json())都能返回IHttpActionResult对象
  2. HttpStatusCode是一个枚举,包含了200、301、302、404等状态码
  3. 可以直接把ModelState传入BadRequest()


Cookie/Session

如果不使用HttpContext.Current.Request/Response,问题就变得复杂了。

获取

从header中取出所有cookie(如果其中包含“user”的话)

CookieHeaderValue cookies = Request.Headers.GetCookies("user").FirstOrDefault();

再从cookies中取出name为user的cookie值

return cookies["user"].Value;

注意:如果cookie中存放的是“多值”的话,这个Value就会被截断(演示

应该使用:

return cookies["user"].Values["id"];

生成

更加麻烦,需要返回的整个Action返回一个HttpResponseMessage对象:

public HttpResponseMessage Get()

然后生成cookie并附加到Header中:

HttpResponseMessage response = new HttpResponseMessage();
response.Headers.AddCookies(
    new List<CookieHeaderValue>{
            new CookieHeaderValue("user", new NameValueCollection
            {
                {"id", "12"},
                {"pwd", "yuanzhan17bang" }
            })
});
return response;

但是,cookie以外正常的Body怎么办?

生成简单的文本数据:

response.Content = new StringContent("源栈欢迎你");

或者,返回JSON数据 (需要引入package:System.Net.Http.Json)

response.Content = JsonContent.Create(new User
{
    Name = "大飞哥",
    Age = 20

PS:cookie都不一定在前后端分离中使用(时髦的是JWT),所以Session就略过了……


文件

上传

WebApi里没有对上传文件的Model绑定,如果不使用HttpContext.Current.Request.Files,就需要利用ReadAsMultipartAsync()和CopyToAsync()两个方法。

首先需要通过provider把上传文件内容读到Request.Content中:

MultipartStreamProvider provider = new MultipartMemoryStreamProvider();
await Request.Content.ReadAsMultipartAsync(provider);

然后获取文件内容:

HttpContent item = provider.Contents.FirstOrDefault();

这里(如果有必要的话)可以判断一下:

if (Request.Content.IsMimeMultipartContent())

最后保存文件内容:

using (FileStream stream = new FileStream(serverFileName, FileMode.Create))
{
    await item.CopyToAsync(stream);
}

其中,serverFileName可来源于:

//获取文件本地名,但注意不是FileName,而是FileNameStar
//因为FileName还包含着双引号,(*/ω\*)
string localFileName = item.Headers.ContentDisposition.FileNameStar;
string serverFileName = Path.Combine(
    HttpContext.Current.Server.MapPath("~/App_Data"), localFileName);

下载

没有File()的封装,还是需要利用到HttpResponseMessage返回:

HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK)
{
    //Content = new StreamContent(stream);需要配合stream.Seek(0, SeekOrigin.Begin);
    Content = new ByteArrayContent(stream.ToArray())
};
result.Content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
return result;


Filter

和MVC的Filter具有相同/似的原理,但他们需要继承和实现的类和接口,属于不同的程序集,具有不同的名称空间,不能混用。(演示)

ActionFilter

WebApi没有View,所以只有OnActionExecuting()和OnActionExecuted()方法(但多了异步方法)。

以ModelState验证为例:

public class ModelStateValidAttribute : ActionFilterAttribute
{ public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (!actionContext.ModelState.IsValid)
        {
            HttpResponseMessage response = new HttpResponseMessage();
            response.Content = JsonContent.Create(
                //整理ModelState里的error格式为:
                //"user.Name": "* 用户名必填"
                actionContext.ModelState
                    .Where(m => m.Value.Errors?.Count > 0)
                    .Select(m => new Dictionary<string, string> { 
                        { m.Key, m.Value.Errors.First().ErrorMessage }
                    })
                );
            //截断后续代码的运行
            actionContext.Response = response;
应该都能读懂吧?

ExceptionFilterAttribute

声明一个子类:

public class ErrorHandlerFilter : ExceptionFilterAttribute
全局注册在WebApiConfig.Register():
config.Filters.Add(new ErrorHandlerFilter());


其他

安全

除了:后端永远不要相信前端,还涉及到一个前后端职责划分的问题。比如:

  • Script注入,后端管不管?前后端分离之后,前端可以有很强的独立性,可以让后端完全不要管。就算是用户有注入的脚本,前端可以在呈现的时候自行予以处理。
  • 用户的身份标记(token),究竟是用cookie,还是JWT?在哪里生成?后端是否需要验证cookie/JWT有无伪造?
  • ……

性能

前后端分离之后,其实大幅降低了后端开发人员的“性能”压力:

  • 静态资源的加载,比如图片、.css、.js文件,不归后端管了
  • 页面生成,不劳后端操心了
  • Ajax请求,需要的数据量本身就少很多了
  • 很多数据,都可以直接缓存(cache)在前端了……

如果说后端要使用缓存,个人建议,必须确保缓存的数据是“即时有效”的。

架构

前后端分离之后,后端的架构可以更简单:

  • 没有View了,Model可以被省略掉只保留entity即可,往前端发送的数据如果和entity不合,可以直接使用匿名对象
  • 没有Model,Service层也可以省略掉,因为:
    • Service层,以前Model和Entity的映射这一块没了
    • UI层,以前跳转传值啥的一大块也没了
    • MockService层,也用处不大了,前端可以自己模拟后端返回

    在Controller的Action里,就可以一股脑的把Serivice的活干了

  • 所以AutoMapper没用了,Autofac可用可不用了……

  • Repository都可以省略掉,只是把可重用的查询封装到一个Query项目中(全部做成扩展方法)


作业

说明:时间有限的,可选择在core中完成。

参考之前MVC作业和17bang示例网站,实现前端调用所需的restful风格的webapi接口。包括但不限于:

  1. 注册/登录:
    1. [GET] bool api/User/HasName/{name}:检查用户名{name}有无重复
    2. [POST] int api/User/Register:传入用户名和密码等,返回用户id
    3. [POST] string api/User/Logon:传入用户名和密码等,成功登录生成cookie,失败返回原因
    4. [GET] object api/User/NameStartsWith/{prefix}:用户名以{prefix}开头的用户(至少包含id和Name)
    5. 其他邀请人相关的api……
  2. 文章相关:
    1. [POST] int api/Article/New:发布,传入文章内容(标题、正文、作者、关键字、分类等返回新生成文章Id
    2. [PUT] datetime api/Article/Edit/{id}:修改,传入文章内容返回文章最后修改时间
    3. [GET] object api/Article/{id}:传入文章id,返回文章单页相关信息
    4. [GET] object api/Article/Index:传入作者、排序要求、从第n条起到第m条止等内容(@想一想@:用URL还是data?)
    5. [GET]:object api/Keyword/{content}:是否已存在{content}关键字?是:返回Id和已使用次数;否:返回null
    6. [POST]:api/Category/New:传入分类名称,返回其id
    7. 其他草稿/广告等相关api……
  3. 评论评价:
    1. 点赞和踩
    2. 发布评论
    3. 回复评论
    4. ……
  4. 消息:
    1. 有无未读
    2. 标记为已读
    3. [DELETE]:删除,@想一想@:messageIds怎么传到后台?
注意上述webapi的访问权限、异常信息返回等。
学习笔记
源栈学历
大多数人,都低估了编程学习的难度,而高估了自己的学习能力和毅力。

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 ASP.NET 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码