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

39

情况:我有多个Web服务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);
}

问题是,解析出来的类型基于包含这些无意义属性Item1Item2等的底层Tuple。例如:

enter image description here

问题:有人找到了一种解决方案,可以将命名元组的名称序列化到我的JSON响应中吗?或者,有人找到了通用解决方案,允许使用单个类/表示来表示可以用于明确命名JSON响应包含什么的随机结构。


2
你能不能不使用动态对象来实现这个?比如 return new { speed = 5.0, distance = 4 }; - Nick Coad
动态或对象应该可以工作,但我不确定。这更多是一个Swagger问题,而不是一个一般的API问题。 - Nick Coad
ProducesResponseTypeAttribute是来自命名空间Microsoft.AspNetCore.Mvc的ASP.Core属性。它用于文档和ApiExplorers,包括Swashbuckle在内都会使用它。使用dynamic时,我的文档就不够具体了,因此我尝试使用命名元组,并就如何正确使用它们提出了问题。 - Quality Catalyst
2
从概念上讲,这根本不是命名元组的工作——它们没有属性,也不应该用于建模实体/进行类型检查。最终,您可能需要一些数据传输对象(DTO)样板代码,例如 MyType - j4nw
1
如果你只有一个返回值,你根本不需要使用“ProducesResponseType”。然而,我也在寻找一种将命名元组转换为具有可读性属性名称的JSON的方法。 - Ciantic
显示剩余5条评论
5个回答

7

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

属性

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,
            },
        });
    }

6
在您的情况下使用命名元组的问题在于它们只是语法糖
如果您查看命名和未命名元组的文档,您会发现其中一部分内容:
这些同义词由编译器和语言处理,以便您可以有效地使用命名元组。IDE和编辑器可以使用Roslyn API读取这些语义名称。您可以在同一程序集中的任何位置通过这些语义名称引用命名元组的元素。编译器在生成已编译输出时将您定义的名称替换为Item*等效项。已编译的Microsoft Intermediate Language (MSIL)不包括您给这些元素的名称。
所以您的问题是在运行时进行序列化而不是在编译期间,并且您希望使用在编译期间丢失的信息。一个方法是设计自定义序列化器,在编译之前用一些代码初始化来记住命名元组的名称,但我想这种复杂性对于此示例来说太多了。

6

改用匿名对象。

(double speed, int distance) = (5.0, 4);
return new { speed, distance };

3
它会返回什么类型?“object”是公共API方法非常不明显且因此不可取的返回类型。 - anatol
@anatol 是的,它是 object。不,这不太好。匿名对象并不适用于公共使用(因此是匿名的)。 - CervEd

4
您有一些冲突的要求。
问题:
我有很多这样的自定义类,比如"我的类型",我想使用一个通用容器。
评论:
然而,如果我要在"ProducesResponseType"属性中声明哪种类型来明确地公开我要返回什么,则必须保留现有的类型。
基于上述情况 - 您应该继续使用已有的类型。这些类型为其他开发人员/读者或几个月后的自己提供了有价值的代码文档说明。
从可读性的角度来看。
[ProducesResponseType(typeof(Trip), 200)]

将会更好

[ProducesResponseType(typeof((double speed, int distance)), 200)]

从可维护性的角度出发
添加/删除属性只需要在一个地方完成。而使用通用方法,您需要记得更新属性。


3
我完全同意你所说的。目前来看,坚持使用明确类型是我能想到的最明确的选择,但我正在努力寻找一种更灵活的解决方案,避免编写所有这些类。 - Quality Catalyst
4
问题是如何将命名元组的名称序列化到JSON响应中。我也想知道如何做到这一点。 - Tomo
虽然这不是对问题的直接回答,但它是更好的答案,因为它展示了“正确的方法”:不要使用元组,而是使用一个小类,不需要自定义序列化代码。另一种替代方案需要一些花哨的代码,几乎肯定比POCO序列化慢,更不用说缺乏可维护性了。 - Alexei - check Codidact

1
最简单的解决方案是使用dynamic代码,即C#的ExpandoObject将您的响应包装成您期望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响应。

我无法想象为什么你会使用这个而不是匿名对象。 - Daniel
好的,在回答这个问题时,我当时在大量使用EO,因为它们可以在运行时更改定义,因为它们被传递了,而匿名对象则不行(就我所知)。 - divay pandey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接