Make names of named tuples appear in serialized JSON responses

后端 未结 4 1640
借酒劲吻你
借酒劲吻你 2021-02-05 05:31

Situation: I have multiple Web service API calls that deliver object structures. Currently, I declare explicit types to bind those object structures together. F

4条回答
  •  南笙
    南笙 (楼主)
    2021-02-05 06:02

    For serializing response just use any custom attribute on action and custom contract resolver (this is only solution, unfortunately, but I'm still looking for any more elegance one).

    Attribute:

    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()
                ?.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 _names;
    
        public ValueTuplesContractResolver(IList names)
        {
            _names = names;
        }
    
        protected override IList 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;
        }
    }
    

    Usage:

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

    This one returns next JSON:

    [  
       {  
          "id":0,
          "name":"Other"
       },
       {  
          "id":1,
          "name":"Shipping Document"
       }
    ]
    

    Here the solution for 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()?.TransformNames;
            if (names == null)
            {
                return;
            }
    
            var responseType = apiDescription.ResponseDescription.DeclaredType;
            FieldInfo[] tupleFields;
            var props = new Dictionary();
            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>
                {
                    props,
                };
            }
            else
            {
                result = props;
            }
    
            operation.responses.Clear();
            operation.responses.Add("200", new Response
            {
                description = "OK",
                schema = new Schema
                {
                    example = result,
                },
            });
        }
    

提交回复
热议问题