在 ServiceStack 显式响应 DTO 中反序列化根对象

Deserialize Root Object in ServiceStack Explicit Response DTO

我正在使用 ServiceStack 使用第三方 WebApi。想象一下这个 API 有以下路线。

https://api.example.com/v1/people/{id}returns指定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; }
}

我想改用 explicit response DTO,以防 API 发生变化。问题是 /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,它没有封装人员数据。我知道我们可以使用 System.Runtime.Serialization 中的 DataContractDataMember 属性来控制 JSON 元素如何映射到 DTO,但我认为没有任何办法映射根元素。有什么方法可以提示 ServiceStack.Text JSON 反序列化器当前 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 和名称必须保持 public 以便 JSON 反序列化器能够访问它们。真正的DTO也有30+字段,所以这将是一场噩梦。

这样做的目的是使应用程序与客户端库分离,只需要更新客户端库即可利用新的 API 版本。应用程序将继续访问 response.Person,就好像什么都没发生一样。

最终,这归结为一个 ServiceStack.Text 问题。是否可以编写 GetPersonResponse1 的版本,除了 Person 之外没有 public 属性并且没有样板代码,使得以下断言通过?

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

更新

电线上数据形状的变化令人担忧,因为我无法控制电线 - 它是第 3 方 WebAPI。 WebAPI 实现了 HATEOAS HAL 超媒体类型,因此响应中有 _links 和其他数据,它们不是模型的一部分。对于某些端点,它目前 returns 是裸对象。可以想象,如果他们将非模型元数据添加到响应中,那么模型数据将被移动到响应中的 _embedded 元素。

Application (3rd Party)
  => Client Library (Me)
  => WebAPI (3rd Party)

我的错误是想象应用程序将直接与响应 DTO 一起工作。回想起来,出于多种原因,这没有意义。相反,响应 DTO 应明确遵循线路上数据的形状(mythz 的第一个建议)。然后客户端库应该公开一个抽象层供应用程序与之交互(mythz 的第二个建议)。

Application
  => Client Library API
  => Client Library Response DTOs
  => WebAPI

我确实打算看一下 RestSharp,看它是否更适合我的用例,但决定先从 ServiceStack 开始。

purpose of DTOs(数据传输对象)是使用与有线格式的形状相匹配的类型化模式来定义服务合同,因此它们可以与通用序列化程序一起使用以自动 Serialize/Deserialize没有手动自定义逻辑样板的有效负载。

所以我没有理解为什么你试图隐藏 DTO 的 public 模式以及为什么类型包含嵌入式逻辑,这两者都是 anti-patterns DTO 应该是良性 impl-free 数据结构。

我的第一个建议是更改您的类型,使它们成为 DTO,其中它们的 public 架构与它试图反序列化的数据的形状相匹配。

如果做不到这一点,鉴于您的类型不是 DTO,并且您想使用它来混合数据模型,我会考虑创建一个单独的类型化 DTO 并使用 AutoMapping library(或自定义类型映射器扩展方法)将类型化 DTO 中的数据复制到理想的数据模型中。

如果您不想从数据模型中维护一个单独的类型化数据 DTO,您可以使用 generic JSON parser 将任意数据结构反序列化为 loose-typed 通用 .NET 数据结构,例如Dictionary<string,object>.

如果您只是想避免 public 属性并且乐于使用 public 字段,您可以指定 ServiceStack.Text' s 序列化程序用以下内容填充 public 字段:

JsConfig.IncludePublicFields = true;