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
WebApi route的endpoint是还是Controller和Action。
但是Controller里面的
只是用于匹配HTTP请求的method的。
换言之,url里面的path,只能决定controller,不能决定action;action是有HTTP请求的method决定的!
所以请求同一个url,但使用不同的Http谓语(verb/method) ,服务器端应予以不同的响应。
Postman演示:同一个url,进入不同的Get()/Post()方法
按Restful风格,controller一般是名称,配合作为method的名词,进行表意,比如URL:api/user:
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
config.Routes.MapHttpRoute(
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)
说明:
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}$)} |
另因为优先级问题,配置了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
MVC里面出现的:
[HttpPost] [HttpPut]
等在WebApi中一样适用,且优先级高于Action方法名识别。
PS:对前后端分离的项目来说,用url参数其实更能充分表意(比较)
自定义route的使用,更多是为了url对于“普通用户”更友好……
默认是[FromUri],数据源是从uri中(url参数或route data)获得的,这种参数要参与route匹配。
标记为[FromBody]的参数,数据源是Ajax请求的Body(即$.ajax()的data),这种参数不会参与route匹配。
Postman演示:Body中设置数据,Send到后台……
注意选择正确/合适的Content-Type!当Action参数
public void Put(int id, [FromBody] string value)最好选择raw-JSON,即:application/json
public void Put(int id, [FromBody] User user)可以使用raw-JSON或urlencoded:
如果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,所以有些属性和方法就和MVC中的Controller不同:(演示)
获取所有你熟悉的对象: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
#体会#:抽象的作用……
验证没有问题,和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验证)
说明:如果不使用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;
和MVC的Filter具有相同/似的原理,但他们需要继承和实现的类和接口,属于不同的程序集,具有不同的名称空间,不能混用。(演示)
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;应该都能读懂吧?
声明一个子类:
public class ErrorHandlerFilter : ExceptionFilterAttribute全局注册在WebApiConfig.Register():
config.Filters.Add(new ErrorHandlerFilter());
除了:后端永远不要相信前端,还涉及到一个前后端职责划分的问题。比如:
前后端分离之后,其实大幅降低了后端开发人员的“性能”压力:
如果说后端要使用缓存,个人建议,必须确保缓存的数据是“即时有效”的。
前后端分离之后,后端的架构可以更简单:
在Controller的Action里,就可以一股脑的把Serivice的活干了
所以AutoMapper没用了,Autofac可用可不用了……
说明:时间有限的,可选择在core中完成。
参考之前MVC作业和17bang示例网站,实现前端调用所需的restful风格的webapi接口。包括但不限于:
多快好省!前端后端,线上线下,名师精讲
更多了解 加: