Situation: I have multiple Web service API calls that deliver object structures. Currently, I declare explicit types to bind those object structures together. F
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
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.
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.
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,
},
});
}