问题
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