Make names of named tuples appear in serialized JSON responses
Asked Answered
P

5

39

Situation: I have multiple Web service API calls that deliver object structures. Currently, I declare explicit types to bind those object structures together. For the sake of simplicity, here's an example:

[HttpGet]
[ProducesResponseType(typeof(MyType), 200)]
public MyType TestOriginal()
{
    return new MyType { Speed: 5.0, Distance: 4 };
}

Improvement: I have loads of these custom classes like MyType and would love to use a generic container instead. I came across named tuples and can successfully use them in my controller methods like this:

[HttpGet]
[ProducesResponseType(typeof((double speed, int distance)), 200)]
public (double speed, int distance) Test()
{
    return (speed: 5.0, distance: 4);
}

Problem I am facing is that the resolved type is based on the underlying Tuple which contains these meaningless properties Item1, Item2 etc. Example:

enter image description here

Question: Has anyone found a solution to get the names of the named tuples serialized into my JSON responses? Alternatively, has anyone found a generic solution that allows to have a single class/representation for random structures that can be used so that the JSON response explicitly names what it contains.

Psychognosis answered 29/8, 2017 at 6:20 Comment(10)
Could you not use dynamic objects for this? return new { speed = 5.0, distance = 4 }; for example?Fire
That would work indeed, yes. However, what type would I have to declare in my ProducesResponseType attribute to explicitly expose what I am returning?Psychognosis
dynamic or object I believe should work, but I'm not sure. That's more a Swagger question than a general API question.Fire
ProducesResponseTypeAttribute is an ASP.Core attribute from the namespace Microsoft.AspNetCore.Mvc. Yes, it is used for documentation and ApiExplorers including Swashbuckle use it. With dynamic my documentation is less specific hence my experimentation with named tuples and my question around how to use them properly.Psychognosis
On a conceptual level, this simply isn't a job for named tuples - they don't have properties and aren't supposed to model entities/be used for type checking. You probably should, after all, end up with some DTO boilerplate like MyType.Chisel
@QualityCatalyst sorry, I meant the specific type used in the response type was more of a documentation question (I used Swagger as an example, but whatever). I didn't mean to imply your original question of how to ensure the properties are named in the response was off-topic. I only meant that the type you place into the "ProducesResponseTypeAttribute" is not going to impact the returned JSON and thus the effectiveness of my suggestion (to my knowledge).Fire
You shouldn't need the ProducesResponseType at all if you have one return type. However I'm also looking a way to turn named tuple to JSON with legible property names.Fully
did you solve that?Underbelly
@anatol: No, I didn't.Psychognosis
Exposing tuples to callers is a bad idea for API. Your previous solution is perfect and working fine, I don't think you need to change them to value tuples.Kreiner
U
7

For serializing response just use any custom attribute on action and custom contract resolver (this is only solution, unfortunately, but I'm still looking for any more elegance one).

Attribute:

public class ReturnValueTupleAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
    {
        var content = actionExecutedContext?.Response?.Content as ObjectContent;
        if (!(content?.Formatter is JsonMediaTypeFormatter))
        {
            return;
        }

        var names = actionExecutedContext
            .ActionContext
            .ControllerContext
            .ControllerDescriptor
            .ControllerType
            .GetMethod(actionExecutedContext.ActionContext.ActionDescriptor.ActionName)
            ?.ReturnParameter
            ?.GetCustomAttribute<TupleElementNamesAttribute>()
            ?.TransformNames;

        var formatter = new JsonMediaTypeFormatter
        {
            SerializerSettings =
            {
                ContractResolver = new ValueTuplesContractResolver(names),
            },
        };

        actionExecutedContext.Response.Content = new ObjectContent(content.ObjectType, content.Value, formatter);
    }
}

ContractResolver:

public class ValueTuplesContractResolver : CamelCasePropertyNamesContractResolver
{
    private IList<string> _names;

    public ValueTuplesContractResolver(IList<string> names)
    {
        _names = names;
    }

    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        var properties = base.CreateProperties(type, memberSerialization);
        if (type.Name.Contains(nameof(ValueTuple)))
        {
            for (var i = 0; i < properties.Count; i++)
            {
                properties[i].PropertyName = _names[i];
            }

            _names = _names.Skip(properties.Count).ToList();
        }

        return properties;
    }
}

Usage:

[ReturnValueTuple]
[HttpGet]
[Route("types")]
public IEnumerable<(int id, string name)> GetDocumentTypes()
{
    return ServiceContainer.Db
        .DocumentTypes
        .AsEnumerable()
        .Select(dt => (dt.Id, dt.Name));
}

This one returns next JSON:

[  
   {  
      "id":0,
      "name":"Other"
   },
   {  
      "id":1,
      "name":"Shipping Document"
   }
]

Here the solution for Swagger UI:

public class SwaggerValueTupleFilter : IOperationFilter
{
    public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
    {
        var action = apiDescription.ActionDescriptor;
        var controller = action.ControllerDescriptor.ControllerType;
        var method = controller.GetMethod(action.ActionName);
        var names = method?.ReturnParameter?.GetCustomAttribute<TupleElementNamesAttribute>()?.TransformNames;
        if (names == null)
        {
            return;
        }

        var responseType = apiDescription.ResponseDescription.DeclaredType;
        FieldInfo[] tupleFields;
        var props = new Dictionary<string, string>();
        var isEnumer = responseType.GetInterface(nameof(IEnumerable)) != null;
        if (isEnumer)
        {
            tupleFields = responseType
                .GetGenericArguments()[0]
                .GetFields();
        }
        else
        {
            tupleFields = responseType.GetFields();
        }

        for (var i = 0; i < tupleFields.Length; i++)
        {
            props.Add(names[i], tupleFields[i].FieldType.GetFriendlyName());
        }

        object result;
        if (isEnumer)
        {
            result = new List<Dictionary<string, string>>
            {
                props,
            };
        }
        else
        {
            result = props;
        }

        operation.responses.Clear();
        operation.responses.Add("200", new Response
        {
            description = "OK",
            schema = new Schema
            {
                example = result,
            },
        });
    }
Underbelly answered 3/7, 2019 at 7:0 Comment(0)
P
7

Make use of anonymous object instead.

(double speed, int distance) = (5.0, 4);
return new { speed, distance };
Pungy answered 26/2, 2021 at 13:27 Comment(2)
and what return type will it present? object is a very unobvious and consequently undesirable return type for public API methodUnderbelly
@Underbelly yes it's object. No, it's not great. Anonymous objects aren't designed to public consumption (hence anonymous)He
P
6

Problem with using named tuples in your case is that they are just syntactic sugar.

If you check named-and-unnamed-tuples documentation you will find part:

These synonyms are handled by the compiler and the language so that you can use named tuples effectively. IDEs and editors can read these semantic names using the Roslyn APIs. You can reference the elements of a named tuple by those semantic names anywhere in the same assembly. The compiler replaces the names you've defined with Item* equivalents when generating the compiled output. The compiled Microsoft Intermediate Language (MSIL) does not include the names you've given these elements.

So you have problem as you do your serialization during runtime, not during compilation and you would like to use the information which was lost during compilation. One could design custom serializer which gets initialized with some code before compilation to remember named tuple names but I guess such complication is too much for this example.

Plod answered 26/7, 2019 at 9:51 Comment(0)
C
4

You have a little bid conflicting requirements

Question:

I have loads of these custom classes like MyType and would love to use a generic container instead

Comment:

However, what type would I have to declare in my ProducesResponseType attribute to explicitly expose what I am returning

Based on above - you should stay with types you already have. Those types provide valuable documentation in your code for other developers/reader or for yourself after few months.

From point of readability

[ProducesResponseType(typeof(Trip), 200)]

will be better then

[ProducesResponseType(typeof((double speed, int distance)), 200)]

From point of maintainability
Adding/removing property need to be done only in one place. Where with generic approach you will need to remember update attributes too.

Cady answered 29/8, 2017 at 7:42 Comment(3)
I fully agree with all you said. Sticking to the explicit types is the most explicit option I can think of as of now, yet I am striving for a more flexible solution that avoids writing all of these classes.Psychognosis
The question was how to get the names of the named tuples serialized into JSON responses. I wonder how to do it as well.Landwaiter
While this is not a direct answer to the question, it is a better answer since it shows "the right way": instead of working with tuples, a small class requires no custom serialization code. The alternative requires some fancy code which almost certainly is slower than the POCO serialization, not to mention lack of maintainability.Furfuran
C
1

The simplest solution is using dynamic code, i.e. C#'s ExpandoObject to wrap your response in the format you expect the API to have

    public JsonResult<ExpandoObject> GetSomething(int param)
    {
        var (speed, distance) = DataLayer.GetData(param);
        dynamic resultVM = new ExpandoObject();
        resultVM.speed= speed;
        resultVM.distance= distance;
        return Json(resultVM);
    }

The return type of "GetData" is

(decimal speed, int distance)

This gives a Json response in the way you expect it to

Chemar answered 14/9, 2020 at 5:51 Comment(2)
I can't imagine why you would use this over an anonymous object.Ep
well, at the time of answering this question I was using EO heavily since they can change definitions on run-time as they were passed around which anon-objects dont (as far as i know)Chemar

© 2022 - 2024 — McMap. All rights reserved.