카테고리 없음

[OCA] Azure Function Extension OpenAPI Issue

흰무 2023. 8. 11. 23:58

Issue #396

OCA에서 Challenger 과정이 끝나가면서, Issue 하나를 정해 파악하는 시간을 가지게 되었다.

처음 정한 Issue는 #396으로 다음과 같다.

https://github.com/Azure/azure-functions-openapi-extension/issues/396

 

Nullable<byte> => "Sequence contains no elements" · Issue #396 · Azure/azure-functions-openapi-extension

Describe the issue I'm getting an exception trying to render the Swagger Document when my API includes a class that has a Nullable in it. To Reproduce Steps to reproduce the behavior: Use Visual St...

github.com

1. 원인 파악

Sequence contains no elements

   at System.Linq.ThrowHelper.ThrowNoElementsException()
   at System.Linq.Enumerable.First[TSource](IEnumerable`1 source)
   at Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Visitors.NullableObjectTypeVisitor.Visit(IAcceptor acceptor, KeyValuePair`2 type, NamingStrategy namingStrategy, Attribute[] attributes)
   at Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Visitors.OpenApiSchemaAcceptor.Accept(VisitorCollection collection, NamingStrategy namingStrategy)
   at Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Visitors.ObjectTypeVisitor.ProcessProperties(IOpenApiSchemaAcceptor instance, String schemaName, Dictionary`2 properties, NamingStrategy namingStrategy)
   at Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Visitors.ObjectTypeVisitor.Visit(IAcceptor acceptor, KeyValuePair`2 type, NamingStrategy namingStrategy, Attribute[] attributes)
   at Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Visitors.OpenApiSchemaAcceptor.Accept(VisitorCollection collection, NamingStrategy namingStrategy)
   at Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.DocumentHelper.GetOpenApiSchemas(List`1 elements, NamingStrategy namingStrategy, VisitorCollection collection)
   at Microsoft.Azure.WebJobs.Extensions.OpenApi.Document.Build(Assembly assembly, OpenApiVersionType version)
   at Microsoft.Azure.WebJobs.Extensions.OpenApi.OpenApiTriggerFunctionProvider.RenderSwaggerDocument(HttpRequest req, String extension, ExecutionContext ctx, ILogger log)

해당 Exception을 따라가보면, Schema에 값이 추가가 되지 않아 NullableObjectTypeVisitor에서 OpenApiSchemaAcceptor의 Schema가 비어있어 First() 메서드에서 오류가 발생하는 것을 알 수 있었습니다.

// NullableObjectTypeVisitor에서 오류가 발생하는 코드 (58~63번째 줄)

subAcceptor.Accept(this.VisitorCollection, namingStrategy);

  
// Adds the schema for the underlying type.
var name = subAcceptor.Schemas.First().Key;
var schema = subAcceptor.Schemas.First().Value;
schema.Nullable = true;

OpenApiSchemaAcceptor의 Accept 메서드를 확인해보면 Visitor Collection을 하나하나 돌면서, IsVisitable이 True일 때, Byte Type이 Schema에 추가가 되지 않는 것으로 보입니다.

// OpenApiSchemaAccepter의 Accept 메소드
// 해당 메소드의 19번째 줄의 foreach문에서 Visitor Collection을 돌면서 확인할 때, Byte Type에 해당하는 Visitor가 없어서 발생하는 것으로 추정

public void Accept(VisitorCollection collection, NamingStrategy namingStrategy)
        {
            // Checks the properties only.
            if (this.Properties.Any())
            {
                foreach (var property in this.Properties)
                {
                    var attributes = new List<Attribute>
                    {
                        property.Value.GetCustomAttribute<OpenApiSchemaVisibilityAttribute>(inherit: false),
                        property.Value.GetCustomAttribute<OpenApiPropertyAttribute>(inherit: false),
                    };
                    attributes.AddRange(property.Value.GetCustomAttributes<ValidationAttribute>(inherit: false));
                    attributes.AddRange(property.Value.GetCustomAttributes<JsonPropertyAttribute>(inherit: false));

                    foreach (var visitor in collection.Visitors)
                    {
                        if (!visitor.IsVisitable(property.Value.PropertyType))
                        {
                            continue;
                        }

                        var type = new KeyValuePair<string, Type>(property.Key, property.Value.PropertyType);
                        visitor.Visit(this, type, namingStrategy, attributes.ToArray());
                    }
                }

                return;
            }

Nullable<T>를 사용할 수 있는 다른 값 변수들 역시 실험해본 결과, Nullable<byte> 타입과 Nullable<char> 타입, 2가지 타입이 오류가 발생하는 것을 발견할 수 있었습니다.

 

2. 해결 방안

ByteTypeVisitor가 없어서 발생하는 오류라 생각하고 ByteTypeVisitor를 만들고 실행한 결과, 오류가 없이 실행되는 것을 볼 수 있었습니다.

using System;
using System.Collections.Generic;

using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Abstractions;
using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Extensions;
using Microsoft.OpenApi.Models;
using Newtonsoft.Json.Serialization;

namespace Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Visitors
{
    /// <summary>
    /// This represents the type visitor for <see cref="byte"/>.
    /// </summary>
    public class ByteTypeVisitor : TypeVisitor
    {
        /// <inheritdoc />
        public ByteTypeVisitor(VisitorCollection visitorCollection)
            : base(visitorCollection)
        {
        }

        /// <inheritdoc />
        public override bool IsVisitable(Type type)
        {
            var isVisitable = this.IsVisitable(type, TypeCode.Byte);

            return isVisitable;
        }

        /// <inheritdoc />
        public override void Visit(IAcceptor acceptor, KeyValuePair<string, Type> type, NamingStrategy namingStrategy, params Attribute[] attributes)
        {
            this.Visit(acceptor, name: type.Key, title: null, dataType: "string", dataFormat: "binary", attributes: attributes);
        }

        /// <inheritdoc />
        public override bool IsParameterVisitable(Type type)
        {
            var isVisitable = this.IsVisitable(type);

            return isVisitable;
        }

        /// <inheritdoc />
        public override OpenApiSchema ParameterVisit(Type type, NamingStrategy namingStrategy)
        {
            return this.ParameterVisit(dataType: "string", dataFormat: "binary");
        }

        /// <inheritdoc />
        public override bool IsPayloadVisitable(Type type)
        {
            var isVisitable = this.IsVisitable(type);

            return isVisitable;
        }

        /// <inheritdoc />
        public override OpenApiSchema PayloadVisit(Type type, NamingStrategy namingStrategy)
        {
            return this.PayloadVisit(dataType: "string", dataFormat: "binary");
        }
    }
}

 

3. 의문점

실행은 되지만, Nullable<byte>가 아닌 byte의 경우에는 어떤 Visitor를 거쳐서 실행되는지 알 수 없었습니다.

using System;

namespace Microsoft.Azure.WebJobs.Extensions.OpenApi.FunctionApp.Models
{
    public class Dto
    {
        public byte Byte {get; set; }
    }
}

해당 사진은 Dto 클래스에 아무것도 표현되지 않는 것을 볼 수 있어 byte 타입의 경우 Visitor가 따로 존재하지 않는 것인지 의문이 듭니다.

 

==> 피드백 후, ByteTypeVisitor와 CharTypeVisitor가 없는 것으로 확인하고 OpenAPI 문서를 참고해 작성하기로 했습니다.

 

4. PR 그리고 Merge

직접 올린 PR : https://github.com/Azure/azure-functions-openapi-extension/pull/600

피드백을 받고, OpenAPI 문서와 Json Schema ruleset을 참고해 ByteTypeVisitor,CharTypeVisitor를 작성했습니다.

이 후에, Unit Test와 Intergration Test를 작성해야 했는데 단지 2개의 파일 추가였음에도 생각보다 많은 양의 Test Code를 작성해야 했습니다.

그렇게 각 Visitor 별로, Test Code를 작성한 결과! PR이 main 브랜치에 merge 될 수 있었습니다!

처음 오픈소스 컨트리뷰션을 진행해보면서 왠지 모를 뿌듯함도 느껴졌고, 큰 프로젝트에서 어떻게 디자인패턴이 쓰이는지 볼 수 있어 뜻 깊은 경험이었습니다.