8

I have a service stack service we'll call Orders that has the standard GET routes

  • /orders - Gets All Customers
  • /orders/{Ids} - Gets Specific customers

This works all fine and dandy, but I thought I'd add another route

  • /orders/customers/{CustomerId} -Gets orders with a specific customer id

This works find when hitting the routes in the browser, but when I use the ServiceStack Client I get ambiguous routes exception, and it lists the three routes.

I'm not quite sure what the best way around this is..is what I'm doing not the correct RESTish way to it?

I know I can simply manual enter the routes into the JsonServiceClient like

client.Get<List<Orders>>("/orders/customers/7")

and that will work, but I would prefer to do it the typed way...i.e

client.Get(new OrdersRequest { CustomerId = 7 });

Here's an example RequestDTO i'm using

public class OrdersRequest : IReturn<List<Orders>>
{
   public int[] Ids {get; set;}
   public CustomerId {get; set;}

   public OrdersRequest(params int[] ids)
   {
        this.Ids = ids;
   }
}

Do I have to use different Dtos for this or...?

any help or pointers to any of the samples that have a way around this, or a better way to create the services would be appreciated.

Thanks

Kyle Gobel
  • 5,530
  • 9
  • 45
  • 68
  • question: As it is (probably not the right routes), if you delete the constructor with parameter the array, it will work or not ? – stefan2410 Dec 15 '13 at 16:34
  • Also, I don't think that having a list of ids within the Uri itself, is acceptable. Maybe the route should be /orders (Get Orders of all Customers) and in Url parameters orders/?ids=1&ids=2&ids=3 @Scott is right in how to create the best routes. – stefan2410 Dec 15 '13 at 16:53

1 Answers1

7

My advise is you're not using REST properly. There is a good answer about how to best structure a ServiceStack REST service. It's a common issue when starting out, I had issues like this too.

Understanding your use case:

In your specific case if we look at /orders/customers/7 this would work better is you think of it this way:

/customers/7/orders

The reason you do it this way:

  • It resolves your route ambiguity problems
  • The context is clear. We can see we are working with Customer 7 without navigating a long url
  • We can see 'with customer 7' we want their orders

Think of routing like this:

/orders                   Everybody's Orders
/orders/1                 Order 1 (without being aware of the customer it belongs to)
/customers                All Customers
/customers/7              Customer 7's details
/customers/7/orders       All Customer 7's orders
/customers/7/orders/3     Customer 7's order number 3

The beauty of doing things like this is operations on data are done consistently. So you want to find all cancelled orders:

/orders/cancelled

You want to cancel a specific order by it's orderId

/orders/4/cancel

You want to list a specific customer's open orders

/customers/6/orders/open

You want to list customer 6's cancelled orders

/customers/6/orders/cancelled

You want to cancel an order for customer 6 that you are viewing

/customers/6/orders/2/cancel

Obviously these are just scenarios, your routes will differ.

Simplifying action handlers:

You will probably want to define your action handlers so they can cope with coming from multiple routes. What I mean is, one action handler would be responsible for Listing Orders

/orders
/customers/6/orders

What I do is this:

[Route("/orders","GET")]
[Route("/customers/{CustomerId}/orders","GET")]
public class ListOrdersRequest : IReturn<List<Order>>
{
    public int? CustomerId { get; set; }
}

So you can see the two order listing routes come in. If they use /orders then our customerId won't have a value but the other route will. So in our action handler we can simply test for this:

public List<Order> Get(ListOrdersRequest request)
{
    // Use `CustomerId` to narrow down your search scope

    if(request.CustomerId.HasValue){
        // Find customer specific orders
    } else {
        // Find all orders
    }

    return orders;
}

Client-side concerns:

So to address your using 'typed' client concerns. Creating the routes and action handlers using the above method will allow you to do:

client.Get(new ListOrdersRequest { CustomerId = 7 }); // To get customer 7's orders
client.Get(new ListOrdersRequest()); // All orders

Thus giving you 1 clear method for Listing Orders.

Final thoughts:

Obviously it means you will have to rework what you have which will probably be a pain, but it's the best approach, well worth the effort.

I hope this helps you understand the best structure.


Update:

@stefan2410 requested that I address the issue of Kyle's use of an int[] in a route for a GET request:

If you want to use an int[] in a GET request you have to consider changing it during transport in the URL so it can be used RESTfully.

So you could do this:

class OrdersRequest
{
    public string Ids { get; set; }

    public OrdersRequest(int[] ids)
    {
        Ids = string.Join(",", Array.ConvertAll(ints, item => item.ToString()));
    }
}

client.Get(new OrdersRequest(new [] {1,2,3}));

Creates the route /orders/1,2,3 and matches /orders/{Ids}. The problem with doing this is in order to use the Ids at the server side you have to convert the string "1,2,3" back to an int[].

The better way to deal with an int[] in a request is to use POST. So you can do:

class OrdersRequest
{
    public int[] Ids { get; set; }

    public OrdersRequest(int[] ids)
    {
        Ids = ids;
    }
}    

client.Post(new OrdersRequest(new[] {1,2,3}));

Creates the route /orders and matches the route /orders.

Community
  • 1
  • 1
Scott
  • 21,211
  • 8
  • 65
  • 72
  • Very helpful answer, i'd give you +2 if i could. – Kyle Gobel Dec 15 '13 at 13:19
  • @Scott, maybe you are right about the routes who match better the use case, but I cannot understand where is the ambiguity. There are errors in the Dto ( the constructor with parameter the array. Also the /orders/{Ids} , first it means ids of Orders not Customers, second, is it possible in Get to have an array of integers in the Uri ? (of course is not correct REST) – stefan2410 Dec 15 '13 at 16:18
  • @stefan2410 Yes you are right passing an array of Ids in the route would be wrong. I think Kyle probably simplified his question, eliminating the ambiguity, but he did say it through an `AmbiguousRoutesException` when accessing making a request (probably other routes). The use of `/orders/customers` lead me to believe he had a structural problem. Is there something you are stuck with? – Scott Dec 15 '13 at 16:52
  • @Scott, your answer is very good about the best routes, but it hides the real problem. For this reason is confusing. Can you identify the real problem, adding and the use cases part that it is very good ? – stefan2410 Dec 15 '13 at 16:56
  • Also the /orders/customers/7 and the /customers/7/orders , I don't think that it is reason of ambiguity. Of course is not the best route. – stefan2410 Dec 15 '13 at 16:58
  • @stefan2410 The reason for the ambiguity is likely because Kyle tried to create two or more action handlers with the same single DTO `OrdersRequest` for the same http method and the service couldn't determine which action to take. I addressed this by showing how to send two routes to the same handler and check for the inclusion of parameters. I am sure you can see there are issues in the question `/orders - Gets All Customers` and I have addressed these as best I can. If you feel it doesn't address the question, though it was accepted. You can submit your own answer to address the specifics. – Scott Dec 15 '13 at 17:19
  • @Scott, one question: As it is (probably not the right routes), if you delete the constructor with parameter the array, it will work or not ? I am not sure, I ask you. Can I ? I don't want to submit my answer. I try to understand the problem. – stefan2410 Dec 15 '13 at 17:24
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/43210/discussion-between-scott-and-stefan2410) – Scott Dec 15 '13 at 17:32
  • @Scott, thanks, another interesting issue in general, is that the constructors with parameters for GET, can be used in the ServiceStack C# client (and in case we use IReturn). But in this case the GET will not accessible from javascript clients, I think. – stefan2410 Dec 15 '13 at 19:18