Skip to content

Files

Latest commit

33025dc · Jan 8, 2022

History

History
495 lines (328 loc) · 18.4 KB

File metadata and controls

495 lines (328 loc) · 18.4 KB

五、控制器

控制器基础

控制器是资源管理中的主要元素,因为它代表对客户端公开的资源的操作。所有的网络应用编程接口控制器都必须实现IHttpController接口,一般来说,它们继承自ApiControllerApiController是一个抽象类,向所有控制器公开一些基本功能,并实现IHttpController

ApiController界面报告如下:

  public class ApiController
  {
      HttpRequestMessage Request { /* */ }
      HttpConfiguration Configuration { /* */ }
      HttpControllerContext ControllerContext
  { /*
  */}
      ModelStateDictionary ModelState { /* */}
      UrlHelper Url { /* */}
      IPrincipal User { /* */ }
      Task<HttpResponseMessage> ExecuteAsync(HttpControllerContext controllerContext,
  CancellationToken
  cancellationToken){ /* */ }
      void Dispose() {/* */ }
  }

如前所述,控制器的入口点是ExecuteAsync方法,负责选择动作并执行。动作选择基于 HTTP 方法,因此如果有GET请求,控制器将搜索以Get开头的公共实例方法,或者搜索具有HttpGet属性的方法。

当执行所选择的动作时,控制器执行过滤器流水线;如果有任何筛选器,它将在操作本身之前执行。

考虑到一个博客文章资源,管理它的控制器应该是这样的:

  public class PostsController : ApiController
  {
      private readonly IPostRepository _repository;

      public PostsController(IPostRepository repository)
      {

  _repository = repository;
      }

      public IQueryable<Post> Get()
      {

  return
  _repository.GetAll();
      }

      public Post Get(int id)
      {

  return
  _repository.Get(id);
      }

      public HttpResponseMessage Post(Post post)
      {

  _repository.Create(post);

  var response = Request.CreateResponse(HttpStatusCode.Created);

  response.StatusCode = HttpStatusCode.Created;
          string uri = Url.Link("DefaultApi", new { id = post.Id });
          response.Headers.Location
  = new Uri(uri);

          return response;
      }

      public HttpResponseMessage Put(int id, Post post)
      {

  post.Id = id;
          _repository.Update(post);
          var response =
  Request.CreateResponse(HttpStatusCode.NoContent);
          string uri = Url.Link("DefaultApi", new { id = post.Id });
          response.Headers.Location
  = new Uri(uri);
          return response;
      }

      public HttpResponseMessage Delete(int id)
      {
          _repository.Delete(id);

  var response =
  Request.CreateResponse(HttpStatusCode.NoContent);

  return response;        
      }
   }

它展示了用于进行 CRUD 操作的五种典型方法。每个方法都有自己的签名,除了使用存储库读取和保存数据之外,它还在必要的地方用头和其他东西来丰富响应。

在前面的例子中,我们省略了错误检查,以保持代码集中在 REST API 上。我们将在本章后面看到如何管理错误。

让我们深入每一种方法来了解发生了什么。

行动

Get()

实际上Get有两个重载:一个返回整个帖子集合,一个返回单个帖子。第一个将简单地建立一个IQueryable帖子集合(我们稍后将回到IQueryable的意思)。

  public IQueryable<Post> Get()
  {
      return
  _repository.GetAll();
  }

这种方法相当容易;它只是要求存储库返回一个没有过滤器或其他选择子句的可用集合Posts,然后将集合(IQueryable<Post>)返回给调用者。

前面我们说过,控制器的结果应该是一个HttpResponseMessage。当动作返回类似Post的模型对象时会发生什么?

调用Get方法的基础ApiController中的方法将结果包装在一个HttpResponseMessage中,将Post对象放入内容中。因此,我们有两种方法向客户返回结果:

  • 直接构建HttpResponseMessage
  • 返回一个对象,让 ASP.NET 网络应用编程接口为我们创建HttpResponseMessage响应。

通常,如果不需要处理响应(标题、状态等)。),返回结果最简单的方法是直接返回对象,就像我们在前面的代码示例中所做的那样。

要调用这个方法,我们必须在没有任何其他参数的情况下向 URI 发出GET请求。

图 5:简单的 get

在邮递员的帮助下,我们可以检查反应。状态为200 OK。如果一切正常,ASP.NET Web API 管道会为我们添加 HTTP 状态,在这种情况下200 OK就是我们所期望的。在正文中,我们可以看到控制器返回的Post对象列表。我们可以看到,格式是JSON;如果请求中没有指定标题,这是默认格式。


邮递员

Postman 是一个 Chrome 扩展,可以帮助你在使用 API 时更高效。它帮助您构建带有参数、头、身份验证等的 HTTP 请求。它发送请求并读取响应。这是一个开源项目。更多信息可以在官方网站上找到:http://www.getpostman.com


Get(int id)

这是第二种类型的GET,在这里我们指定我们想要的资源的Id

  public Post Get(int id)
  {
      return 
  _repository.Get(id);
  }

代码与前面的情况非常相似,唯一的区别是这里的动作返回的是单个Post而不是集合。这里又是ApiController,它将返回的帖子包装成一个HttpResponseMessage,并设置标题和状态代码(在这种情况下,与前面的情况一样为200 OK)。

员额(员额)

这里是事情变得有趣的地方。Post()方法的目的是在存储器中创建一个新的Post,并将正确的状态代码返回给调用者。

  public HttpResponseMessage Post(Post post)
  {
      _repository.Create(post);
      HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.Created);
      response.StatusCode
  = HttpStatusCode.Created;
      string uri = Url.Link("DefaultApi", new { id = post.Id });

  response.Headers.Location = new URI(uri);

      return response;
  }

要存储Post,我们使用PostRepositoryCreate()方法,将帖子保存到定义的存储中。在这种情况下,必须手动创建响应消息,以便能够设置一些属性,并从 REST 的角度构建正确的响应。

为了创建响应消息,我们使用方法CreateResponse(),传递这种请求的状态代码(201 Created)。

然后我们添加位置头,它将包含新创建的资源的 URI。为此,我们只需要用新的Post的 URI 设置response.Headers.Location属性的值。为了构建 URI,我们不用手动编写它,而是使用 URL 助手,并将路线名称(DefaultApi)和Post.Id传递给Link方法。这个方法会返回类似/api/Posts/42的东西,其中42是新创建的Postid

使用 Postman,我们创建了一个新的HTTP POST请求,在请求正文中指定了关于Post对象的信息。

响应体将为空,因为我们不返回任何值,只返回对象的状态(201 Created)和新位置。有趣的是标题。

图 6:开机自检

在图 6 中,我们可以看到标题位置指向新创建的Post,其id等于 5。通过指定新位置,客户端应用程序将被自动告知新对象的位置,而不是猜测和手动构建 URI;这正是我们构建一个真正的超媒体 API 所需要的。

如果我们想看看新创建的Post看起来如何,我们只需要向位置 URI / api/posts/5发出GET请求:

图 7:对新创建的资源的 GET 请求

Put(内部 id,发布帖子)

Put()动作用于改变资源。该方法采用两个参数:一个来自 URI(T1),另一个在请求主体(T2)对象本身中指定。由于Put()方法对资源起作用,我们需要在 URI 拥有Id(就像我们使用GET方法/api/posts/5时一样)。Id将代表要修改的资源,请求体将包含要存储的Post的新内容。

  public HttpResponseMessage Put(int id, Post post)
  {
      post.Id = id;

  _repository.Update(post);
      var response = Request.CreateResponse(HttpStatusCode.OK, post);
      string uri = Url.Link("DefaultApi", new { id = post.Id });

  response.Headers.Location = new URI(uri);
      return response;
  }

Put()方法的实现类似于Post()方法。区别在于回复状态码:是200 OK,回复包含更新后的帖子(对CreateResponse的调用取状态和内容)。

将更新后的Post对象放入响应中是一个偏好问题。有时PUT有一个空的主体,只包含更新资源的位置头。在这种情况下,我们决定包括更新的Post即使不是严格必要的。

为了将帖子包含在响应中,我们使用了CreateResponse()方法的重载,除了状态代码之外,还传递了Post的实例。

当我们试图用PUT调用资源时,按照 HTTP 标准,我们需要传递需要修改的资源的全部内容。如果我们要做部分更新,首选的方法是使用PATCH(更多关于补丁的信息可以在http://www.rfc-editor.org/rfc/rfc5789.txtPOST动词中找到)。

因此,如果我们在正文中传递新标题,响应将包含带有新标题的完整帖子,并且我们在操作中指定的位置标题将在标题中。

图 8:一个 PUT

删除(int id)

最后一个动作是DELETE法。

  public HttpResponseMessage Delete(int id)
  {
   _repository.Delete(id);
   HttpResponseMessage response =
  Request.CreateResponse(HttpStatusCode.NoContent);
   return response;        
  }

我们只需使用存储库删除给定的帖子,并向客户端返回一个空响应。不需要指定特定的标题。我们使用203 No Content HTTP 状态来指定响应没有正文。

自定义操作

到目前为止,我们已经看到了执行 CRUD 操作的五个经典操作,每个方法都以 HTTP 方法名开始。如果我们需要其他方法呢?

我们可以使用动作属性来指定与请求匹配的 HTTP 方法。

考虑以下示例:我们希望添加一个操作来使用日期查看帖子,我们希望有一组像这样的 URIs:

表 4:网址和描述

| /API/post/archive/2010/04/11 | 返回 2010 年 4 月 11 日的所有帖子 | | /API/post/archive/2010/04 | 返回 2010 年 4 月的所有帖子 | | /API/post/archive/2010 | 返回 2010 年的所有帖子 |

我们首先需要为归档操作定义一个新的路径:

  config.Routes.MapHttpRoute(
      name: "Archive",
      routeTemplate: "api/posts/archive/{year}/{month}/{day}",
      defaults: new { 
                       controller
  = "Posts", 
                       month
  = RouteParameter.Optional, 
                       day
  = RouteParameter.Optional 
                    },
      constraints: new { month = @"\d{0,2}", day = @"\d{0,2}"}
  );

这段代码用三个参数定义了一条新路线:年、月和日。分配给PostsController,月和日必须是数字。

有了这条路线,我们可以在PostsController类中定义一个响应这条路线的新方法:

  [HttpGet]
  public IQueryable<Post> Archive(int year, int month =
  0, int day = 0)
  {
      return _repository.Search(year, month, year);
  }

该操作只需接受这三个参数,并使用存储库发出查询。请注意,月和日具有默认值,因为它们在请求中不是必需的。

这个动作用路由请求所需的HttpGetAttribute修饰,因为方法名不以Get开头。

| | 注意:另一个解决方案是将方法重命名为 GetArchive,这样它就符合默认约定。 |

模型绑定

在前面的例子中,我们看到了这样一个动作:

  public HttpResponseMessage Post(Post post)
  {
    /*
  ...some code here... */
  }

Post参数是如何建立的?

如果我们分析该请求,我们将看到如下内容:

  POST http://localhost:1085/api/posts
  HTTP/1.1
  Host: localhost:1085
  Connection: keep-alive
  Content-Length: 51
  Cache-Control: no-cache
  User-Agent: Mozilla/5.0 (Windows NT
  6.2; WOW64) AppleWebKit/537.22 (KHTML, like Gecko) Chrome/25.0.1364.152
  Safari/537.22
  Content-Type:
  application/x-www-form-urlencoded
  Accept: */*
  Accept-Encoding: gzip,deflate,sdch
  Accept-Language:
  en-US,en;q=0.8,it;q=0.6
  Accept-Charset:
  ISO-8859-1,utf-8;q=0.7,*;q=0.3

  Title=Hello+again&Date=2013-04-20&Body=hi+everybody!

我们可以在 post 请求的末尾看到值的集合(请求的主体):

  Title=Hello+again&Date=2013-04-20&Body=hi+everybody!

参数的所谓模型绑定器将该实体转换为Post对象。模型绑定器的任务是解析请求并从中提取操作所需的值。默认情况下,有两个模型绑定器以不同的方式运行:ModelBinderParameterBindingFormatterParameterBinding

这些类继承自抽象类HttpParameterBinding,并在方法ExecuteBindingAsync中实现它们的行为。ModelBinderParameterBinding是首选方法,因为它从 URI(路线参数或查询字符串)中提取值,而FormatterParameterBinding使用MediaTypeFormatter(在下一章中有更多关于这方面的内容)来解析主体和构建复杂对象。

班级ModelBinderParameterBinding里面是ValueProviders。这些对象从传入请求的各种元素(标头、查询字符串、正文等)中聚合值。ModelBinder使用这些ValueProviders来构建模型。

为了更好地理解这一切是如何工作的,让我们看一个例子。假设我们需要从头部提取一个动作参数。为此,我们需要实现一个IValueProvider:

  public class HeaderValueProvider : IValueProvider
  {
      public HttpRequestHeaders Headers { get; set; }
      public HeaderValueProvider(HttpRequestHeaders headers)
      {
          Headers =
  headers;
      }

      public bool ContainsPrefix(string prefix)
      {
          return Headers.Any(s => s.Key.StartsWith(prefix));
      }

      public ValueProviderResult GetValue(string key)
      {
          KeyValuePair<string, IEnumerable<string>> header = Headers.FirstOrDefault(s =>
  s.Key.StartsWith(key));
          string headerValue = string.Join(",", header.Value);
          return new ValueProviderResult(headerValue, headerValue, CultureInfo.InvariantCulture);
      }
  }

这个类有两个值得一提的方法。调用ContainsPrefix方法来验证头部包含我们需要的信息,其中前缀字符串通常是动作参数的名称。调用GetValue方法从报头中提取信息,并以ValueProviderResult的形式返回。

要将此提供程序链接到我们的操作,我们需要使用一个属性:

  public HttpResponseMessage Post([ValueProvider(typeof(HeaderValueFactory))] String username)
  {
    //...
  }

既然ValueProviderAttribute需要一个ValueProviderFactory,我们的最后一个任务就是实施一个工厂来建设我们的HeaderValueProvider:

  public class HeaderValueFactory : ValueProviderFactory
  {
      public override IValueProvider GetValueProvider(HttpActionContext actionContext)
      {
          return new HeaderValueProvider(actionContext.Request.Headers);
      }
  }

这就完成了构建从标头中提取值的绑定器所需的类。正如我们所看到的,这并不容易,它需要很多任务;幸运的是,默认绑定通常足够聪明,可以反序列化请求并为您的操作创建正确的参数。

请记住,默认情况下,对于基本类型和DateTimeTimeSpanGuidDecimalString,ASP.NET 网络应用编程接口使用模型绑定并从 URI 提取值。如果需要,我们可以使用属性FromBody覆盖默认行为,以指定值来自主体:

  public HttpResponseMessage Post([FromBody] String username)

在其他特殊情况下,我们可以使用ModelBinder属性来指定我们需要ModelBinder的特定实现:

  public HttpResponseMessage Post([ModelBinder(typeof(UserModelBinder))] User username)

通过ModelBinder属性,我们可以指定构建User参数的IModelBinder的实现:

  public interface IModelBinder
  {
      object BindModel(ControllerContext cc, ModelBindingContext mbc);
  }

构建User实例的方法BindModel将被传递给Post动作。

让我们看看参数与我们的行动之间的不同联系。默认情况下,ASP.NET 网络应用编程接口试图从查询字符串或路由参数中获取简单的值。例如,如果请求网址是这样的:

/?tag=web&date=20130409

tagdate均取自 URI,因此无需指定自定义绑定或属性。

如果我们强制使用参数的属性,那么参数将从主体中读取:

void Action([FromBody] string name);

相反,如果我们不指定其他属性,并且我们有一个包含一个简单类型和一个复杂类型的操作,则第一个从 URI 读取,而复杂类型将从正文中获取:

void Action(int id, Post p)

id取自 URI,而Post取自身体,因为它是一个复杂的类型。

如果我们有一个动作需要两个复杂的类型,一个必须来自身体,而另一个必须来自另一个来源:

void Action([FromUri] Customer c1, Customer c2)

c1来自 URI,c2来自身体

在任何情况下,我们都可以定义一个自定义模型绑定器,并使用一个属性来指定我们想要使用它:

void Action([ModelBinder(PostCustomBinder)] Post p)

PostCustomModelBinder将用于构建帖子实例。

| | 注意:需要记住的一件重要的事情是,Web API 最多读取一次响应体,所以一个动作只有一个参数可以来自请求体。如果需要从请求正文中获取多个值,请定义一个复杂类型。如果你的动作需要多个复杂类型,那么应该只有一个来自身体。 |

这意味着这个动作:

    public HttpResponseMessage Post(Post post, User user)

InvalidOperationException : Can't bind multiple parameters ('post' and 'user') to the request's content.`` 将会升起一个

为了确保此操作有效,我们需要使用一个属性来指定Post(或User)来自 URI:

  public HttpResponseMessage Post(Post
  post, [FromUri]User user)

FromUri属性强制用户从 URI 而不是身体进行解析。

总结

控制器是 ASP.NET 网络应用编程接口的核心。这是程序员花费大部分时间的地方,也是输入、模型和服务之间合作的地方。

在本章中,我们看到了什么是控制器,以及它公开了什么类型的操作,从默认操作到自定义操作。然后我们看到了模型绑定如何将请求数据转换成动作参数。