使命名元组的名称出现在序列化的JSON响应中

Qua*_*yst 11 c# json tuples asp.net-web-api json-serialization

情况:我有多个Web服务API调用来提供对象结构.目前,我声明显式类型将这些对象结构绑定在一起.为简单起见,这是一个例子:

[HttpGet]
[ProducesResponseType(typeof(MyType), 200)]
public MyType TestOriginal()
{
    return new MyType { Speed: 5.0, Distance: 4 };
}
Run Code Online (Sandbox Code Playgroud)

改进:我有很多这样的自定义类MyType,并且喜欢使用通用容器.我遇到了命名元组,可以在我的控制器方法中成功使用它们,如下所示:

[HttpGet]
[ProducesResponseType(typeof((double speed, int distance)), 200)]
public (double speed, int distance) Test()
{
    return (speed: 5.0, distance: 4);
}
Run Code Online (Sandbox Code Playgroud)

我面临的问题是已解析的类型是基于Tuple包含这些无意义属性的底层Item1,Item2等等.示例:

在此输入图像描述

问题:有没有人找到一个解决方案来将命名元组的名称序列化为我的JSON响应?或者,有没有人找到一个通用的解决方案,允许为可以使用的随机结构提供单个类/表示,以便JSON响应显式地命名它包含的内容.

小智 7

请改用匿名对象。

(double speed, int distance) = (5.0, 4);
return new { speed, distance };
Run Code Online (Sandbox Code Playgroud)

  • 它会呈现什么返回类型?对于公共 API 方法来说,“object”是一种非常不明显且因此不受欢迎的返回类型 (3认同)

wat*_*rif 6

在您的情况下使用命名元组的问题在于它们只是语法糖

如果您检查命名和未命名元组文档,您会发现以下部分:

这些同义词由编译器和语言处理,以便您可以有效地使用命名元组。IDE 和编辑器可以使用 Roslyn API 读取这些语义名称。您可以在同一程序集中的任何位置通过这些语义名称来引用命名元组的元素。生成编译输出时,编译器会将您定义的名称替换为 Item* 等效项。已编译的 Microsoft 中间语言 (MSIL) 不包括您为这些元素指定的名称。

因此,当您在运行时而不是编译期间进行序列化时,您会遇到问题,并且您希望使用编译期间丢失的信息。人们可以设计自定义序列化器,它在编译之前用一些代码进行初始化,以记住命名元组名称,但我想这种复杂性对于这个例子来说太复杂了。


ana*_*tol 5

对于序列化响应,只需在操作和自定义合同解析器上使用任何自定义属性(不幸的是,这只是解决方案,但我仍在寻找更优雅的解决方案)。

属性

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

合同解析器

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

用法

[ReturnValueTuple]
[HttpGet]
[Route("types")]
public IEnumerable<(int id, string name)> GetDocumentTypes()
{
    return ServiceContainer.Db
        .DocumentTypes
        .AsEnumerable()
        .Select(dt => (dt.Id, dt.Name));
}
Run Code Online (Sandbox Code Playgroud)

这个返回下一个 JSON:

[  
   {  
      "id":0,
      "name":"Other"
   },
   {  
      "id":1,
      "name":"Shipping Document"
   }
]
Run Code Online (Sandbox Code Playgroud)

这是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,
            },
        });
    }
Run Code Online (Sandbox Code Playgroud)


Fab*_*bio 4

您的出价要求有些冲突

问题:

我有很多这样的自定义类,MyType并且很想使用通用容器来代替

评论:

但是,我必须在 ProducesResponseType 属性中声明什么类型才能显式公开我返回的内容

根据上述内容 - 您应该保留已有的类型。这些类型在您的代码中为其他开发人员/读者或几个月后的您自己提供了有价值的文档。

从可读性的角度来看

[ProducesResponseType(typeof(Trip), 200)]
Run Code Online (Sandbox Code Playgroud)

那时会更好

[ProducesResponseType(typeof((double speed, int distance)), 200)]
Run Code Online (Sandbox Code Playgroud)

从可维护性的角度来看,
添加/删除属性只需在一处完成。使用通用方法时,您还需要记住更新属性。

  • 问题是如何将命名元组的名称序列化为 JSON 响应。我也想知道该怎么做。 (4认同)
  • 我完全同意你所说的一切。坚持使用显式类型是我现在能想到的最显式的选择,但我正在努力寻求一种更灵活的解决方案,以避免编写所有这些类。 (3认同)