我正在使用ServiceStack消费第三方WebAPI。想象一下此API具有以下路线。
https://api.example.com/v1/people/{id}
返回具有指定ID的人。
json:
{
"id": 1,
"name": "Jean-Luc Picard"
}
我可以使用以下C#代码消耗此内容。
class Program
{
static void Main(string[] args)
{
var client = new JsonServiceClient("https://api.example.com/v1/");
Person person = client.Get(new GetPerson() { ID = 1 });
}
}
[Route("/people/{id}")]
public class GetPerson : IReturn<Person>
{
public int ID { get; set; }
}
public class Person
{
public int ID { get; set; }
public string Name { get; set; }
}
如果API更改,我想使用明确的响应DTO。问题在于,/people/{id}
端点返回的JSON是一个裸体Person
对象。假设V2中的响应已更改。
json:
{
"person": {
"id": 1,
"name": "Jean-Luc Picard"
}
}
通过此响应,以下代码将起作用。
[Route("/people/{id}")]
public class GetPerson : IReturn<GetPersonResponse>
{
public int ID { get; set; }
}
public class GetPersonResponse
{
public Person Person { get; set; }
}
我想将此GetPersonResponse
与其Person
属性一起使用,用于上面的当前JSON,该JSON不会封装人数据。我知道我们可以使用DataContract
和DataMember
属性从System.Runtime.Serialization
控制如何将JSON元素映射到DTO,但是我认为没有任何方法可以映射根元素。有什么方法可以暗示ServiceStack.Text
JSON DESERIALIZER,即需要将当前JSON的根元素归为此Person
对象?
我确定的最佳解决方案与属性有关。
public class GetPersonResponse
{
private int _id;
private string _name;
private Person _person;
public int ID { get => _id; set { _id = value; if(_person != null) _person.ID = _id; } }
public string Name { get => _name; set { _name = value; if (_person != null) _person.Name = _name; } }
public Person Person { get => _person ?? new Person() { ID = this.ID, Name = this.Name }; set => _person = value; }
}
这是站不住脚的,因为它没有正确封装。ID和名称必须保留JSON DESERIALIZER才能访问它们。真正的DTO也有30多个领域,所以这将是一场噩梦。
目的是将应用程序与客户端库解耦,而只需更新客户端库即可利用新的API版本。该应用程序将继续访问response.Person
,好像什么都没发生。
最终,这归结为ServiceStack.Text
问题。可以编写GetPersonResponse1
的版本,除了Person
以外没有其他公共属性,也没有样板代码,因此以下断言会通过吗?
using ServiceStack;
using System;
using System.Diagnostics;
class Program
{
static void Main(string[] args)
{
string v1 = "{"id":1,"name":"Jean-Luc Picard"}";
string v2 = "{"person":{"id":1,"name":"Jean-Luc Picard"}}";
GetPersonResponse1 p1 = v1.FromJson<GetPersonResponse1>();
GetPersonResponse2 p2 = v2.FromJson<GetPersonResponse2>();
Debug.Assert(p1.Person != null
&& p2.Person != null
&& p1.Person.ID == p2.Person.ID
&& p1.Person.Name == p2.Person.Name);
}
}
public class GetPersonResponse1
{
public Person Person { get; set; }
}
public class GetPersonResponse2
{
public Person Person { get; set; }
}
public class Person
{
public int ID { get; set; }
public string Name { get; set; }
}
更新
电线上的数据形状是一个问题,因为我不控制电线 - 这是第三方WebAPI。WebAPI实现了Hateoas Hal Hypermedia类型,因此响应中的_link和其他数据不是模型的一部分。对于某些端点,它当前返回裸体对象。可以想象,如果他们在响应中添加了非模型元数据,那么模型数据将移至响应中的_embedd元素。
Application (3rd Party)
=> Client Library (Me)
=> WebAPI (3rd Party)
我的错误是想象该应用程序将直接与响应DTO一起使用。回想起来,由于多种原因,这是没有意义的。取而代之的是,响应DTO应明确遵循导线上的数据形状(MyTHZ的第一个建议(。然后,客户库库应公开一个抽象的图层,以便应用程序与之互动(MyTHZ的第二个建议(。
Application
=> Client Library API
=> Client Library Response DTOs
=> WebAPI
我确实计划看一下Restsharp,以查看它是否更适合我的用例,但决定先从Servicestack首先开始。
DTO(数据传输对象(的目的是使用与电线格式的形状匹配的类型架构来定义服务合同,以便它们可以与通用序列化器一起使用以自动序列化。/无需手动自定义逻辑样板的有效载荷。
所以我不遵循为什么您要隐藏DTO的公共模式,以及为什么类型包含嵌入式逻辑,这两种逻辑都是DTO的抗模式,应该是良性的无impl数据结构。
我的第一个建议是更改您的类型,因此它们是DTOS,其公共模式与试图估算为数据的数据形状相匹配。
失败了,鉴于您的类型不是DTO,您想使用它来补充一个数据模型,我会考虑创建单独的键入DTO并使用自动化库(或自定义类型的映射扩展方法(来复制从键入DTO到理想数据模型的数据。
如果您不想从数据模型中维护单独的键入数据DTO,则可以使用通用的JSON解析器,该解析器将任意数据结构供应到宽松的通用.NET数据结构,例如Dictionary<string,object>
。
如果您只是想避免公共属性,并且很乐意拥有公共字段,则可以指定Servicestack.Text的序列化器,可以使用以下方式填充公共字段:
JsConfig.IncludePublicFields = true;