Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ServiceStack Clients and Ambiguous Routes

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

like image 536
Kyle Gobel Avatar asked Dec 15 '13 02:12

Kyle Gobel


1 Answers

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.

like image 155
Scott Avatar answered Nov 03 '22 14:11

Scott