跳转至

ResourceController

ResourceController是一个controller,它为实现端点控制器提供了方便。一个 ResourceController必须被子类化,在这个子类中,你需要针对该资源类型上的每个操作编写一个方法。例如,一个UserController可以处理以下操作。

  • 创建新用户 (POST /users)
  • 获得所有用户(GET /users)
  • 获取个人用户 (GET /users/:id)
  • 更新个人用户 (PUT /users/:id)
  • 删除个人用户 (DELETE /users/:id)

这些为一个操作而调用的方法被称为操作方法

操作方法

操作方法是ResourceController子类的一个实例方法,它有一个@Operation注解。必须返回一个Future<Response>的实例。下面是一个例子:

class CityController extends ResourceController {
  @Operation.get()
  Future<Response> getAllCities() async {
    return Response.ok(["Atlanta", "Madison", "Mountain View"]);
  }
}

CityController处理没有路径变量的GET请求时,上述操作方法将被调用。为了处理带路径变量的操作方法,路径变量的名称被添加到@Operation注解中。

class CityController extends ResourceController {
  @Operation.get()
  Future<Response> getAllCities() async {
    return Response.ok(["Atlanta", "Madison", "Mountain View"]);
  }

  @Operation.get('name')
  Future<Response> getCityByName() async {
    final id = request.path.variables['name'];
    return Response.ok(fetchCityWithName(name));
  }
}

Warning

这个控制器将被链接到路由规范/cities/[:name],所以它可以处理这两个操作。请阅读Routing中的路径变量。

名为Operation的构造函数告诉我们该操作方法处理哪种HTTP方法。 存在以下命名构造函数:

  • Operation.post()
  • Operation.get()
  • Operation.put()
  • Operation.delete()

规范的Operation()构造函数将HTTP方法作为非标准操作的第一个参数,例如:

@Operation('PATCH', 'id')
Future<Response> patchObjectWithID() async => ...;

所有Operation构造函数都采用路径变量的变量列表。 一个操作可以有多个路径变量。 仅当操作路径的所有路径变量都存在于请求路径中时,才会调用该操作方法。 给定的HTTP方法可以有多种操作方法,只要每种方法都需要一组不同的路径变量即可。

这是一个需要两个路径变量的操作示例:

@Operation.get('userID', 'itemID')
Future<Response> getUserItem() async {
  final userID = request.path.variables['userID'];
  final itemID = request.path.variables['itemID'];
  return Response.ok(...);
}

如果不存在用于请求的操作方法,则会自动发送405 Method Not Allowed响应,并且不会调用任何操作方法。

路由到ResourceController

在应用程序通道中,ResourceController子类必须在Router之前。Router将解析路径变量,以便控制器可以使用它们来确定应调用哪种操作方法。 到ResourceController的典型路由包含一个可选的标识路径变量:

router
  .route("/cities/[:name]")
  .link(() => CityController());

该路由将允许CityController为所有没有路径变量和name路径变量的HTTP方法实现操作方法。

将子资源分解成自己的控制器被认为是一种很好的做法。例如,以下是首选的方法:

router
  .route("/cities/[:name]")
  .link(() => CityController());

router
  .route("/cities/:name/attractions/[:id]")
  .link(() => CityAttractionController());

相比之下,路由/cities/[:name/[attractions/[:id]]]有效时,会使控制器逻辑变得更加笨拙。

请求绑定

操作方法可以将HTTP请求的属性绑定到其参数。 调用操作方法时,该属性的值将作为参数传递给操作方法。 例如,以下代码将名为X-API-Key的标头绑定到参数apiKey

@Operation.get('name')
Future<Response> getCityByName(@Bind.header('x-api-key') String apiKey) async {
  if (!isValid(apiKey)) {
    return Response.unauthorized();
  }

  return Response.ok(...);
}

下表显示了可能的绑定类型:

Property Binding
Path Variable @Bind.path(pathVariableName)
URL Query Parameter @Bind.query(queryParameterName)
Header @Bind.header(headerName)
Request Body @Bind.body()

你可以将任意数量的HTTP请求属性绑定到一个操作方法。

可选绑定

Bindings can be made optional. If a binding is optional, the operation method will still be called even if the binding isn't in a request. To make a binding optional, wrap it in curly brackets.

@Operation.get()
Future<Response> getAllCities({@Bind.header('x-api-key') String apiKey}) async {
  if (apiKey == null) {
    // No X-API-Key in request
    ...
  }
  ...
}

The curly bracket syntax is a Dart language feature for optional method parameters (see more here). If there are multiple optional parameters, use only one pair of curly brackets and list each optional parameter in those brackets.

A bound parameter will be null if is not present in the request. You can provide a default value for optional parameters.

@Operation.get()
Future<Response> getAllCities({@Bind.header("x-api-key") String apiKey: "public"}) async {
  ...
}

自动解析绑定

Query, header and path bindings can automatically be parsed into other types, such as int or DateTime. Simply declare the bound parameter's type to the desired type:

Future<Response> getCityByID(@Bind.query('id') int cityID)

The type of a bound parameter may be String or any type that implements parse (e.g., int, DateTime). Query parameters may also be bound to bool parameters; a boolean query parameter will be true if the query parameter has no value (e.g. /path?boolean).

If parsing fails for any reason, an error response is sent and the operation method is not called. For example, the above example binds int cityID - if the path variable 'id' can't be parsed into an int, a 404 Not Found response is sent. If a query parameter or header value cannot be parsed, a 400 Bad Request response is sent.

You may also bind List<T> parameters to headers and query parameters, where T must meet the same criteria as above. Query parameters and headers may appear more than once in a request. For example, the value of ids is [1, 2] if the request URL ends with /path?id=1&id=2 and the operation method looks like this:

Future<Response> getCitiesByIDs(@Bind.query('id') List<int> ids)

Note that if a parameter is not bound to a list and there are multiple occurrences of that property in the request, a 400 Bad Request response will be sent. If you want to allow multiple values, you must bind to a List<T>.

标头绑定

The following operation method binds the header named X-API-Key to the apiKey parameter:

class CityController extends ResourceController {
  @Operation.get()
  Future<Response> getAllCities(@Bind.header('x-api-key') String apiKey) async {
    if (!isValid(apiKey)) {
      return Response.unauthorized();
    }

    return Response.ok(['Atlanta', 'Madison', 'Mountain View']);
  }
}

If an X-API-Key header is present in the request, its value will be available in apiKey. If it is not, getAllCities(apiKey) would not be called and a 400 Bad Request response will be sent. If apiKey were optional, the method is called as normal and apiKey is null or a default value.

Header names are case-insensitive per the HTTP specification. Therefore, the header name may be 'X-API-KEY', 'X-Api-Key' 'x-api-key', etc. and apiKey will be bound in all cases.

查询参数绑定

The following operation methods binds the query parameter named 'name' to the parameter cityName:

class CityController extends ResourceController {
  @Operation.get()
  Future<Response> getAllCities(@Bind.query('name') String cityName) async {
    return Response.ok(cities.where((c) => c.name == cityName).toList());
  }
}

Query parameters can be required or optional. If required, a 400 Bad Request response is sent and no operation method is called if the query parameter is not present in the request URL. If optional, the bound variable is null or a default value.

Query parameters are case-sensitive; this binding will only match the query parameter 'name', but not 'Name' or 'NAME'.

Query parameters may also bound for query strings in the request body when the content-type is 'application/x-www-form-urlencoded'.

路径变量绑定

The following operation method binds the path variable 'id' to the parameter cityID:

class CityController extends ResourceController {
  @Operation.get('id')
  Future<Response> getCityByID(@Bind.path('id') String cityID) async {
    return Response.ok(cities.where((c) => c.id == cityID).toList());
  }
}

Path variables are made available when creating routes. A Router must have a route that includes a path variable and that path variable must be listed in the Operation annotation. Path variables are case-sensitive and may not be optional.

If you attempt to bind a path variable that is not present in the Operation, you will get a runtime exception at startup. You do not have to bind path variables for an operation method to be invoked.

HTTP请求体绑定

The body of an HTTP request can also be bound to a parameter:

class CityController extends ResourceController {
  CityController(this.context);

  final ManagedContext context;

  @Operation.post()
  Future<Response> addCity(@Bind.body() City city) async {
    final insertedCity = await context.insertObject(city);

    return Response.ok(insertedCity);
  }
}

Since there is only one request body, Bind.body() doesn't take any identifying arguments (however, it does take optional arguments for ignoring, requiring or rejecting keys; this matches the behavior of Serializable.read and only works when the bound type is a Serializable or list of).

The bound parameter type (City in this example) must implement Serializable. Aqueduct will automatically decode the request body from it's content-type, create a new instance of the bound parameter type, and invoke its read method. In the above example, a valid request body would be the following JSON:

{
  "id": 1,
  "name": "Atlanta"
}

HTTP正文解码

Request bodies are decoded according to their content-type prior to being deserialized. For more information on request body decoding, including decoding content-types other than JSON, see this guide.

If parsing fails or read throws an exception, a 400 Bad Request response will be sent and the operation method won't be called.

You may also bind List<Serializable> parameters to the request body. Consider the following JSON that contains a list of cities:

[
  {"id": 1, "name": "Atlanta"},
  {"id": 2, "name": "Madison"}
]

This body can be bound by declaring the bound parameter to be a List of the desired type:

Future<Response> addCity(@Bind.body() List<City> cities)

List vs Object

An endpoint should either take a single object or a list of objects, but not both. If the request body is a JSON list and the bound variable is not a list, a 400 Bad Request response will be sent (and vice versa). Declaring a body binding of the appropriate type validates the expected value and aids in automatically generating an OpenAPI specification for your application.

Note that if the request's Content-Type is 'x-www-form-urlencoded', its must be bound with Bind.query and not Bind.body.

Key Filters in Bind.body()

Filters can be applied to keys of the object being read. Filters can ignore keys, require keys or throw an error if a key is found. See more here.

属性绑定

The properties of an ResourceControllers may also have Bind.query and Bind.header metadata. This binds values from the request to the ResourceController instance itself, making them accessible from all operation methods.

class CityController extends ResourceController {
  @requiredBinding
  @Bind.header("x-timestamp")
  DateTime timestamp;

  @Bind.query("limit")
  int limit;

  @Operation.get()
  Future<Response> getCities() async {
      // can use both limit and timestamp
  }
}

In the above, both timestamp and limit are bound prior to getCities being invoked. By default, a bound property is optional. Adding an requiredBinding annotation changes a property to required. If required, any request without the required property fails with a 400 Bad Request status code and none of the operation methods are invoked.

其他ResourceController行为

Besides binding, ResourceControllers have some other behavior that is important to understand.

请求体和响应体

ResourceController可以限制它接受的HTTP请求体的内容类型。默认情况下,ResourceController将只接受POSTPUT方法的application/json请求体。这可以通过设置构造函数中的 acceptedContentTypes属性进行修改。

class UserController extends ResourceController {
  UserController() {
    acceptedContentTypes = [ContentType.JSON, ContentType.XML];
  }
}

If a request is made with a content type other than the accepted content types, the controller automatically responds with a 415 Unsupported Media Type response.

The body of an HTTP request is decoded if the content type is accepted and there exists a operation method to handle the request. The body is not decoded if there is not a matching operation method for the request. The body is decoded by ResourceController prior to your operation method being invoked. Therefore, you can always use the synchronous RequestBody.as method to access the body from within an operation method:

@Operation.post()
Future<Response> createThing() async {
  // do this:
  Map<String, dynamic> bodyMap = request.body.as();

  // no need to do this:
  Map<String, dynamic> bodyMap = await request.body.decode();

  return ...;
}

A ResourceController can also have a default content type for its responses. By default, this is application/json. This default can be changed by changing responseContentType in the constructor:

class UserController extends ResourceController {
  UserController() {
    responseContentType = ContentType.XML;
  }
}

The responseContentType is the default response content type. An individual Response may set its own contentType, which takes precedence over the responseContentType. For example, the following controller returns JSON by default, but if the request specifically asks for XML, that's what it will return:

class UserController extends ResourceController {
  UserController() {
    responseContentType = ContentType.JSON;
  }

  @Operation.get('id')
  Future<Response> getUserByID(@Bind.path('id') int id) async {
    var response = Response.ok(...);

    if (request.headers.value(Bind.headers.ACCEPT).startsWith("application/xml")) {
      response.contentType = ContentType.XML;
    }

    return response;
  }
}

更多专业ResourceControllers

许多ResourceController子类将执行queries。 有一些有用的ResourceController子类可以减少样板代码。

A QueryController<T> builds a Query<T> based on the incoming request. If the request has a body, this Query<T>'s values property is read from that body. If the request has a path variable, the Query<T> assigns an expression to the primary key value. For example, in a normal ResourceController that responds to a PUT request, you might write the following:

@Operation.put('id')
Future<Response> updateUser(@Bind.path('id') int id, @Bind.body() User user) async {
  var query = Query<User>(context)
    ..where((u) => u.id).equalTo(id)
    ..values = user;

  return Response.ok(await query.updateOne());
}

A QueryController<T> builds this query before a operation method is invoked, storing it in the inherited query property. A ManagedObject<T> subclass is the type argument to QueryController<T>.

class UserController extends QueryController<User> {
  UserController(ManagedContext context) : super(context);

  @Operation.put('id')
  Future<Response> updateUser(@Bind.path('id') int id) async {
    // query already exists and is identical to the snippet above
    var result = await query.updateOne();
    return Response.ok(result);
  }
}

A ManagedObjectController<T> is significantly more powerful; you don't even need to subclass it. It does all the things a CRUD endpoint does without any code. Here's an example usage:

router
  .route("/users/[:id]")
  .link(() => ManagedObjectController<User>(context));

This controller has the following behavior:

Request Action
POST /users Inserts a user into the database with values from the request body
GET /users Fetches all users in the database
GET /users/:id Fetches a single user by id
DELETE /users/:id Deletes a single user by id
PUT /users/:id Updated a single user by id, using values from the request body

The objects returned from getting the collection - e.g, GET /users - can be modified with query parameters. For example, the following request will return the users sorted by their name in ascending order:

GET /users?sortBy=name,asc

The results can be paged (see Paging in Advanced Queries) with query parameters offset, count, pageBy, pageAfter and pagePrior.

A ManagedObjectController<T> can also be subclassed. A subclass allows for callbacks to be overridden to adjust the query before execution, or the results before sending the respond. Each operation - fetch, update, delete, etc. - has a pair of methods to do this. For example, the following subclass alters the query and results before any update via PUT:

class UserController extends ManagedObjectController<User> {
  UserController(ManagedContext context) : super(context);

  Future<Query<User>> willUpdateObjectWithQuery(
      Query<User> query) async {
    query.values.lastUpdatedAt = DateTime.now().toUtc();
    return query;
  }

  Future<Response> didUpdateObject(User object) async {
    object.removePropertyFromBackingMap("private");
    return Response.ok(object);
  }
}

See the chapter on validations, which are powerful when combined with ManagedObjectController<T>.