UPDATE:
I've recently worked on HTTP API for one of our products. As it was a bit of time pressure, we've decided to use ServiceStack - an open source web services framework. If you didn't hear about it go ahead and read. It is simple yet very powerful, well written framework which just works and makes things done without friction. And it exists for years already (if you would ask why not we were using Web API, which was not yet released at the moment).
I'd like to share some experience gained during my work. So here is the context. The first consumer of API will be our another project having back-end written in C# as well. ServiceStack allows to provide clients with strongly-typed assembly reusing the same requests/responses dtos used to build service (note that no code generation is needed). In fact this is recommended approach for C# clients.
The preferred way to call web service is to utilize REST endpoints, which can be configured using RestService attribute per each request DTO. Example below (here is a gist) shows how we can call services via REST endpoints, and how DTOs are reused.
Notice that dtos follow naming convention - response classes named exactly like request classes but with Response prefix. This simplifies clients life, as it becomes obvious what response is expected. It also allows to generate services metadata automatically.
However, there are several things that bothers me here. While I still want clients to call services via REST endpoints, I'd like to have more generic and easy to follow api. Here are things that come to mind:
As side effect of achieving this, we'll get another important benefit - ability to change Urls and even Http methods without breaking clients code. And this is essential for me - I'm sure I would like to change some Urls while it used by our other product only (but before it goes public).
So this is a method I'd like to have:
IRestClient.Send<TResponse>(IRequest<TResponse> request)
where IRequest - is just a marker interface applied to all request types, thus allowing to determine corresponding response type at compile time. And this is how our example will looks like (here is the gist):
That being said, I've implemented such extension method for IRestService interface. You can find source code with a test in a gist at github. There are several things missed there, but they should not be hard to implement:
This post is no longer relevant, because these ideas were included into the heart of the ServiceStack New API. It just a pleasure to use such a great framework!
I've recently worked on HTTP API for one of our products. As it was a bit of time pressure, we've decided to use ServiceStack - an open source web services framework. If you didn't hear about it go ahead and read. It is simple yet very powerful, well written framework which just works and makes things done without friction. And it exists for years already (if you would ask why not we were using Web API, which was not yet released at the moment).
I'd like to share some experience gained during my work. So here is the context. The first consumer of API will be our another project having back-end written in C# as well. ServiceStack allows to provide clients with strongly-typed assembly reusing the same requests/responses dtos used to build service (note that no code generation is needed). In fact this is recommended approach for C# clients.
The preferred way to call web service is to utilize REST endpoints, which can be configured using RestService attribute per each request DTO. Example below (here is a gist) shows how we can call services via REST endpoints, and how DTOs are reused.
/// DTOs [RestService("/orders/{id}", "GET")] [RestService("/orders/by-code/{code}", "GET")] [RestService("/orders/search", "GET")] public class GetOrders { public int? Id { get; set; } public string Code { get; set; } public string Name { get; set; } public string Customer { get; set; } } public class GetOrdersResponse { public Order[] Orders { get; set; } public ResponseStatus ResponseStatus { get; set; } } public class Order { public int Id { get; set; } public string Code { get; set; } public string Name { get; set; } public string Customer { get; set; } } [RestService("/orders", "POST")] // to create new order. [RestService("/orders/{id}", "PUT")] // to create or update existing order with specified Id. public class SaveOrder { public int? Id { get; set; } // Order details. } public class SaveOrderResponse { public int Id { get; set; } public ResponseStatus ResponseStatus { get; set; } } //// Rest endpoints usage IRestClient client = new JsonServiceClient(); var orderById = client.Get<GetOrdersResponse>("/orders/" + 5).Orders.Single(); var orderByCode = client.Get<GetOrdersResponse>("orders/by-code/" + Uri.EscapeDataString(orderById.Code)).Orders.Single(); var searchUrl = "orders/search?Name={0}&Customer={1}".Fmt( Uri.EscapeDataString(orderByCode.Name), Uri.EscapeDataString(orderByCode.Customer)); var foundOrders = client.Get<GetOrdersResponse>(searchUrl).Orders; var createOrderResponse = client.Post<SaveOrderResonse>("/orders", new SaveOrder { /* Order details */}); int orderId = createOrderResponse.Id; var updateOrderResponse = client.Put<SaveOrderResonse>("/orders/" + orderId, new SaveOrder { /* Order details */ });
Notice that dtos follow naming convention - response classes named exactly like request classes but with Response prefix. This simplifies clients life, as it becomes obvious what response is expected. It also allows to generate services metadata automatically.
However, there are several things that bothers me here. While I still want clients to call services via REST endpoints, I'd like to have more generic and easy to follow api. Here are things that come to mind:
- Whether we really need to specify Response type, while we already know what kind of request we send. Couldn't we automatically determine it by the request type?
- We already know urls available for given request type (from RestService attributes). It would nice if we can simplify developers life by picking and populating them automatically based on request state.
- We can go further and automatically determine required HTTP method.
- And last - for GET and DELETE request we could send additional request properties (not mapped in url template) as query parameters.
As side effect of achieving this, we'll get another important benefit - ability to change Urls and even Http methods without breaking clients code. And this is essential for me - I'm sure I would like to change some Urls while it used by our other product only (but before it goes public).
So this is a method I'd like to have:
IRestClient.Send<TResponse>(IRequest<TResponse> request)
where IRequest - is just a marker interface applied to all request types, thus allowing to determine corresponding response type at compile time. And this is how our example will looks like (here is the gist):
// Response types omitted. // Note that request types now marked with IRequest<tresponse> interface. // This allows Send method to determine response type at compile time. [RestService("/orders/{id}", "GET")] [RestService("/orders/by-code/{code}", "GET")] [RestService("/orders/search", "GET")] public class GetOrders : IRequest<GetOrdersResponse> { public int? Id { get; set; } public string Code { get; set; } public string Name { get; set; } public string Customer { get; set; } } [RestService("/orders", "POST")] // to create new order. [RestService("/orders/{id}", "PUT")] // to create or update existing order with specified Id. public class SaveOrder : IRequest<SaveOrderResonse> { public int? Id { get; set; } // Order details. } //// Proposed interface // Marker interface allowing to determine response type at compile time. public interface IRequest<TResponse> { } public static class RestServiceExtensions { public static TResponse Send<TResponse>(this IRestClient client, IRequest<TResponse> request) { // Determine matching REST endpoint for specified request (via RestService attributes). // Populate url with encoded variables and optional query parameters. // Invoke corresponding REST method with proper Http method // and return strongly typed response } } //// Service usage IRestClient client = new JsonServiceClient(); // We don't specify response type - it is determined automatically based on request type. // We don't specify URLs or HTTP verbs - they are determined based on request state. // We don't write boilerplate code to encode url parts. It is done automatically. // GET /orders/5 var orderById = client.Send(new GetOrders { Id = 5 }).Orders.Single(); // GET /orders/by-code/Code var orderByCode = client.Send(new GetOrders { Code = orderById.Code }).Orders.Single(); // GET /orders/search?Name=Name&Customer=Customer var getOrders = new GetOrders { Name = orderById.Name, Customer = orderByCode.Customer }; var foundOrders = client.Send(getOrders).Orders; // POST /orders var createOrderResponse = client.Send(new SaveOrder { /* Order details */}); // PUT /orders/id int orderId = createOrderResponse.Id; var updateOrderResponse = client.Send(new SaveOrder { Id = orderById.Id, /* Order details */ });
That being said, I've implemented such extension method for IRestService interface. You can find source code with a test in a gist at github. There are several things missed there, but they should not be hard to implement:
- Same extension for Async service.
- Special formatting for DateTime, List variables in url.
- Other opinionated decisions on how to choose url when several urls matches specified request.
Hope you find this useful. Anyway, please let me know event if it feels like completely wrong approach.
3 comments:
Very nice concepts that can be also applied to everyday life for making better APIs. Thanks Ivan!
Must be a great feeling to have an impact on such a great framework as ServiceStack! :)
Great conversation! Mr. Zachman has been telling the “Truth” about his Framework for over 40 years.
Arthur Schopenhauer said, “Every truth passes through three stages before it is accepted. In the first it is ridiculed; in the second it is opposed; and in the third it is regarded as self-evident”
I typically score the pipe with a few more wiggle-tighten-wiggle-tightens, etc.... at the initial phase. You boys just barely scored it and then cranked down on it. It worked (obvioulsy) but don't think it's best for the tools blade life or for getting the smoothest of cuts. ANYWAY, everyone's an expert, right!? LOL!!
Post a Comment