Make names of named tuples appear in serialized JSON responses

后端 未结 4 1638
借酒劲吻你
借酒劲吻你 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 05:41

    The simplest solution is using dynamic code, i.e. C#'s ExpandoObject to wrap your response in the format you expect the API to have

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

    The return type of "GetData" is

    (decimal speed, int distance)
    

    This gives a Json response in the way you expect it to

    0 讨论(0)
  • 2021-02-05 05:43

    You have a little bid conflicting requirements

    Question:

    I have loads of these custom classes like MyType and would love to use a generic container instead

    Comment:

    However, what type would I have to declare in my ProducesResponseType attribute to explicitly expose what I am returning

    Based on above - you should stay with types you already have. Those types provide valuable documentation in your code for other developers/reader or for yourself after few months.

    From point of readability

    [ProducesResponseType(typeof(Trip), 200)]
    

    will be better then

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

    From point of maintainability
    Adding/removing property need to be done only in one place. Where with generic approach you will need to remember update attributes too.

    0 讨论(0)
  • 2021-02-05 06:00

    Problem with using named tuples in your case is that they are just syntactic sugar.

    If you check named-and-unnamed-tuples documentation you will find part:

    These synonyms are handled by the compiler and the language so that you can use named tuples effectively. IDEs and editors can read these semantic names using the Roslyn APIs. You can reference the elements of a named tuple by those semantic names anywhere in the same assembly. The compiler replaces the names you've defined with Item* equivalents when generating the compiled output. The compiled Microsoft Intermediate Language (MSIL) does not include the names you've given these elements.

    So you have problem as you do your serialization during runtime, not during compilation and you would like to use the information which was lost during compilation. One could design custom serializer which gets initialized with some code before compilation to remember named tuple names but I guess such complication is too much for this example.

    0 讨论(0)
  • 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<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;
        }
    }
    

    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<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,
                },
            });
        }
    
    0 讨论(0)
提交回复
热议问题