REST API(ASP.NET Web API 2)最佳实践:如何返回不属于一种类型但与3个相关类型有关的1-N个项目?

5
我有以下三种类型。不同的模型可以有不同的属性。属性是指代码描述属性,意味着它具有有限的值集合。
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;
}

为了将此模型用于实际汽车,最后需要进行说明。

好的。现在我想要GET、POST和PUT操作,以便客户端可以获取、添加和更新整个模型。我应该创建一个包含所有这些操作的类型吗?

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

然后有:

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

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

或者有没有什么更好的方法,可以在不创建容器类的情况下使用输出参数或其他方式来实现?我真的很困惑。
用于实际汽车模型:
Car
{
    int Id;
    int ModelId;
    //ETC
}

CarAttribute
{
    int Id;
    int CarId;
    int CatAttributeTypeId
    int CodeValue;
}
1个回答

14

您原始模型的问题在于,这三个类别并没有通过引用以任何方式相互关联。

我猜您想要的是这样的内容。

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) {...}
}

在你的代码中某个位置,你想要存储静态汽车模型

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

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

如果你正在使用ORM,这意味着你将获取加载的子项(如果它们通常是1:1的话,默认情况下,如果它们是0:n,那么可能是懒加载)。

这可能会导致大量的对象图被加载并编组成JSON/XML。

实际上,您希望您的编组程序仅创建解耦的引用,例如HATEOS样式。

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
}

如果您想查看模型的详细信息,可以执行GET /myApp/carModel/54。

这意味着您的GET/PUT/POST等不需要填充或操作一个大的对象图。如果您希望检索整个图形,可以使用捆绑概念(我自己的术语)

GET /myApp/car/3/bundle

将返回

"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
}

然后解析结果以查找链接,减少交互,同时保持完全平坦的图形。

对于0..*列表,您可以使用ListContainer(我自己的术语)。

"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}
     ]
}       

对于 URL 格式正确的列表获取请求,始终返回 200,并包含一个空列表。

"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"
}      

不要忘记使用良好的状态码,404表示未找到,410(我想是这样,请检查维基百科)表示删除的实体等。在POST / PUT验证失败(无法处理的实体)的情况下使用适当的代码。

注意使用部分

/myApp/car/3/attribute

对于Car id=3而言,易于阅读的方式是获取其所有属性(这将返回1,2,3,4四个属性)。

如果将来想要使用快捷方式,可以使用“/myApp/attribute/4”(如果ID是唯一的)。

如果想要提交整个汽车,需要提交一个bundle,所有的IDs都为空(除非你想支持客户端ID生成,通常是基于GUID,不要使用序列!)。

如果想要同时提交多辆汽车,请使用Transaction(确保你的端点被注释为transactional),例如:

POST /myApp/car/transaction

一个Transaction资源看起来像是一个捆绑包,但它会有一系列的捆绑包。

"transaction" 
{
        bundle : [ ]
}

一个 bundle 包含汽车及其属性,另一个可能包含车型信息(这样如果您想要 10 辆丰田普锐斯,就不会重复车型信息)。

希望这有所帮助。

一些澄清:

“1. 为什么 Car 实体的第一个属性是 url?”

通常将其视为“良好实践”,包含一个自我引用,可使您可以可幂等地返回您拥有的资源。

想象一下,如果有人保存了 JSON,您并不总是知道它来自哪里,但有了嵌入式 URL,您就知道了!

“2. 对于 Bundle,您写到:“然后解析结果以查找链接,在保持完全平坦的图形的同时减少聊天。” -> 如果所有东西都在那里,我该怎么处理链接?3. 关于第二个问题:图形是平面的。为什么不是‘包容’?”

您有一个平面图,以避免循环引用/深度对象。

想象一下,如果车上一次又一次地使用相同的零件,我们将说轮子,它有 20KB 的属性。

您可以 a) 每次重复相同的 20KB 属性,或者 b) 在对象中放置指向 /carPart/3 的链接,然后在捆绑包中仅具有完整对象一次。

更费力的解析,但避免了过度嵌套和重复。

此外,作为一个额外的奖励,您可以选择不包含子对象(如果您不想要捆绑版本),因此您的资源仅具有链接,这非常轻量级。这样,如果客户端需要更多信息,他们只需获取它。

“4. ListContainer 是常见实践吗?尽管使人感到困惑。”

我看过几个 REST 实现都使用了类似的内容,这是一种很好的方式,将列表与类型信息和告诉客户端列表可能为空的能力进行包装。

我的经验法则是,如果是直接的 ID 引用,例如 /car/3,则返回确定的 1 匹配项或状态代码(即您几乎可以肯定该项目存在)。如果您正在“查找”某些东西,例如

/car?manufacturer="Toyota"&model="Prius",那么您期望获得一个 0..* 的列表容器,即使实际上,它可能是 0..1。

“5.‘被注释为 transactional 的终点’?事务和 REST 听起来很糟糕,您能解释一下吗?我理解不重复模型信息。”

事务很棘手,但即使在 REST 中,有时也必须做一些原子性的事情。这给了您这种能力。我不喜欢使用它,但有时是不可避免的。


非常感谢您提供如此详细的答案!我稍后会仔细阅读它。 - char m
1
这就是我想知道的!看起来不错。几个问题:1.为什么Car-entity的第一个属性是url?2.在Bundle的情况下,您写道:“然后解析结果以查找链接,减少闲聊,同时仍保持完全平坦的图形”->如果一切都在那里,我该怎么处理链接?3.与2有关:图形是平面的。为什么没有“包含”?4.这种ListContainer是常见做法吗?虽然有点说得通。5.“标记为事务性的端点”?事务和REST听起来很糟糕,您能解释一下吗?我理解了不重复模型信息。很有道理。 - char m

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接