Make named tuple names in serialized JSON responses

The situation . I have several web service API calls that provide object structures. I am currently declaring explicit types for binding these object structures. For simplicity, here is an example:

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

Improvement . I have many of these custom classes, such as MyType , and I would like to use a generic container. I came across the named tuples and can successfully use them in my controller methods, for example:

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

Problem I was faced with the fact that the permitted type is based on a basic Tuple that contains these meaningless properties of Item1 , Item2 , etc. Example:

enter image description here

Question Has anyone found a solution to get the names of named tuples serialized into my JSON responses? Alternatively, someone has found a general solution that allows you to have one class / view for random structures that you can use so that the JSON response explicitly indicates what it contains.

+11
source share
3 answers

You have slightly conflicting wagering requirements.

Question:

I have many of these custom classes, such as MyType , and I'd really like to use a shared container instead.

A comment:

However, what type would I have to declare in my ProducesResponseType attribute in order to explicitly show what I return

Based on the above, you should stay with existing types. These types provide valuable documentation in code for other developers / readers or for themselves in a few months.

From the point of readability

 [ProducesResponseType(typeof(Trip), 200)] 

will be better than

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

In terms of maintainability
Adding / removing property must be done in only one place. Where with a general approach you will also need to remember the update attributes.

+2
source

To serialize the response, just use any custom attribute in action and a custom contract handler (unfortunately this is the only solution, but I'm still looking for some kind of elegance).

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 readonly 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); for (var i = 0; i < properties.Count; i++) { properties[i].PropertyName = _names[i]; } 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 returns the following JSON:

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

Here is 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, }, }); } 
+2
source

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

If you look at the documentation for named and unnamed tuples , you will find the part:

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

Thus, you have a problem because you are doing serialization at runtime, not at compile time, and you would like to use information that was lost during compilation. You can create your own serializer that initializes with some code before compilation to remember the names of named tuples, but I think this complication is too great for this example.

0
source

Source: https://habr.com/ru/post/1271313/


All Articles