More ServiceStack request DTO advice

2020-03-26 11:34发布

问题:

This is a follow up regarding:

ServiceStack Request DTO design

In the above question the design was strictly regarding read operations. What about write operations? Say we wanted to add operations for creating a new booking limit, would reusing the noun be appropriate here?

[Route("/bookinglimits/","POST")]
public class CreateBookingLimit : IReturn<BookingLimit>
{      
     BookingLimit newBookingLimit
}

-OR- Would this be better design?

[Route("/bookinglimits/","POST")]
public class CreateBookingLimit : IReturn<BookingLimit>
{      
  public int ShiftId { get; set; }
  public DateTime StartDate { get; set; }
  public DateTime EndDate { get; set; }
  public int Limit { get; set; }    }
}

Also, if we wanted to add editing--should we have insert and edit share the same models and add the ID?

[Route("/bookinglimits/","POST")]
[Route("/bookinglimits/{Id}/","PUT")]
public class CreateBookingLimit : IReturn<BookingLimit>
{      
  public int Id { get; set; }
  public int ShiftId { get; set; }
  public DateTime StartDate { get; set; }
  public DateTime EndDate { get; set; }
  public int Limit { get; set; }    }
}

I'm trying to wrap my head around when it makes the most sense to reuse POCOs and when it makes more sense to separate intentions.

回答1:

Message-based API Design

There are a few things to bear in mind when designing the ideal message-based API where your Services effectively end up serving 2 masters: a Native Client API and a REST API. Native Clients just send and receive messages in their original form so they get a natural API for free modelled using C# Request and Response DTOs to capture what information is required for the Service to perform its Operation and what it should return.

Projecting messages into the ideal HTTP API

After designing your message-based API you'll then want to focus on how best to project the messages into a REST API by annotating Request DTOs with [Route] Attributes to define the Custom endpoints for your Services.

This previous answer on Designing a REST-ful service with ServiceStack provides examples on which routes different Request DTOs map to, in general you'll want to design your APIs around Resources where each operation "acts on a Resource" which will make defining your Custom Routes easier. The ideal HTTP API for Creating and Updating a Booking Limit would look like:

POST /bookinglimits       (Create Booking Limit)
PUT  /bookinglimits/{id}  (Update Booking Limit)

General recommendations on good API Design

Whilst not specifically about Web Services this article on Ten Rules for Good API Design provides good recommendations on general (Code or Services) API design. As API Consumers are the intended audience of your APIs who'll primarily be deriving the most value from them, their design should be optimized so that they're self-descriptive, use consistent naming, are intuitive to use and can be evolved without breaking existing clients. Messages are naturally suited to versioning but you still need to be mindful when making changes to existing published APIs that any additional properties are optional with default fallback behavior if required.

For this reason whilst you can save some code by returning a naked BookingLimit, my preference is to instead return a specific Response DTO for each Service which allows the Service to return additional metadata without breaking existing clients whilst maintaining a consistent Request/Response pattern for all Services. Although this is just my preference - returning naked types is also fine.

ServiceStack Implementation

To implement this in ServiceStack I wouldn't use the same Request DTO to support multiple verbs. Since the Request DTO is called Create* that conveys that users should only send this Request DTO to Create Booking limits which is typically done using a POST request, e.g:

[Route("/bookinglimits", "POST")]
public class CreateBookingLimit : IReturn<CreateBookingLimitResponse>, IPost
{      
  public int ShiftId { get; set; }
  public DateTime StartDate { get; set; }
  public DateTime EndDate { get; set; }
  public int Limit { get; set; } 
}

public class CreateBookingLimitResponse
{
    public BookingLimit Result { get; set; }
    public ResponseStatus ResponseStatus { get; set; }
}

The IPut, IPost are Verb interface markers which lets both the User and Service Client know which Verb this message should be sent with which makes it possible to have all messages sent in a single Service Gateway method.

If your Service also supports updating a Booking Limit then I'd create a separate Service for it which would look like:

[Route("/bookinglimits/{Id}", "PUT")]
public class UpdateBookingLimit : IReturn<UpdateBookingLimitResponse>, IPut
{      
  public int Id { get; set; }
  public int ShiftId { get; set; }
  public DateTime StartDate { get; set; }
  public DateTime EndDate { get; set; }
  public int Limit { get; set; } 
}

public class UpdateBookingLimitResponse
{
    public BookingLimit Result { get; set; }
    public ResponseStatus ResponseStatus { get; set; }
}

By using separate Operations you can ensure Request DTOs contains only the properties relevant to that operation, reducing the confusion for API consumers.

If it makes sense for your Service, e.g. the schemas for both operations remains the same I'll merge both Create/Update operations into a single Operation. When you do this you should use a consistent Verb that indicates when an operation does both, e.g. Store* or CreateOrUpdate*:

[Route("/bookinglimits", "POST")]
public class StoreBookingLimit : IReturn<StoreBookingLimitResponse>, IPost
{      
  public int Id { get; set; }
  public int ShiftId { get; set; }
  public DateTime StartDate { get; set; }
  public DateTime EndDate { get; set; }
  public int Limit { get; set; } 
}

public class StoreBookingLimitResponse
{
    public BookingLimit Result { get; set; }
    public ResponseStatus ResponseStatus { get; set; }
}

In most cases where the Server generates the Id for the Resource you should use POST, in the rare case where the client specifies the Id, e.g. Slug or Guid you can use PUT which roughly translates to "PUT this resource at this location" which is possible when the client knows the url for the resource.

Message based API examples

Most of the time what messages should contain will be obvious based on the Service requirements and becomes intuitive and natural to create over time. For examples on a comprehensive message-based API you can have a look AWS Web Services who've effectively servicified their Web Services behind a message-based design that uses Service Clients to send messages to access all their APIs, e.g. AWS DynamoDB API Reference lists each Actions that's available as well as other DTO Types that the Services return, e.g here are DynamoDB APIs they have around Creating / Modifying and Querying Items:

Actions

  • BatchGetItem
  • BatchWriteItem
  • DeleteItem
  • GetItem
  • PutItem
  • Query
  • Scan
  • UpdateItem

Data Types

  • AttributeDefinition
  • AttributeValue
  • AttributeValueUpdate
  • Condition
  • ...

In ServiceStack Actions are called Operations and what you'll use Request DTOs to define, whilst AWS Data Types are just called DTOs which I keep in a Types namespace to differentiate from Operations.

DynamoDb.ServiceModel (project)

/GetItem
/PutItem
/UpdateItem
/DeleteItem
/Query
/Scan

/Types 
  /AttributeDefinition
  /AttributeValue
  /AttributeValueUpdate

You typically wouldn't need additional explicit Services for Batch Requests as you can get that for free using ServiceStack's Auto Batched Requests. ServiceStack also includes a number of other benefits where it's able to generate richer DTOs containing Custom Attributes and interfaces in the Source DTOs to enable a richer and succinct end-to-end typed API requiring less boilerplate and generated code that lets you use the same Generic Service Client to call any ServiceStack Service offering both Sync and idiomatic Async APIs. The additional metadata also enables seamless higher-level functionality like Encrypted Messaging, Cache Aware Clients, Multiple Formats, Service Gateway, HTTP Verb Interface Markers, etc.

Otherwise AWS follows a very similar approach to ServiceStack for designing message-based APIs using generic Service Clients to send DTOs native in each language.