Сделать имена названных кортежей в сериализованных ответах 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) не включает имена Вы дали эти элементы.

Таким образом, у вас есть проблема, поскольку вы делаете сериализацию во время выполнения, а не во время компиляции, и вы хотели бы использовать информацию, которая была потеряна во время компиляции. Можно создать собственный сериализатор, который инициализируется с некоторым кодом перед компиляцией, чтобы запомнить имена именованных кортежей, но я думаю, что такое усложнение слишком велико для этого примера.