Сделать имена названных кортежей в сериализованных ответах JSON
Ситуация. У меня есть несколько вызовов API веб-сервисов, которые предоставляют структуры объектов. В настоящее время я объявляю явные типы для связывания этих структур объектов. Для простоты здесь приведен пример:
[HttpGet]
[ProducesResponseType(typeof(MyType), 200)]
public MyType TestOriginal()
{
return new MyType { Speed: 5.0, Distance: 4 };
}
Улучшение. У меня есть множество этих пользовательских классов, таких как MyType
, и я бы хотел использовать общий контейнер. Я наткнулся на названные кортежи и могу успешно использовать их в своих методах контроллера, например:
[HttpGet]
[ProducesResponseType(typeof((double speed, int distance)), 200)]
public (double speed, int distance) Test()
{
return (speed: 5.0, distance: 4);
}
Проблема Я столкнулся с тем, что разрешенный тип основан на базовом Tuple
, который содержит эти бессмысленные свойства Item1
, Item2
и т.д. Пример:
![введите описание изображения здесь]()
Вопрос. Кто-нибудь нашел решение, чтобы получить имена названных кортежей, сериализованных в мои ответы JSON? В качестве альтернативы, кто-нибудь нашел общее решение, которое позволяет иметь один класс/представление для случайных структур, которые можно использовать, чтобы ответ JSON явно указывал то, что он содержит.
Ответы
Ответ 1
У вас есть немного противоречивые требования для ставок.
Вопрос:
У меня есть множество этих пользовательских классов, таких как MyType
, и мне бы очень хотелось использовать вместо этого используется общий контейнер
Комментарий:
Однако, какой тип я должен был бы объявить в моем ProducesResponseType атрибут , чтобы явно показывать то, что я возвращаю
Основываясь на выше, вы должны остаться с уже имеющимися типами. Эти типы предоставляют ценную документацию в коде для других разработчиков/читателей или для себя через несколько месяцев.
Из точки удобочитаемости
[ProducesResponseType(typeof(Trip), 200)]
будет лучше, чем
[ProducesResponseType(typeof((double speed, int distance)), 200)]
С точки зрения ремонтопригодности
Добавление/удаление имущества необходимо выполнять только в одном месте. Где с общим подходом вам также нужно будет запомнить атрибуты обновления.
Ответ 2
Для сериализации ответа просто используйте любой пользовательский атрибут в действии и пользовательский обработчик контракта (к сожалению, это единственное решение, но я все еще ищу какую-то элегантность).
Атрибут:
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;
}
}
Использование:
[ReturnValueTuple]
[HttpGet]
[Route("types")]
public IEnumerable<(int id, string name)> GetDocumentTypes()
{
return ServiceContainer.Db
.DocumentTypes
.AsEnumerable()
.Select(dt => (dt.Id, dt.Name));
}
Этот возвращает следующий JSON:
[
{
"id":0,
"name":"Other"
},
{
"id":1,
"name":"Shipping Document"
}
]
Вот решение для 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,
},
});
}
Ответ 3
Проблема с использованием именованных кортежей в вашем случае заключается в том, что они просто синтаксический сахар.
Если вы посмотрите документацию по именованным и неназванным кортежам, вы найдете часть:
Эти синонимы обрабатываются компилятором и языком так, чтобы Вы можете эффективно использовать именованные кортежи. IDE и редакторы могут прочитать эти семантические имена с использованием API Roslyn. Вы можете ссылаться на элементы именованного кортежа этими семантическими именами в любом месте того же сборка. Компилятор заменяет имена, которые вы определили, на Item * эквиваленты при генерации скомпилированного вывода. Скомпилированный Microsoft Intermediate Language (MSIL) не включает имена Вы дали эти элементы.
Таким образом, у вас есть проблема, поскольку вы делаете сериализацию во время выполнения, а не во время компиляции, и вы хотели бы использовать информацию, которая была потеряна во время компиляции. Можно создать собственный сериализатор, который инициализируется с некоторым кодом перед компиляцией, чтобы запомнить имена именованных кортежей, но я думаю, что такое усложнение слишком велико для этого примера.