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

时间:2018-02-20 07:40:24

标签: servicestack servicestack-text

我正在使用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; }
}

我希望使用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。我知道我们可以使用DataContract中的DataMemberSystem.Runtime.Serialization属性来控制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和名称必须保持公开,JSON反序列化器才能访问它们。真正的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超媒体类型,因此响应中的_links和其他数据不属于模型。对于某些端点,它当前返回裸对象。可以想象,如果他们将非模型元数据添加到响应中,那么模型数据将被移动到响应中的_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开始。

1 个答案:

答案 0 :(得分:2)

purpose of DTOs(数据传输对象)用于定义具有与有线格式形状匹配的类型模式的服务合同,以便它们可以与通用序列化程序一起使用,以自动序列化/反序列化有效负载而无需手动定制逻辑样板。

所以我不理解为什么你试图隐藏DTO的公共模式以及为什么类型包含嵌入式逻辑,这两者都是DTO的反模式,它们应该是良性的无内容数据结构。

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

如果没有这个,假设你的类型不是DTO而你想用它来水化数据模型我会考虑创建一个单独的Typed DTO并使用AutoMapping library(或自定义Type mapper扩展方法) )将类型DTO中的数据复制到理想的数据模型中。

如果您不想在数据模型中维护单独的Typed Data DTO,可以使用generic JSON parser将任意数据结构反序列化为松散类型的通用.NET数据结构,如{{1} }。

如果您只是想避开公共属性并且很乐意拥有公共字段,您可以指定ServiceStack.Text的序列化程序来填充公共字段:

Dictionary<string,object>