Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

REST API (ASP.NET Web API 2) best practices: How to return 1 - N items that are not one type but 3 related types?

I have 3 types like below. Different models can have different attributes. An attribute is a code description attribute meaning that it's limited set of values.

CarModel
{
   int Id;
   string Name;
}

CarAttributeType
{
     int Id;
     int ModelId;
     string GroupName;
     int DataOrder;
     bool Mandatory;
     // default values etc.
}

CarCodeDescriptions
{
    int Id;
    int AttributeTypeId;
    int Code;
    int Descr;
}

To use this model for actual cars there is explanation in the end.

Ok. Now I want to have GET, POST and PUT operations that client can fetch, add and update whole model. Do I make a type that contains all these

CarModelContainer
{
    CarModel Model;
    IEnumerable<CarAttributeType> Attributes;
    IEnumerable<CarCodeDescriptions> Attributes;
}

and then have:

[Route("carmodels/{id:int}"")]
public CarModelContainer Get(int id)
{
    return Persistence.Instance.GetCarModel(id);
}

[Route("carmodels")]
public IEnumerable<CarModelContainer> GetAll()
{
    return Persistence.Instance.GetCarModels();
}

or is it desireable to do this somehow (out parameters or something) without making this container class? I'm truly lost here.

To use model for actual cars:

Car
{
    int Id;
    int ModelId;
    //ETC
}

CarAttribute
{
    int Id;
    int CarId;
    int CatAttributeTypeId
    int CodeValue;
}
like image 392
char m Avatar asked Nov 14 '14 06:11

char m


1 Answers

The problem with your original model is that the three classes aren't really in any way linked via references.

I'm guessing you want something in the line of.

public abstract class CarModel
{
     int id;
     string manufacturer;
     string name;
     List<CarAttributeType> attributes; // 0..n

}

public class CarModelAttribute
{
     int id;
     string name;
     CodifiedValue value; // 0..1 or do we want 0..* i.e. put it in a list
     int dataOrder; // no idea what this is doing
     bool mandatory;
}

public class CodifiedValue
{
     int id;
     int code;
     string description;
}

public class Car
{
     int id;
     string registration;
     int mileage;
     CarModel model;

     public Car(registration, mileage, model) {...}
}

somewhere in your code, where you want to store static car models

readonly CarModel toyotaPrius = new CarModel("Toyota", "Prius", priusAttributes)

Car myToyota = new Car("DF01 1DXM", 1000, toyotaPrius)

Now, if you're using an ORM, this means you'll get the children loaded (if they're 1:1 usually by default or if they're 0:n then possibly lazily).

This can result in MASSIVE object graphs being loaded and marshalled into JSON/XML

In reality, you'd like your marshaller to create decoupled references only, maybe HATEOS stylee e.g.

GET /myApp/car/3 (where 3 is the ID of my Prius), which would get you

"Car"
{
"url" : "/myApp/car/3",
"id" : 3,
"registration" : "DF01 1DXM",
"mileage": 1000,
"model" : "/myApp/carModel/54",       // where 54 maps back to the Id for the Toyota Prius
}

If you wish to see the details of the model, you could then do a GET /myApp/carModel/54.

This means your GET/PUT/POST etc. do not need to hydrate or juggle a massive object graph. If you wish to retrieve the whole graph, you could use the concept of a bundle (my own terminology)

GET /myApp/car/3/bundle

which would return

"Bundle"
{

    "Car"
    {
    "url" : "/myApp/car/3",
    "id" : 3,
    "registration" : "DF01 1DXM",
    "mileage": 1000,
    "model" : "/myApp/carModel/54",       // where 54 maps back to the Id for the Toyota Prius
    },
    "CarModel"
    {
    "url" : "/myApp/carModel/54",
    "id" : 54,
    "manufacturer" : "Toyota",
    "model": "Prius,
    } // and so on and so forth for attributes
}

and then parse the results to find the links, reducing chatter while still maintaining a completely flat graph

For 0..* lists, you could use a ListContainer (my own terminology)

"ListContainer"
{
     "url" : "/myApp/car/3/attribute" // this URL returns a list of attributes, you may decide to use 2 base URLs, I've used 1 base
     "ItemType": "CarModelAttribute",
     "Count": 2,
     "Items" : [
                {attribute JSON goes here}, {attribute JSON goes here}
     ]
}       

Always return a 200 for list gets where the URL is well formed, but include an empty list

"ListContainer"
{
     "url" : "/myApp/car/3/attribute" // this URL returns a list of attributes, you may decide to use 2 base URLs, I've used 1 base
     "ItemType": "CarModelAttribute",
     "Count": 0,
     "Items" : [],
     "emptyReason" : "This Car Model has no attributes"
}      

Don't forget to use good Status Codes, 404 for not found, 410 (I think, check wikipedia) for deleted entities etc. Use an appropriate code for POST/PUT where it fails validation (unprocessable entity)

Notice the use of compartments

/myApp/car/3/attribute

So it's easy to read, for Car id=3, I would like all its attributes (this returns attributes 1,2,3,4)

So you can use "/myApp/attribute/4" in the future if you wish as a shortcut (if the IDs are unique of course)

If you wish to post a whole car, you post a bundle, all IDs empty (unless you want to support client ID generation (usually GUID based, don't use a sequence for this!).

If you wish to post many cars at once, use a Transaction (ensuring your endpoint is annotated as transactional) e.g.

POST /myApp/car/transaction

where a Transaction resource looks like a bundle, but would have a list of bundles

"transaction" 
{
        bundle : [ ]
}

where one bundle would be the cars and their attributes and maybe another would be model information (so you don't duplicate model information if you want 10 Toyota Priuses).

I hope that helps.

Some clarifications:

"1. Why there is url as 1st attribute of Car-entity?"

It's usually seen as "good practice" to include a self reference to how you can idempotently get back to the resource you have.

Imagine if somebody saved you the JSON, you'd not always know where it came from, with an embedded URL, you do!

"2. In case of Bundle you write: "and then parse the results to find the links, reducing chatter while still maintaining a completely flat graph" -> If everything is there what I do withv links? 3. Related to 2: The graph is flat. Why no "containment"?"

You have a flat graph to avoid circular references/n-depth objects.

Imagine you have the same part on a car time after time, we'll say the wheel, it has 20kb of attributes on it.

You can a) reproduce the same 20kb of attributes each time, or b) put a link in your objects to /carPart/3 and then later on in the bundle just have the full object once.

More effort to parse, but avoids excessive nesting and repetition.

Also, as a bonus, you can choose to NOT include child objects if you don't want the bundled version so your resource only has links, which is really lightweight. That way, if the client wants more information, they just GET it.

"4. Is this ListContainer common practice? It makes kinda sense though."

Several REST implementations I've seen use something similar, it's a good way of wrapping a list with type info and the ability to tell the client why the list is potentially empty.

My rule of thumb is, if it's a direct ID reference e.g. /car/3 it's returned as definitely 1 match OR a Status Code (i.e. you expect almost certantly that this item exists). If you're "finding" something e.g.

/car?manufacturer="Toyota"&model="Prius" then you expect a list container of 0..* even if really, it's likely to be 0..1.

"5. "endpoint annotated as transactional"? Transactions and REST sounds bad, can you explain. I Understood not duplicating model Info."

Transactions are tricky, but even in REST, sometimes you have to do some atomic things. This gives you that ability. I don't like using it, but sometimes it's unavoidable.

like image 183
Jeff Watkins Avatar answered Sep 28 '22 16:09

Jeff Watkins