ServiceStack中的root对象显式响应DTO



我正在使用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不会封装人数据。我知道我们可以使用DataContractDataMember属性从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;

最新更新