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:
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.
Python tuples are JSON serializable, just like lists or dictionaries. The JSONEncoder class supports the following objects and types by default. The process of converting a tuple (or any other native Python object) to a JSON string is called serialization.
The results shows Tuple<T1,T2,T3,...> is serializable and deserializable.
To serialize a collection - a generic list, array, dictionary, or your own custom collection - simply call the serializer with the object you want to get JSON for. Json.NET will serialize the collection and all of the values it contains.
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, }, }); }
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With