Сделать так, чтобы имена именованных кортежей появлялись в сериализованных ответах 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 явно указывалось, что он содержит.
5 ответов
вместо этого использовать анонимный объект
(double speed, int distance) = (5.0, 4);
return new { speed,distance };
Для сериализации ответа просто используйте любой настраиваемый атрибут в действии и настраиваемом преобразователе контрактов (к сожалению, это единственное решение, но я все еще ищу более элегантное).
Атрибут:
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;
}
}
Использование:
[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,
},
});
}
Проблема с использованием именованных кортежей в вашем случае заключается в том, что они просто синтаксический сахар.
Если вы проверите документацию по именованным и безымянным кортежам, вы найдете часть:
Эти синонимы обрабатываются компилятором и языком, поэтому вы можете эффективно использовать именованные кортежи. IDE и редакторы могут читать эти семантические имена с помощью API Roslyn. Вы можете ссылаться на элементы именованного кортежа с помощью этих семантических имен в любом месте той же сборки. Компилятор заменяет имена, которые вы определили, эквивалентами Item* при создании скомпилированного вывода. Скомпилированный промежуточный язык Microsoft (MSIL) не включает имена, которые вы дали этим элементам.
Таким образом, у вас есть проблема, поскольку вы выполняете сериализацию во время выполнения, а не во время компиляции, и вы хотите использовать информацию, которая была потеряна во время компиляции. Можно разработать собственный сериализатор, который инициализируется некоторым кодом перед компиляцией, чтобы запоминать именованные имена кортежей, но я думаю, что такое усложнение слишком велико для этого примера.
У вас есть небольшие ставки противоречивые требования
Вопрос:
У меня есть множество этих пользовательских классов, таких как
MyType
и хотел бы использовать универсальный контейнер вместо
Комментарий:
Однако, какой тип я должен объявить в своем атрибуте ProducesResponseType, чтобы явным образом раскрыть то, что я возвращаю
Исходя из вышеизложенного - вам следует придерживаться типов, которые у вас уже есть. Эти типы предоставляют ценную документацию в вашем коде для других разработчиков / читателей или для вас самих через несколько месяцев.
С точки зрения читабельности
[ProducesResponseType(typeof(Trip), 200)]
тогда будет лучше
[ProducesResponseType(typeof((double speed, int distance)), 200)]
С точки зрения ремонтопригодности
Добавление / удаление свойства нужно делать только в одном месте. Где с универсальным подходом вам нужно будет также помнить атрибуты обновления.
Самое простое решение - использовать
dynamic
код, то есть ExpandoObject C#, чтобы обернуть ваш ответ в формат, который вы ожидаете от API
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);
}
Тип возврата "GetData
" является
(decimal speed, int distance)
Это дает ответ Json так, как вы ожидаете