1
votes

I have a POST endpoint that takes a URL path param and then the body is a list of submitted DTOs.

So right now the request DTO looks something along the lines of:

[Route("/prefix/{Param1}", "POST")]
public class SomeRequest
{
    public string          Param1  { get; set; }
    public List<SomeEntry> Entries { get; set; }
}

public class SomeEntry
{
    public int    ID    { get; set; }
    public int    Type  { get; set; }
    public string Value { get; set; }
}

And the service method looks something like:

public class SomeService : Service
{
    public SomeResponse Post(SomeRequest request)
    {
    }
}

If encoded via JSON, the client would have to encode the POST body this way:

{
    "Entries":
    [
        {
            "id":    1
            "type":  42
            "value": "Y"
        },
        ...
    ]
}

This is redundant, I would like the client to submit the data like this:

[
    {
        "id":    1
        "type":  42
        "value": "Y"
    },
    ...
]

Which would have been the case if my request DTO was simply List<SomeEntry>

My questions is: is there a way to "flatten" the request this way? Or designate one property of the request as the root of the message body? i.e perhaps:

[Route("/prefix/{Param1}", "POST")]
public class SomeRequest
{
    public string          Param1  { get; set; }
    [MessageBody]
    public List<SomeEntry> Entries { get; set; }
}

Is this doable in any way in ServiceStack?

2

2 Answers

1
votes

I was able to sort of get this to sort of work by subclassing List<T>:

[Route("/prefix/{Param1}", "POST")]
public class SomeRequest : List<SomeEntry>
{
    public string          Param1  { get; set; }
}

Then you can send a request like this:

POST /prefix/someParameterValue
Content-Type: application/json
[ { "ID": 1, "Type": 2, "Value": "X" }, ... ]

But if you have any choice in the design, I wouldn't recommend this. Here's a couple of reasons to start with:

  • I found at least one problem with this at runtime: sending an empty array, e.g. [ ] in JSON, is resulting in a 400 status code with RequestBindingException
  • It's less flexible. What if you do need to add additional top-level properties to the request in the future? You would be stuck with them being path/query params. Having a regular class-containing-a-list allows you to add new optional properties at the top level of the request body, with backward compatibility
0
votes

OK I've managed to achieve this. Not the prettiest solution but will do for now.

I wrapped the content type filter for JSON:

var serz   = ContentTypeFilters.GetResponseSerializer("application/json");
var deserz = ContentTypeFilters.GetStreamDeserializer("application/json");
ContentTypeFilters.Register("application/json", serz, (type, stream) => MessageBodyPropertyFilter(type, stream, deserz));

Then the custom deserializer looks like this:

private object MessageBodyPropertyFilter(Type type, Stream stream, StreamDeserializerDelegate original)
{
    PropertyInfo prop;
    if (_messageBodyPropertyMap.TryGetValue(type, out prop))
    {
        var requestDto = type.CreateInstance();
        prop.SetValue(requestDto, original(prop.PropertyType, stream), null);
        return requestDto;
    }
    else
    {
        return original(type, stream);
    }
}

_messageBodyPropertyMap is populated after init by scanning the request DTOs and looking for a certain attribute, as in the example in my original question.