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, }, }); }