< Summary - CoreWCF Coverage — PR #1766

Information
Class: CoreWCF.OpenApi.OpenApiSchemaBuilder
Assembly: CoreWCF.WebHttp
File(s): /home/runner/work/CoreWCF/CoreWCF/src/CoreWCF.WebHttp/src/CoreWCF/OpenApi/OpenApiSchemaBuilder.cs
Line coverage
95%
Covered lines: 505
Uncovered lines: 25
Coverable lines: 530
Total lines: 1130
Line coverage: 95.2%
Branch coverage
79%
Covered branches: 277
Total branches: 350
Branch coverage: 79.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/CoreWCF/CoreWCF/src/CoreWCF.WebHttp/src/CoreWCF/OpenApi/OpenApiSchemaBuilder.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System;
 5using System.Collections;
 6using System.Collections.Generic;
 7using System.Globalization;
 8using System.IO;
 9using System.Linq;
 10using System.Reflection;
 11using System.Runtime.Serialization;
 12using System.Text.RegularExpressions;
 13using System.Threading.Tasks;
 14using System.Xml;
 15using CoreWCF.Description;
 16using CoreWCF.OpenApi.Attributes;
 17using CoreWCF.Web;
 18using Microsoft.AspNetCore.WebUtilities;
 19using Microsoft.Extensions.Primitives;
 20using Microsoft.OpenApi.Any;
 21using Microsoft.OpenApi.Interfaces;
 22using Microsoft.OpenApi.Models;
 23
 24namespace CoreWCF.OpenApi
 25{
 26    /// <summary>
 27    /// This class builds an OpenAPI specification file out of attributes applied to WCF service interfaces.
 28    /// </summary>
 29    public static class OpenApiSchemaBuilder
 30    {
 31        private const string ArrayNamespace = "http://schemas.microsoft.com/2003/10/Serialization/Arrays";
 32        private const string DataContractNamespace = "http://schemas.datacontract.org/2004/07/";
 33
 34        /// <summary>
 35        /// Build the OpenAPI specification file.
 36        /// </summary>
 37        /// <param name="info">Top level information about the API.</param>
 38        /// <param name="contracts">One or more service contracts.</param>
 39        /// <returns>An OpenAPI specification file.</returns>
 40        /// <exception cref="ArgumentNullException"></exception>
 41        public static OpenApiDocument BuildOpenApiSpecificationDocument(OpenApiOptions info, IEnumerable<OpenApiContract
 42        {
 4243            if (info == null)
 44            {
 045                throw new ArgumentNullException(nameof(info));
 46            }
 47
 4248            if (contracts == null)
 49            {
 050                throw new ArgumentNullException(nameof(contracts));
 51            }
 52
 4253            OpenApiDocument document = new OpenApiDocument
 4254            {
 4255                Components = new OpenApiComponents(),
 4256                Paths = new OpenApiPaths()
 4257            };
 58
 4259            PopulateOpenApiInfo(document, info);
 4260            PopulateOpenApiPathsOperations(document, contracts, info.TagsToHide);
 61
 4262            if (info.TagsSorter != null)
 63            {
 164                var tags = document.Tags as List<OpenApiTag> ?? document.Tags.ToList();
 165                tags.Sort(info.TagsSorter);
 166                document.Tags = tags;
 67            }
 68
 4269            return document;
 70        }
 71
 72        /// <summary>
 73        /// Populate some top level general info about the API.
 74        /// </summary>
 75        /// <param name="document">The document object that is being built up.</param>
 76        /// <param name="info">Top level information about the API.</param>
 77        private static void PopulateOpenApiInfo(OpenApiDocument document, OpenApiOptions info)
 78        {
 4279            document.Info = new OpenApiInfo
 4280            {
 4281                Version = info.Version ?? "",
 4282                Description = info.Description,
 4283                Title = info.Title ?? "",
 4284                TermsOfService = info.TermsOfService
 4285            };
 86
 4287            if (info.ContactName != null || info.ContactEmail != null || info.ContactUrl != null)
 88            {
 189                document.Info.Contact = new OpenApiContact
 190                {
 191                    Name = info.ContactName,
 192                    Email = info.ContactEmail,
 193                    Url = info.ContactUrl
 194                };
 95            }
 96
 4297            if (info.LicenseName != null)
 98            {
 199                document.Info.License = new OpenApiLicense
 1100                {
 1101                    Name = info.LicenseName,
 1102                    Url = info.LiceneUrl
 1103                };
 104            }
 105
 42106            if (info.ExternalDocumentUrl != null)
 107            {
 1108                document.ExternalDocs = new OpenApiExternalDocs
 1109                {
 1110                    Description = info.ExternalDocumentDescription,
 1111                    Url = info.ExternalDocumentUrl
 1112                };
 113            }
 42114        }
 115
 116        /// <summary>
 117        /// Populate the paths and operations from a given API.
 118        /// </summary>
 119        /// <param name="document">The document object that is being built up.</param>
 120        /// <param name="contracts">The WCF contracts that should be documented.</param>
 121        /// <param name="tagsToHide">Any tags that need to be hidden for some reason.</param>
 122        private static void PopulateOpenApiPathsOperations(OpenApiDocument document, IEnumerable<OpenApiContractInfo> co
 123        {
 160124            foreach (OpenApiContractInfo contractInfo in contracts)
 125            {
 38126                List<MethodInfo> methods = new List<MethodInfo>();
 80127                foreach (Type interfaceInfo in contractInfo.Contract.GetInterfaces())
 128                {
 2129                    methods.AddRange(interfaceInfo.GetMethods());
 130                }
 38131                methods.AddRange(contractInfo.Contract.GetMethods());
 132
 38133                OpenApiBasePathAttribute basePathAttribute = contractInfo.Contract.GetCustomAttribute<OpenApiBasePathAtt
 134
 168135                foreach (MethodInfo method in methods)
 136                {
 46137                    PopulateOpenApiPath(
 46138                        document,
 46139                        method,
 46140                        tagsToHide,
 46141                        basePathAttribute?.BasePath,
 46142                        contractInfo.ResponseFormat,
 46143                        GetMethodUriWebGet);
 144
 46145                    PopulateOpenApiPath(
 46146                        document,
 46147                        method,
 46148                        tagsToHide,
 46149                        basePathAttribute?.BasePath,
 46150                        contractInfo.ResponseFormat,
 46151                        GetMethodUriWebInvoke);
 152                }
 153            }
 42154        }
 155
 156        /// <summary>
 157        /// Populate a path that uses a from a given method.
 158        /// </summary>
 159        /// <param name="document">The document object that is being built up.</param>
 160        /// <param name="methodInfo">The given method.</param>
 161        /// <param name="tagsToHide">Any tags that need to be hidden for some reason.</param>
 162        /// <param name="additionalBasePath">An additional base path a given service contract is registered under.</para
 163        /// <param name="behaviorFormat">The default format in the WebHttpBehavior.</param>
 164        /// <param name="getOperationInfo">Get necessary information about an operation.</param>
 165        private static void PopulateOpenApiPath(
 166            OpenApiDocument document,
 167            MethodInfo methodInfo,
 168            IEnumerable<string> tagsToHide,
 169            string additionalBasePath,
 170            WebMessageFormat behaviorFormat,
 171            Func<MethodInfo, OperationInfo> getOperationInfo)
 172        {
 92173            OperationInfo operationInfo = getOperationInfo(methodInfo);
 92174            if (operationInfo.Method == null || operationInfo.UriTemplate == null)
 175            {
 47176                return;
 177            }
 178
 45179            if (methodInfo.GetCustomAttribute<OpenApiHiddenAttribute>() != null)
 180            {
 1181                return;
 182            }
 183
 99184            foreach (OpenApiTagAttribute tagAttribute in methodInfo.GetCustomAttributes<OpenApiTagAttribute>())
 185            {
 6186                if (tagsToHide.Contains(tagAttribute.Tag))
 187                {
 1188                    return;
 189                }
 190            }
 191
 43192            OpenApiOperation operation = new OpenApiOperation();
 193
 43194            string uri = Regex.Replace(operationInfo.UriTemplate, @"\?.*", "");
 195
 43196            if (!string.IsNullOrEmpty(additionalBasePath))
 197            {
 0198                uri = additionalBasePath + uri;
 199            }
 200
 43201            DefaultContentType defaultContentType = new DefaultContentType
 43202            {
 43203                ResponseFormatExplicitlySet = operationInfo.IsResponseFormatSetExplicitly,
 43204                ResponseAttributeFormat = operationInfo.ResponseFormat,
 43205                ResponseBehaviorFormat = behaviorFormat
 43206            };
 207
 43208            NameTable table = new NameTable();
 43209            XmlNamespaceManager nsManager = new XmlNamespaceManager(table);
 210
 43211            PopulateOpenApiResponses(document, operation, methodInfo, defaultContentType, tagsToHide, nsManager);
 43212            PopulateOpenApiParameters(document, operation, methodInfo, operationInfo.UriTemplate, defaultContentType, ta
 43213            PopulateOpenApiOperationTags(document, operation, methodInfo);
 43214            PopulateOpenApiOperationSummary(operation, methodInfo);
 215
 43216            OperationType? operationType = GetOperationType(operationInfo.Method);
 43217            if (operationType.HasValue && document.Paths.ContainsKey(uri))
 218            {
 1219                if (!document.Paths[uri].Operations.ContainsKey(operationType.Value))
 220                {
 1221                    document.Paths[uri].Operations.Add(operationType.Value, operation);
 222                }
 223            }
 42224            else if (operationType.HasValue)
 225            {
 42226                document.Paths.Add(uri, new OpenApiPathItem
 42227                {
 42228                    Operations = new Dictionary<OperationType, OpenApiOperation>
 42229                    {
 42230                        { operationType.Value, operation }
 42231                    }
 42232                });
 233            }
 43234        }
 235
 236        /// <summary>
 237        /// Maps an HTTP method to an OperationType.
 238        /// </summary>
 239        /// <param name="method">An HTTP method.</param>
 240        /// <returns>An OperationType.</returns>
 241        private static OperationType? GetOperationType(string method)
 242        {
 43243            switch (method.ToLower())
 244            {
 245                case "get":
 22246                    return OperationType.Get;
 247                case "put":
 0248                    return OperationType.Put;
 249                case "post":
 21250                    return OperationType.Post;
 251                case "delete":
 0252                    return OperationType.Delete;
 253                case "options":
 0254                    return OperationType.Options;
 255                case "head":
 0256                    return OperationType.Head;
 257                case "patch":
 0258                    return OperationType.Patch;
 259                case "trace":
 0260                    return OperationType.Trace;
 261                default:
 0262                    return null;
 263            }
 264        }
 265
 266        /// <summary>
 267        /// Get the method and URI for a service contract method with a WebGetAttribute.
 268        /// </summary>
 269        /// <param name="methodInfo">A method in a service contract.</param>
 270        /// <returns>An HTTP method and URI.</returns>
 271        private static OperationInfo GetMethodUriWebGet(MethodInfo methodInfo)
 272        {
 46273            WebGetAttribute attribute = methodInfo.GetCustomAttribute<WebGetAttribute>()
 46274                ?? WebHttpServiceModelCompat.GetNativeAttribute<WebGetAttribute>(methodInfo);
 275
 46276            if (attribute == null)
 277            {
 22278                return new OperationInfo();
 279            }
 280
 24281            return new OperationInfo
 24282            {
 24283                Method = "get",
 24284                UriTemplate = attribute.UriTemplate,
 24285                IsResponseFormatSetExplicitly = attribute.IsResponseFormatSetExplicitly,
 24286                ResponseFormat = attribute.ResponseFormat,
 24287                IsRequestFormatSetExplicitly = attribute.IsRequestFormatSetExplicitly,
 24288                RequestFormat = attribute.RequestFormat
 24289            };
 290        }
 291
 292        /// <summary>
 293        /// Get the method and URI for a service contract method with a WebInvokeAttribute.
 294        /// </summary>
 295        /// <param name="methodInfo">A method in a service contract.</param>
 296        /// <returns>An HTTP method and URI.</returns>
 297        private static OperationInfo GetMethodUriWebInvoke(MethodInfo methodInfo)
 298        {
 46299            WebInvokeAttribute attribute = methodInfo.GetCustomAttribute<WebInvokeAttribute>()
 46300                ?? WebHttpServiceModelCompat.GetNativeAttribute<WebInvokeAttribute>(methodInfo);
 301
 46302            if (attribute == null)
 303            {
 24304                return new OperationInfo();
 305            }
 306
 22307            return new OperationInfo
 22308            {
 22309                Method = attribute.Method?.ToLower(CultureInfo.InvariantCulture),
 22310                UriTemplate = attribute.UriTemplate,
 22311                IsResponseFormatSetExplicitly = attribute.IsResponseFormatSetExplicitly,
 22312                ResponseFormat = attribute.ResponseFormat,
 22313                IsRequestFormatSetExplicitly = attribute.IsRequestFormatSetExplicitly,
 22314                RequestFormat = attribute.RequestFormat
 22315            };
 316        }
 317
 318        /// <summary>
 319        /// Populate the responses for a given method.
 320        /// </summary>
 321        /// <param name="document">The document object that is being built up.</param>
 322        /// <param name="operation">The schema object that is being built up.</param>
 323        /// <param name="method">The given method.</param>
 324        /// <param name="defaultContentType">Calculates the default content type.</param>
 325        /// <param name="tagsToHide">Any tags that need to be hidden for some reason.</param>
 326        private static void PopulateOpenApiResponses(
 327            OpenApiDocument document,
 328            OpenApiOperation operation,
 329            MethodInfo method,
 330            DefaultContentType defaultContentType,
 331            IEnumerable<string> tagsToHide,
 332            XmlNamespaceManager nsManager)
 333        {
 43334            IEnumerable<OpenApiResponseAttribute> attributes = method.GetCustomAttributes<OpenApiResponseAttribute>();
 43335            if (attributes.Any())
 336            {
 16337                foreach (OpenApiResponseAttribute responseAttribute in method.GetCustomAttributes<OpenApiResponseAttribu
 338                {
 4339                    PopulateOpenApiResponse(
 4340                        responseAttribute.Type,
 4341                        document,
 4342                        operation,
 4343                        defaultContentType,
 4344                        tagsToHide,
 4345                        nsManager,
 4346                        responseAttribute);
 347                }
 348            }
 39349            else if (method.ReturnType != null && method.ReturnType != typeof(Task))
 350            {
 38351                Type type = method.ReturnType;
 38352                if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>))
 353                {
 1354                    type = type.GetGenericArguments()[0];
 355                }
 356
 38357                PopulateOpenApiResponse(
 38358                    type,
 38359                    document,
 38360                    operation,
 38361                    defaultContentType,
 38362                    tagsToHide,
 38363                    nsManager);
 364            }
 43365        }
 366
 367        /// <summary>
 368        /// Populate a response for a given method.
 369        /// </summary>
 370        /// <param name="type">The type of the response.</param>
 371        /// <param name="document">The document object that is being built up.</param>
 372        /// <param name="operation">The schema object that is being built up.</param>
 373        /// <param name="defaultContentType">Calculates the default content type.</param>
 374        /// <param name="tagsToHide">Any tags that need to be hidden for some reason.</param>
 375        /// <param name="nsManager">XmlNamespaceManager instance.</param>
 376        /// <param name="responseAttribute">Open API metadata for a response.</param>
 377        private static void PopulateOpenApiResponse(
 378            Type type,
 379            OpenApiDocument document,
 380            OpenApiOperation operation,
 381            DefaultContentType defaultContentType,
 382            IEnumerable<string> tagsToHide,
 383            XmlNamespaceManager nsManager,
 384            OpenApiResponseAttribute responseAttribute = null)
 385        {
 42386            DataContractAttribute dataContractAttribute = type?.GetCustomAttribute<DataContractAttribute>();
 387            OpenApiSchema schemaSchema;
 388
 42389            if (type == null)
 390            {
 2391                schemaSchema = null;
 392            }
 40393            else if (!string.IsNullOrEmpty(dataContractAttribute?.Name))
 394            {
 6395                PopulateOpenApiSchema(document, type, tagsToHide, nsManager);
 396
 6397                schemaSchema = new OpenApiSchema
 6398                {
 6399                    Reference = new OpenApiReference
 6400                    {
 6401                        Type = ReferenceType.Schema,
 6402                        Id = dataContractAttribute.Name
 6403                    }
 6404                };
 405            }
 406            else
 407            {
 34408                schemaSchema = new OpenApiSchema
 34409                {
 34410                    Type = GetType(type)
 34411                };
 412            }
 413
 42414            OpenApiResponse response = new OpenApiResponse
 42415            {
 42416                Description = responseAttribute?.Description
 42417            };
 418
 42419            if (responseAttribute?.ContentTypes != null)
 420            {
 8421                response.Content = responseAttribute.ContentTypes.ToDictionary(contentType => contentType, _ => new Open
 422            }
 40423            else if (schemaSchema?.Reference != null)
 424            {
 15425                response.Content = defaultContentType.GetContentTypes(true).ToDictionary(contentType => contentType, _ =
 426            }
 427
 42428            int statusCode = responseAttribute?.StatusCode == null ? 200 : (int)responseAttribute.StatusCode;
 429
 42430            operation.Responses.Add(statusCode.ToString(CultureInfo.InvariantCulture), response);
 42431        }
 432
 433        /// <summary>
 434        /// Populate the parameters for a given method.
 435        /// </summary>
 436        /// <param name="document">The document object that is being built up.</param>
 437        /// <param name="operation">The schema object that is being built up.</param>
 438        /// <param name="method">The given method.</param>
 439        /// <param name="uriTemplateRaw">The uri template for the method.</param>
 440        /// <param name="defaultContentType">Calculates the default content type.</param>
 441        /// <param name="tagsToHide">Any tags that need to be hidden for some reason.</param>
 442        /// <param name="nsManager">XmlNamespaceManager instance.</param>
 443        private static void PopulateOpenApiParameters(
 444            OpenApiDocument document,
 445            OpenApiOperation operation,
 446            MethodInfo method,
 447            string uriTemplateRaw,
 448            DefaultContentType defaultContentType,
 449            IEnumerable<string> tagsToHide,
 450            XmlNamespaceManager nsManager)
 451        {
 136452            foreach (ParameterInfo parameter in method.GetParameters())
 453            {
 25454                if (operation.Parameters == null)
 455                {
 0456                    operation.Parameters = new List<OpenApiParameter>();
 457                }
 458
 25459                OpenApiParameterAttribute attribute = parameter.GetCustomAttribute<OpenApiParameterAttribute>();
 460
 25461                bool isHidden = false;
 52462                foreach (OpenApiTagAttribute tagAttribute in parameter.GetCustomAttributes<OpenApiTagAttribute>())
 463                {
 1464                    if (tagsToHide.Contains(tagAttribute.Tag))
 465                    {
 1466                        isHidden = true;
 467                    }
 468                }
 469
 25470                OpenApiHiddenAttribute hiddenAttribute = parameter.GetCustomAttribute<OpenApiHiddenAttribute>();
 471
 25472                if (isHidden || hiddenAttribute != null)
 473                {
 474                    continue;
 475                }
 476
 23477                UriTemplate uriTemplate = new UriTemplate(uriTemplateRaw);
 23478                ParameterLocation? parameterLocation = null;
 26479                if (uriTemplate.PathSegmentVariableNames.Any(variableName => string.Equals(variableName, parameter.Name,
 480                {
 2481                    parameterLocation = ParameterLocation.Path;
 482                }
 25483                else if (uriTemplate.QueryValueVariableNames.Any(variableName => string.Equals(variableName, parameter.N
 484                {
 3485                    parameterLocation = ParameterLocation.Query;
 486                }
 487
 23488                Dictionary<string, StringValues> queryString = QueryHelpers.ParseQuery(new Uri("http://microsoft.com" + 
 23489                string name = parameter.Name;
 23490                if (parameterLocation == ParameterLocation.Query && queryString.ContainsValue("{" + parameter.Name + "}"
 491                {
 7492                    KeyValuePair<string, StringValues> queryStringParameter = queryString.First(kvp => kvp.Value == "{" 
 3493                    name = queryStringParameter.Key;
 494                }
 495
 23496                DataContractAttribute dataContractAttribute = parameter.ParameterType.GetCustomAttribute<DataContractAtt
 23497                if (parameterLocation == null)
 498                {
 499                    OpenApiMediaType content;
 500
 18501                    if (!string.IsNullOrEmpty(dataContractAttribute?.Name))
 502                    {
 17503                        PopulateOpenApiSchema(document, parameter.ParameterType, tagsToHide, nsManager);
 504
 17505                        content = new OpenApiMediaType
 17506                        {
 17507                            Schema = new OpenApiSchema
 17508                            {
 17509                                Reference = new OpenApiReference
 17510                                {
 17511                                    Type = ReferenceType.Schema,
 17512                                    Id = dataContractAttribute.Name
 17513                                }
 17514                            }
 17515                        };
 516                    }
 517                    else
 518                    {
 1519                        content = new OpenApiMediaType
 1520                        {
 1521                            Schema = new OpenApiSchema
 1522                            {
 1523                                Type = GetType(parameter.ParameterType)
 1524                            }
 1525                        };
 526                    }
 527
 18528                    if (attribute?.ContentTypes != null)
 529                    {
 16530                        operation.RequestBody = new OpenApiRequestBody
 16531                        {
 32532                            Content = attribute.ContentTypes.ToDictionary(contentType => contentType, _ => content),
 16533                            Required = !parameter.IsOptional,
 16534                            Description = attribute?.Description
 16535                        };
 536                    }
 537                    else
 538                    {
 2539                        operation.RequestBody = new OpenApiRequestBody
 2540                        {
 16541                            Content = defaultContentType.GetContentTypes(false).ToDictionary(contentType => contentType,
 2542                            Required = !parameter.IsOptional,
 2543                            Description = attribute?.Description
 2544                        };
 545                    }
 546                }
 547                else
 548                {
 5549                    operation.Parameters.Add(new OpenApiParameter
 5550                    {
 5551                        Name = name,
 5552                        Description = attribute?.Description,
 5553                        Schema = new OpenApiSchema
 5554                        {
 5555                            Type = GetType(parameter.ParameterType)
 5556                        },
 5557                        In = parameterLocation,
 5558                        Required = !parameter.IsOptional || parameterLocation == ParameterLocation.Path
 5559                    });
 560                }
 561            }
 43562        }
 563
 564        /// <summary>
 565        /// Populate the tags for a given method.
 566        /// </summary>
 567        /// <param name="document">The document object that is being built up.</param>
 568        /// <param name="operation">The operation object that is being built up.</param>
 569        /// <param name="method">The given method.</param>
 570        private static void PopulateOpenApiOperationTags(OpenApiDocument document, OpenApiOperation operation, MethodInf
 571        {
 94572            foreach (OpenApiTagAttribute attribute in method.GetCustomAttributes<OpenApiTagAttribute>())
 573            {
 4574                if (operation.Tags == null)
 575                {
 0576                    operation.Tags = new List<OpenApiTag>();
 577                }
 578
 4579                operation.Tags.Add(new OpenApiTag { Name = attribute.Tag });
 580
 6581                if (!document.Tags.Any(existingTag => existingTag.Name == attribute.Tag))
 582                {
 4583                    document.Tags.Add(new OpenApiTag
 4584                    {
 4585                        Name = attribute.Tag
 4586                    });
 587                }
 588            }
 43589        }
 590
 591        /// <summary>
 592        /// Populate the operations summary for a given method.
 593        /// </summary>
 594        /// <param name="operation">The schema object that is being built up.</param>
 595        /// <param name="method">The given method.</param>
 596        private static void PopulateOpenApiOperationSummary(OpenApiOperation operation, MethodInfo method)
 597        {
 43598            OpenApiOperationAttribute operationSummaryAttribute = method.GetCustomAttribute<OpenApiOperationAttribute>()
 43599            if (operationSummaryAttribute != null)
 600            {
 1601                operation.Summary = operationSummaryAttribute.Summary;
 1602                operation.Description = operationSummaryAttribute.Description;
 603            }
 43604        }
 605
 606        /// <summary>
 607        /// Populate a schema from a given type.
 608        /// </summary>
 609        /// <param name="document">The document object that is being built up.</param>
 610        /// <param name="definition">The given type.</param>
 611        /// <param name="tagsToHide">Any tags that need to be hidden for some reason.</param>
 612        /// <param name="nsManager">XmlNamespaceManager instance.</param>
 613        private static void PopulateOpenApiSchema(OpenApiDocument document, Type definition, IEnumerable<string> tagsToH
 614        {
 615            bool IsContractAndIsNewContract(Type parentType, Type type, HashSet<string> seenKeys, bool isInArray)
 616            {
 37617                DataContractAttribute parentDataContractAttribute = parentType?.GetCustomAttribute<DataContractAttribute
 37618                DataContractAttribute dataContractAttribute = type?.GetCustomAttribute<DataContractAttribute>();
 619
 37620                if (dataContractAttribute?.Name == null)
 621                {
 9622                    return false;
 623                }
 624
 28625                string schemaKey = GetSchemaKey(parentDataContractAttribute, dataContractAttribute, isInArray);
 28626                if (seenKeys.Contains(schemaKey))
 627                {
 0628                    return false;
 629                }
 630                else
 631                {
 28632                    seenKeys.Add(schemaKey);
 633                }
 634
 28635                return !document.Components.Schemas.ContainsKey(GetSchemaKey(parentDataContractAttribute, dataContractAt
 636            }
 637
 16638            bool IsDataMemberProperty(PropertyInfo property) => property.GetCustomAttribute<DataMemberAttribute>() != nu
 639
 23640            HashSet<string> seenKeys = new HashSet<string>();
 641
 23642            if (!IsContractAndIsNewContract(null, definition, seenKeys, false))
 643            {
 4644                return;
 645            }
 646
 19647            Queue<(Type parent, Type type, bool isInArray)> queue = new Queue<(Type parent, Type type, bool isInArary)>(
 19648            queue.Enqueue((null, definition, false));
 649
 43650            while (queue.Any())
 651            {
 24652                (Type parent, Type type, bool isInArray) = queue.Dequeue();
 24653                AddTypeToSchemas(document, parent, type, isInArray, tagsToHide, nsManager);
 654
 70655                foreach (PropertyInfo property in type.GetProperties().Where(IsDataMemberProperty))
 656                {
 11657                    if (IsContractAndIsNewContract(type, property.PropertyType, seenKeys, false))
 658                    {
 3659                        queue.Enqueue((type, property.PropertyType, false));
 660                    }
 8661                    else if (
 8662                        property.PropertyType.GetInterface("IEnumerable") != null &&
 8663                        property.PropertyType != typeof(string) &&
 8664                        IsContractAndIsNewContract(type, property.PropertyType.GetGenericArguments().FirstOrDefault(), s
 665                    {
 2666                        queue.Enqueue((type, property.PropertyType.GetGenericArguments().FirstOrDefault(), true));
 667                    }
 668                }
 669            }
 19670        }
 671
 672        /// <summary>
 673        /// Get the type to store a schema under.
 674        /// </summary>
 675        /// <param name="parentDataContractAttribute">The data contract attribute for the parent of the type to add.</pa
 676        /// <param name="dataContractAttribute">The data contract attribute for the type to add.</param>
 677        /// <param name="dataContractAttribute">Whether the type is wrapped in an array or not.</param>
 678        /// <returns>The key to store the schema under.</returns>
 679        private static string GetSchemaKey(DataContractAttribute parentDataContractAttribute, DataContractAttribute data
 680        {
 85681            if (parentDataContractAttribute != null && isInArray)
 682            {
 8683                return $"{parentDataContractAttribute.Name}-Array-{dataContractAttribute.Name}";
 684            }
 77685            else if (parentDataContractAttribute != null)
 686            {
 12687                return $"{parentDataContractAttribute.Name}-{dataContractAttribute.Name}";
 688            }
 689            else
 690            {
 65691                return dataContractAttribute.Name;
 692            }
 693        }
 694
 695        /// <summary>
 696        /// Add a specific definition to the schemas section.
 697        /// </summary>
 698        /// <param name="document">The document object that is being built up.</param>
 699        /// <param name="parent">The parent of the type to add.</param>
 700        /// <param name="type">The type to add.</param>
 701        /// <param name="type">Whether the type is in an array.</param>
 702        /// <param name="tagsToHide">Any tags that need to be hidden for some reason.</param>
 703        /// <param name="nsManager">XmlNamespaceManager instance.</param>
 704        private static void AddTypeToSchemas(OpenApiDocument document, Type parent, Type type, bool isInArray, IEnumerab
 705        {
 24706            DataContractAttribute parentDataContractAttribute = parent?.GetCustomAttribute<DataContractAttribute>();
 24707            DataContractAttribute dataContractAttribute = type.GetCustomAttribute<DataContractAttribute>();
 708
 24709            string parentNs = FindNamespace(parentDataContractAttribute?.Namespace ?? dataContractAttribute?.Namespace, 
 24710            string parentPrefix = FindPrefix(nsManager, parentNs);
 711
 24712            string ns = FindNamespace(dataContractAttribute?.Namespace, type);
 24713            string prefix = FindPrefix(nsManager, ns);
 714
 24715            Dictionary<string, IOpenApiExtension> xmlBlock = isInArray ?
 24716                new Dictionary<string, IOpenApiExtension>
 24717                {
 24718                    {"xml", new OpenApiObject
 24719                        {
 24720                            {"name", new OpenApiString(dataContractAttribute?.Name ?? type.Name)},
 24721                            {"namespace", new OpenApiString(ns)},
 24722                            {"prefix", new OpenApiString(prefix)}
 24723                        }
 24724                    }
 24725                } :
 24726                new Dictionary<string, IOpenApiExtension>
 24727                {
 24728                    {"xml", new OpenApiObject
 24729                        {
 24730                            {"namespace", new OpenApiString(parentNs)},
 24731                            {"prefix", new OpenApiString(parentPrefix)}
 24732                        }
 24733                    }
 24734                };
 735
 24736            SortedSet<string> required = new SortedSet<string>();
 24737            OpenApiSchema definitionSchema = new OpenApiSchema
 24738            {
 24739                Type = "object",
 24740                Properties = new Dictionary<string, OpenApiSchema>(),
 24741                // The URI type might mangle the namespace so we do this manually.
 24742                Extensions = xmlBlock,
 24743            };
 744
 24745            IEnumerable<(PropertyInfo, DataMemberAttribute)> properties = type
 24746                .GetProperties()
 16747                .Select(property => (Property: property, DataMemberAttribute: property.GetCustomAttribute<DataMemberAttr
 16748                .Where(property => property.DataMemberAttribute != null)
 35749                .OrderBy(property => property.DataMemberAttribute.Order);
 750
 70751            foreach ((PropertyInfo property, DataMemberAttribute dataMemberAttribute) in properties)
 752            {
 11753                OpenApiHiddenAttribute hiddenAttribute = property.GetCustomAttribute<OpenApiHiddenAttribute>();
 11754                if (hiddenAttribute != null)
 755                {
 756                    continue;
 757                }
 758
 10759                bool isHidden = false;
 24760                foreach (OpenApiTagAttribute tagAttribute in property.GetCustomAttributes<OpenApiTagAttribute>())
 761                {
 2762                    if (tagsToHide.Contains(tagAttribute.Tag))
 763                    {
 1764                        isHidden = true;
 765                    }
 766                }
 767
 10768                if (isHidden)
 769                {
 770                    continue;
 771                }
 772
 9773                string name = dataMemberAttribute.Name ?? property.Name;
 774
 9775                OpenApiPropertyAttribute memberPropertiesAttribute = property.GetCustomAttribute<OpenApiPropertyAttribut
 776
 9777                IEnumerable<CustomAttributeNamedArgument> memberPropertiesAttributeData = property
 9778                        .GetCustomAttributesData()
 14779                        .FirstOrDefault(data => data.AttributeType == typeof(OpenApiPropertyAttribute))
 9780                        ?.NamedArguments;
 21781                bool maxLengthSet = memberPropertiesAttributeData?.Any(arg => arg.MemberName == "MaxLength") ?? false;
 20782                bool minLengthSet = memberPropertiesAttributeData?.Any(arg => arg.MemberName == "MinLength") ?? false;
 783
 9784                if (memberPropertiesAttribute?.IsRequired ?? false)
 785                {
 5786                    required.Add(name);
 787                }
 788
 9789                DataContractAttribute innerDataContractAttribute = property.PropertyType.GetCustomAttribute<DataContract
 9790                if (innerDataContractAttribute?.Name != null)
 791                {
 3792                    DataContractAttribute innerDataMemberAttribute = property.PropertyType.GetCustomAttribute<DataContra
 793
 3794                    definitionSchema.Properties.Add(name, new OpenApiSchema
 3795                    {
 3796                        Reference = new OpenApiReference
 3797                        {
 3798                            Type = ReferenceType.Schema,
 3799                            Id = GetSchemaKey(dataContractAttribute, innerDataMemberAttribute, false)
 3800                        },
 3801                        Description = memberPropertiesAttribute?.Description
 3802                    });
 803                }
 6804                else if (property.PropertyType.GetInterface("IEnumerable") != null && property.PropertyType != typeof(st
 805                {
 806                    // Handles the case of a custom collection that derives from a specialized generic collection.
 3807                    Type innerType = property.PropertyType.GetGenericArguments().FirstOrDefault();
 3808                    if (innerType == null && property.PropertyType.BaseType != null)
 809                    {
 0810                        innerType = property.PropertyType.BaseType.GetGenericArguments().FirstOrDefault();
 811                    }
 812
 3813                    if (innerType != null)
 814                    {
 3815                        DataContractAttribute innerDataMemberAttribute = innerType.GetCustomAttribute<DataContractAttrib
 3816                        if (innerDataMemberAttribute != null)
 817                        {
 2818                            definitionSchema.Properties.Add(name, new OpenApiSchema
 2819                            {
 2820                                Type = "array",
 2821                                Description = memberPropertiesAttribute?.Description,
 2822                                Items = new OpenApiSchema
 2823                                {
 2824                                    Reference = new OpenApiReference
 2825                                    {
 2826                                        Type = ReferenceType.Schema,
 2827                                        Id = GetSchemaKey(dataContractAttribute, innerDataMemberAttribute, true),
 2828                                    },
 2829                                    Xml = new OpenApiXml
 2830                                    {
 2831                                        Namespace = new Uri(ArrayNamespace),
 2832                                        Name = innerDataMemberAttribute.Name,
 2833                                        Prefix = FindPrefix(nsManager, ArrayNamespace)
 2834                                    }
 2835                                },
 2836                                // The URI type might mangle the namespace so we do this manually.
 2837                                Extensions = new Dictionary<string, IOpenApiExtension>
 2838                                {
 2839                                    {"xml", new OpenApiObject
 2840                                        {
 2841                                            {"name", new OpenApiString(name) },
 2842                                            {"namespace", new OpenApiString(parentNs)},
 2843                                            {"prefix", new OpenApiString(parentPrefix)},
 2844                                            {"wrapped", new OpenApiBoolean(true) }
 2845                                        }
 2846                                    }
 2847                                },
 2848                            });
 849                        }
 850                        else
 851                        {
 1852                            string openApiType = GetType(innerType);
 853
 1854                            if (!string.IsNullOrEmpty(openApiType))
 855                            {
 1856                                definitionSchema.Properties.Add(name, new OpenApiSchema
 1857                                {
 1858                                    Type = "array",
 1859                                    Description = memberPropertiesAttribute?.Description,
 1860                                    Items = new OpenApiSchema
 1861                                    {
 1862                                        Type = openApiType,
 1863                                        Xml = new OpenApiXml
 1864                                        {
 1865                                            Namespace = new Uri(ArrayNamespace),
 1866                                            Name = innerType.Name.ToLower(),
 1867                                            Prefix = FindPrefix(nsManager, ArrayNamespace)
 1868                                        }
 1869                                    },
 1870                                    // The URI type might mangle the namespace so we do this manually.
 1871                                    Extensions = new Dictionary<string, IOpenApiExtension>
 1872                                    {
 1873                                        {"xml", new OpenApiObject
 1874                                            {
 1875                                                {"name", new OpenApiString(name) },
 1876                                                {"namespace", new OpenApiString(parentNs)},
 1877                                                {"prefix", new OpenApiString(parentPrefix)},
 1878                                                {"wrapped", new OpenApiBoolean(true) }
 1879                                            }
 1880                                        }
 1881                                    },
 1882                                }); ;
 883                            }
 884                        }
 885                    }
 886                }
 3887                else if (property.PropertyType.IsEnum)
 888                {
 1889                    List<IOpenApiAny> enumValues = new List<IOpenApiAny>();
 6890                    foreach (object value in Enum.GetValues(property.PropertyType))
 891                    {
 2892                        enumValues.Add(new OpenApiString(value.ToString()));
 893                    }
 894
 1895                    definitionSchema.Properties.Add(name, new OpenApiSchema
 1896                    {
 1897                        Type = "string",
 1898                        Description = memberPropertiesAttribute?.Description,
 1899                        Enum = enumValues,
 1900                        // The URI type might mangle the namespace so we do this manually.
 1901                        Extensions = new Dictionary<string, IOpenApiExtension>
 1902                        {
 1903                            {"xml", new OpenApiObject
 1904                                {
 1905                                    {"namespace", new OpenApiString(ns)},
 1906                                    {"prefix", new OpenApiString(prefix)}
 1907                                }
 1908                            }
 1909                        }
 1910                    });
 911                }
 912                else
 913                {
 2914                    definitionSchema.Properties.Add(name, new OpenApiSchema
 2915                    {
 2916                        Type = GetType(property.PropertyType),
 2917                        Description = memberPropertiesAttribute?.Description,
 2918                        MinLength = minLengthSet ? memberPropertiesAttribute?.MinLength : null,
 2919                        MaxLength = maxLengthSet ? memberPropertiesAttribute?.MaxLength : null,
 2920                        Format = memberPropertiesAttribute?.Format,
 2921                        // The URI type might mangle the namespace so we do this manually.
 2922                        Extensions = new Dictionary<string, IOpenApiExtension>
 2923                        {
 2924                            {"xml", new OpenApiObject
 2925                                {
 2926                                    {"namespace", new OpenApiString(ns)},
 2927                                    {"prefix", new OpenApiString(prefix)}
 2928                                }
 2929                            }
 2930                        }
 2931                    });
 932                }
 933            }
 934
 24935            definitionSchema.Required = required.Count > 0 ? required : null;
 936
 24937            document.Components.Schemas.Add(GetSchemaKey(parentDataContractAttribute, dataContractAttribute, isInArray),
 24938        }
 939
 940        /// <summary>
 941        /// Figure out the valid namespace for XML serialization.
 942        /// </summary>
 943        /// <param name="ns">The namespace from the data contract.</param>
 944        /// <param name="type">The type itself.</param>
 945        /// <returns>A valid XML namespace if applicable.</returns>
 946        private static string FindNamespace(string ns, Type type)
 947        {
 48948            if (ns == null)
 949            {
 48950                return $"{DataContractNamespace}{type.Namespace}";
 951            }
 952
 0953            return ns;
 954        }
 955
 956        /// <summary>
 957        /// Figure out a valid prefix for XML serialization.
 958        /// </summary>
 959        /// <param name="nsManager">XmlNamespaceManager instance.</param>
 960        /// <param name="ns">Valid namespace for XML serialization.</param>
 961        /// <returns>A valid XML prefix.</returns>
 962        private static string FindPrefix(XmlNamespaceManager nsManager, string ns)
 963        {
 51964            string prefix = nsManager.LookupPrefix(ns);
 51965            if (!string.IsNullOrEmpty(prefix))
 966            {
 29967                return prefix;
 968            }
 969
 22970            int index = 0;
 22971            IEnumerator enumerator = nsManager.GetEnumerator();
 91972            while (enumerator.MoveNext())
 973            {
 69974                index++;
 975            }
 22976            index++;
 977
 22978            prefix = $"ns{index}";
 22979            nsManager.AddNamespace(prefix, ns);
 22980            return prefix;
 981        }
 982
 983        /// <summary>
 984        /// Map a .NET type to JSON schema type.
 985        /// </summary>
 986        /// <param name="type">The type to be mapped.</param>
 987        /// <returns>The mapped type.</returns>
 988        private static string GetType(Type type)
 989        {
 43990            if (type == null)
 991            {
 0992                return null;
 993            }
 994
 43995            Type actualType = IsNullable(type) ? Nullable.GetUnderlyingType(type) : type;
 996
 43997            if (actualType == typeof(int) || actualType == typeof(long) || actualType == typeof(short) || actualType == 
 998            {
 0999                return "integer";
 1000            }
 431001            else if (actualType == typeof(float) || actualType == typeof(double) || actualType == typeof(decimal))
 1002            {
 01003                return "number";
 1004            }
 431005            else if (actualType == typeof(string) || actualType == typeof(DateTime) || actualType == typeof(Stream) || a
 1006            {
 111007                return "string";
 1008            }
 321009            else if (actualType == typeof(bool))
 1010            {
 01011                return "boolean";
 1012            }
 1013
 321014            return null;
 1015        }
 1016
 1017        /// <summary>
 1018        /// Check if a type is nullable.
 1019        /// </summary>
 1020        /// <param name="type">The type to check.</param>
 1021        /// <returns>Whether the type is nullable.</returns>
 431022        private static bool IsNullable(Type type) => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nul
 1023
 1024        /// <summary>
 1025        /// Decides what the default content type should be.
 1026        /// </summary>
 1027        private class DefaultContentType
 1028        {
 1029            /// <summary>
 1030            /// Whether the format was explicitly set in the WebGet/WebInvoke attribute for the response
 1031            /// </summary>
 481032            public bool ResponseFormatExplicitlySet { get; set; }
 1033
 1034            /// <summary>
 1035            /// The format set in the WebGet/WebInvoke attribute for the response.
 1036            /// </summary>
 451037            public WebMessageFormat ResponseAttributeFormat { get; set; }
 1038
 1039            /// <summary>
 1040            /// The default format in the WebHttpBehavior for the response.
 1041            /// </summary>
 461042            public WebMessageFormat ResponseBehaviorFormat { get; set; }
 1043
 1044            /// <summary>
 1045            /// Whether the format was explicitly set in the WebGet/WebInvoke attribute for the request
 1046            /// </summary>
 21047            public bool RequestFormatExplicitlySet { get; set; }
 1048
 1049            /// <summary>
 1050            /// The format set in the WebGet/WebInvoke attribute for the request.
 1051            /// </summary>
 01052            public WebMessageFormat RequestAttributeFormat { get; set; }
 1053
 1054            /// <summary>
 1055            /// The content type the default response should have.
 1056            /// </summary>
 1057            public IEnumerable<string> GetContentTypes(bool isResponse)
 1058            {
 71059                if (isResponse)
 1060                {
 51061                    WebMessageFormat format = ResponseFormatExplicitlySet ? ResponseAttributeFormat : ResponseBehaviorFo
 1062                    switch (format)
 1063                    {
 1064                        case WebMessageFormat.Json:
 31065                            return new string[] { "application/json" };
 1066                        case WebMessageFormat.Xml:
 21067                            return new string[] { "application/xml" };
 1068                        default:
 01069                            return null;
 1070                    }
 1071                }
 1072                else
 1073                {
 21074                    if (RequestFormatExplicitlySet)
 1075                    {
 01076                        switch (RequestAttributeFormat)
 1077                        {
 1078                            case WebMessageFormat.Json:
 01079                                return new string[] { "application/json", "text/json" };
 1080                            case WebMessageFormat.Xml:
 01081                                return new string[] { "application/xml", "text/xml" };
 1082                            default:
 01083                                return null;
 1084                        }
 1085                    }
 1086                    else
 1087                    {
 21088                        return new string[] { "application/json", "text/json", "application/xml", "text/xml" };
 1089                    }
 1090                }
 1091            }
 1092        }
 1093
 1094        /// <summary>
 1095        /// Information about an operation.
 1096        /// </summary>
 1097        private class OperationInfo
 1098        {
 1099            /// <summary>
 1100            /// Operation method.
 1101            /// </summary>
 1811102            public string Method { get; set; }
 1103
 1104            /// <summary>
 1105            /// Operation URI.
 1106            /// </summary>
 1771107            public string UriTemplate { get; set; }
 1108
 1109            /// <summary>
 1110            /// Whether the response format was explicitly set.
 1111            /// </summary>
 891112            public bool IsResponseFormatSetExplicitly { get; set; }
 1113
 1114            /// <summary>
 1115            /// The response format.
 1116            /// </summary>
 891117            public WebMessageFormat ResponseFormat { get; set; }
 1118
 1119            /// <summary>
 1120            /// Whether the request format was explicitly set.
 1121            /// </summary>
 461122            public bool IsRequestFormatSetExplicitly { get; set; }
 1123
 1124            /// <summary>
 1125            /// The request format.
 1126            /// </summary>
 461127            public WebMessageFormat RequestFormat { get; set; }
 1128        }
 1129    }
 1130}

Methods/Properties

BuildOpenApiSpecificationDocument(CoreWCF.OpenApi.OpenApiOptions,System.Collections.Generic.IEnumerable`1<CoreWCF.OpenApi.OpenApiContractInfo>)
PopulateOpenApiInfo(Microsoft.OpenApi.Models.OpenApiDocument,CoreWCF.OpenApi.OpenApiOptions)
PopulateOpenApiPathsOperations(Microsoft.OpenApi.Models.OpenApiDocument,System.Collections.Generic.IEnumerable`1<CoreWCF.OpenApi.OpenApiContractInfo>,System.Collections.Generic.IEnumerable`1<System.String>)
PopulateOpenApiPath(Microsoft.OpenApi.Models.OpenApiDocument,System.Reflection.MethodInfo,System.Collections.Generic.IEnumerable`1<System.String>,System.String,CoreWCF.Web.WebMessageFormat,System.Func`2<System.Reflection.MethodInfo,CoreWCF.OpenApi.OpenApiSchemaBuilder/OperationInfo>)
GetOperationType(System.String)
GetMethodUriWebGet(System.Reflection.MethodInfo)
GetMethodUriWebInvoke(System.Reflection.MethodInfo)
PopulateOpenApiResponses(Microsoft.OpenApi.Models.OpenApiDocument,Microsoft.OpenApi.Models.OpenApiOperation,System.Reflection.MethodInfo,CoreWCF.OpenApi.OpenApiSchemaBuilder/DefaultContentType,System.Collections.Generic.IEnumerable`1<System.String>,System.Xml.XmlNamespaceManager)
PopulateOpenApiResponse(System.Type,Microsoft.OpenApi.Models.OpenApiDocument,Microsoft.OpenApi.Models.OpenApiOperation,CoreWCF.OpenApi.OpenApiSchemaBuilder/DefaultContentType,System.Collections.Generic.IEnumerable`1<System.String>,System.Xml.XmlNamespaceManager,CoreWCF.OpenApi.Attributes.OpenApiResponseAttribute)
PopulateOpenApiParameters(Microsoft.OpenApi.Models.OpenApiDocument,Microsoft.OpenApi.Models.OpenApiOperation,System.Reflection.MethodInfo,System.String,CoreWCF.OpenApi.OpenApiSchemaBuilder/DefaultContentType,System.Collections.Generic.IEnumerable`1<System.String>,System.Xml.XmlNamespaceManager)
PopulateOpenApiOperationTags(Microsoft.OpenApi.Models.OpenApiDocument,Microsoft.OpenApi.Models.OpenApiOperation,System.Reflection.MethodInfo)
PopulateOpenApiOperationSummary(Microsoft.OpenApi.Models.OpenApiOperation,System.Reflection.MethodInfo)
IsDataMemberProperty(System.Reflection.PropertyInfo)
PopulateOpenApiSchema(Microsoft.OpenApi.Models.OpenApiDocument,System.Type,System.Collections.Generic.IEnumerable`1<System.String>,System.Xml.XmlNamespaceManager)
GetSchemaKey(System.Runtime.Serialization.DataContractAttribute,System.Runtime.Serialization.DataContractAttribute,System.Boolean)
AddTypeToSchemas(Microsoft.OpenApi.Models.OpenApiDocument,System.Type,System.Type,System.Boolean,System.Collections.Generic.IEnumerable`1<System.String>,System.Xml.XmlNamespaceManager)
FindNamespace(System.String,System.Type)
FindPrefix(System.Xml.XmlNamespaceManager,System.String)
GetType(System.Type)
IsNullable(System.Type)
ResponseFormatExplicitlySet()
ResponseAttributeFormat()
ResponseBehaviorFormat()
RequestFormatExplicitlySet()
RequestAttributeFormat()
GetContentTypes(System.Boolean)
Method()
UriTemplate()
IsResponseFormatSetExplicitly()
ResponseFormat()
IsRequestFormatSetExplicitly()
RequestFormat()