Deserialize Root Object in ServiceStack Explicit Response DTO

可紊 提交于 2019-12-24 06:14:32

问题


I'm consuming a third party WebApi using ServiceStack. Imagine this API has the following route.

https://api.example.com/v1/people/{id} returns the person with the specified ID.

JSON:

{
    "id": 1,
    "name": "Jean-Luc Picard"
}

I could consume this with the following C# code.

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

I would like to use an explicit response DTO instead, in case the API changes. The issue is that the JSON returned by the /people/{id} endpoint is a naked Person object. Let's say that the response in v2 is changed.

JSON:

{
    "person": {
        "id": 1,
        "name": "Jean-Luc Picard"
    }
}

With this response, the following code would work.

[Route("/people/{id}")]
public class GetPerson : IReturn<GetPersonResponse>
{
    public int ID { get; set; }
}

public class GetPersonResponse
{
    public Person Person { get; set; }
}

I want to use this GetPersonResponse, with its Person property, for the current JSON above that does not encapsulate the person data. I know that we can use the DataContract and DataMember attributes from System.Runtime.Serialization to control how the JSON elements are mapped to the DTO, but I don't think there's any way to map the root element. Is there any way to hint to the ServiceStack.Text JSON Deserializer that the root element of the current JSON needs to be deserialized to this Person object?

The best solution I've determined works around this with properties.

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

This is untenable as it's not properly encapsulated. The ID and Name have to remain public for the JSON Deserializer to be able to access them. The real DTO also has 30+ fields, so this would be a nightmare.

The goal of this would be to keep the application decoupled from the client library, and simply require an update of the client library to take advantage of the new API version. The application would continue accessing response.Person as if nothing had happened.

Ultimately, this boils down to a ServiceStack.Text question. Can a version of GetPersonResponse1 be written, with no public properties other than Person and no boilerplate code, such that the following assertion passes?

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

Update

The shape of the data on the wire changing is a concern because I don't control the wire - it's a 3rd party WebAPI. The WebAPI implements the HATEOAS HAL hypermedia type, so there's _links and other data in the response that's not part of the model. For some endpoints, it currently returns the naked object. It's conceivable that if they added non-model metadata to the response, then the model data would be moved to an _embedded element in the response.

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

My error was imagining the application would work directly with the response DTOs. In retrospect, this doesn't make sense for a number of reasons. Instead, the response DTOs should follow the shape of the data on the wire explicitly (mythz' first recommendation). Then the client library should expose an abstracted layer for the application to interact with (mythz' second recommendation).

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

I do plan on taking a look at RestSharp to see if it will fit my use case better, but decided to start with ServiceStack first.


回答1:


The purpose of DTOs (Data Transfer Objects) is to define the Service Contract with a Typed Schema that matches the shape of the Wire Format so they can be used with a generic serializer to automatically Serialize/Deserialize payloads without manual custom logic boilerplate.

So I'm not following why you're trying to hide the public schema of the DTO and why the Types contain embedded logic, both of which are anti-patterns for DTOs which should be benign impl-free data structures.

My first recommendation is to change your Types so they are DTOs where their public schema matches the shape of the data it's trying to deserialize into.

Failing that, given your Types aren't DTOs and you want to use it to hydrate a data model I'd look at creating a separate Typed DTO and use an AutoMapping library (or a custom Type mapper extension method) to copy the data from the Typed DTO into your ideal Data Model.

If you don't want to maintain a separate Typed Data DTO from your Data Model you can use a generic JSON parser which will deserialize arbitrary data structures into a loose-typed generic .NET Data Structures like Dictionary<string,object>.

If you're just looking to avoid public properties and are happy to have public fields instead you can specify ServiceStack.Text's serializer to populate public fields with:

JsConfig.IncludePublicFields = true;


来源:https://stackoverflow.com/questions/48880042/deserialize-root-object-in-servicestack-explicit-response-dto

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!