| | | 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 | | |
| | | 4 | | using System; |
| | | 5 | | using System.Collections; |
| | | 6 | | using System.Collections.Generic; |
| | | 7 | | using System.Globalization; |
| | | 8 | | using System.IO; |
| | | 9 | | using System.Linq; |
| | | 10 | | using System.Reflection; |
| | | 11 | | using System.Runtime.Serialization; |
| | | 12 | | using System.Text.RegularExpressions; |
| | | 13 | | using System.Threading.Tasks; |
| | | 14 | | using System.Xml; |
| | | 15 | | using CoreWCF.Description; |
| | | 16 | | using CoreWCF.OpenApi.Attributes; |
| | | 17 | | using CoreWCF.Web; |
| | | 18 | | using Microsoft.AspNetCore.WebUtilities; |
| | | 19 | | using Microsoft.Extensions.Primitives; |
| | | 20 | | using Microsoft.OpenApi.Any; |
| | | 21 | | using Microsoft.OpenApi.Interfaces; |
| | | 22 | | using Microsoft.OpenApi.Models; |
| | | 23 | | |
| | | 24 | | namespace 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 | | { |
| | 42 | 43 | | if (info == null) |
| | | 44 | | { |
| | 0 | 45 | | throw new ArgumentNullException(nameof(info)); |
| | | 46 | | } |
| | | 47 | | |
| | 42 | 48 | | if (contracts == null) |
| | | 49 | | { |
| | 0 | 50 | | throw new ArgumentNullException(nameof(contracts)); |
| | | 51 | | } |
| | | 52 | | |
| | 42 | 53 | | OpenApiDocument document = new OpenApiDocument |
| | 42 | 54 | | { |
| | 42 | 55 | | Components = new OpenApiComponents(), |
| | 42 | 56 | | Paths = new OpenApiPaths() |
| | 42 | 57 | | }; |
| | | 58 | | |
| | 42 | 59 | | PopulateOpenApiInfo(document, info); |
| | 42 | 60 | | PopulateOpenApiPathsOperations(document, contracts, info.TagsToHide); |
| | | 61 | | |
| | 42 | 62 | | if (info.TagsSorter != null) |
| | | 63 | | { |
| | 1 | 64 | | var tags = document.Tags as List<OpenApiTag> ?? document.Tags.ToList(); |
| | 1 | 65 | | tags.Sort(info.TagsSorter); |
| | 1 | 66 | | document.Tags = tags; |
| | | 67 | | } |
| | | 68 | | |
| | 42 | 69 | | 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 | | { |
| | 42 | 79 | | document.Info = new OpenApiInfo |
| | 42 | 80 | | { |
| | 42 | 81 | | Version = info.Version ?? "", |
| | 42 | 82 | | Description = info.Description, |
| | 42 | 83 | | Title = info.Title ?? "", |
| | 42 | 84 | | TermsOfService = info.TermsOfService |
| | 42 | 85 | | }; |
| | | 86 | | |
| | 42 | 87 | | if (info.ContactName != null || info.ContactEmail != null || info.ContactUrl != null) |
| | | 88 | | { |
| | 1 | 89 | | document.Info.Contact = new OpenApiContact |
| | 1 | 90 | | { |
| | 1 | 91 | | Name = info.ContactName, |
| | 1 | 92 | | Email = info.ContactEmail, |
| | 1 | 93 | | Url = info.ContactUrl |
| | 1 | 94 | | }; |
| | | 95 | | } |
| | | 96 | | |
| | 42 | 97 | | if (info.LicenseName != null) |
| | | 98 | | { |
| | 1 | 99 | | document.Info.License = new OpenApiLicense |
| | 1 | 100 | | { |
| | 1 | 101 | | Name = info.LicenseName, |
| | 1 | 102 | | Url = info.LiceneUrl |
| | 1 | 103 | | }; |
| | | 104 | | } |
| | | 105 | | |
| | 42 | 106 | | if (info.ExternalDocumentUrl != null) |
| | | 107 | | { |
| | 1 | 108 | | document.ExternalDocs = new OpenApiExternalDocs |
| | 1 | 109 | | { |
| | 1 | 110 | | Description = info.ExternalDocumentDescription, |
| | 1 | 111 | | Url = info.ExternalDocumentUrl |
| | 1 | 112 | | }; |
| | | 113 | | } |
| | 42 | 114 | | } |
| | | 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 | | { |
| | 160 | 124 | | foreach (OpenApiContractInfo contractInfo in contracts) |
| | | 125 | | { |
| | 38 | 126 | | List<MethodInfo> methods = new List<MethodInfo>(); |
| | 80 | 127 | | foreach (Type interfaceInfo in contractInfo.Contract.GetInterfaces()) |
| | | 128 | | { |
| | 2 | 129 | | methods.AddRange(interfaceInfo.GetMethods()); |
| | | 130 | | } |
| | 38 | 131 | | methods.AddRange(contractInfo.Contract.GetMethods()); |
| | | 132 | | |
| | 38 | 133 | | OpenApiBasePathAttribute basePathAttribute = contractInfo.Contract.GetCustomAttribute<OpenApiBasePathAtt |
| | | 134 | | |
| | 168 | 135 | | foreach (MethodInfo method in methods) |
| | | 136 | | { |
| | 46 | 137 | | PopulateOpenApiPath( |
| | 46 | 138 | | document, |
| | 46 | 139 | | method, |
| | 46 | 140 | | tagsToHide, |
| | 46 | 141 | | basePathAttribute?.BasePath, |
| | 46 | 142 | | contractInfo.ResponseFormat, |
| | 46 | 143 | | GetMethodUriWebGet); |
| | | 144 | | |
| | 46 | 145 | | PopulateOpenApiPath( |
| | 46 | 146 | | document, |
| | 46 | 147 | | method, |
| | 46 | 148 | | tagsToHide, |
| | 46 | 149 | | basePathAttribute?.BasePath, |
| | 46 | 150 | | contractInfo.ResponseFormat, |
| | 46 | 151 | | GetMethodUriWebInvoke); |
| | | 152 | | } |
| | | 153 | | } |
| | 42 | 154 | | } |
| | | 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 | | { |
| | 92 | 173 | | OperationInfo operationInfo = getOperationInfo(methodInfo); |
| | 92 | 174 | | if (operationInfo.Method == null || operationInfo.UriTemplate == null) |
| | | 175 | | { |
| | 47 | 176 | | return; |
| | | 177 | | } |
| | | 178 | | |
| | 45 | 179 | | if (methodInfo.GetCustomAttribute<OpenApiHiddenAttribute>() != null) |
| | | 180 | | { |
| | 1 | 181 | | return; |
| | | 182 | | } |
| | | 183 | | |
| | 99 | 184 | | foreach (OpenApiTagAttribute tagAttribute in methodInfo.GetCustomAttributes<OpenApiTagAttribute>()) |
| | | 185 | | { |
| | 6 | 186 | | if (tagsToHide.Contains(tagAttribute.Tag)) |
| | | 187 | | { |
| | 1 | 188 | | return; |
| | | 189 | | } |
| | | 190 | | } |
| | | 191 | | |
| | 43 | 192 | | OpenApiOperation operation = new OpenApiOperation(); |
| | | 193 | | |
| | 43 | 194 | | string uri = Regex.Replace(operationInfo.UriTemplate, @"\?.*", ""); |
| | | 195 | | |
| | 43 | 196 | | if (!string.IsNullOrEmpty(additionalBasePath)) |
| | | 197 | | { |
| | 0 | 198 | | uri = additionalBasePath + uri; |
| | | 199 | | } |
| | | 200 | | |
| | 43 | 201 | | DefaultContentType defaultContentType = new DefaultContentType |
| | 43 | 202 | | { |
| | 43 | 203 | | ResponseFormatExplicitlySet = operationInfo.IsResponseFormatSetExplicitly, |
| | 43 | 204 | | ResponseAttributeFormat = operationInfo.ResponseFormat, |
| | 43 | 205 | | ResponseBehaviorFormat = behaviorFormat |
| | 43 | 206 | | }; |
| | | 207 | | |
| | 43 | 208 | | NameTable table = new NameTable(); |
| | 43 | 209 | | XmlNamespaceManager nsManager = new XmlNamespaceManager(table); |
| | | 210 | | |
| | 43 | 211 | | PopulateOpenApiResponses(document, operation, methodInfo, defaultContentType, tagsToHide, nsManager); |
| | 43 | 212 | | PopulateOpenApiParameters(document, operation, methodInfo, operationInfo.UriTemplate, defaultContentType, ta |
| | 43 | 213 | | PopulateOpenApiOperationTags(document, operation, methodInfo); |
| | 43 | 214 | | PopulateOpenApiOperationSummary(operation, methodInfo); |
| | | 215 | | |
| | 43 | 216 | | OperationType? operationType = GetOperationType(operationInfo.Method); |
| | 43 | 217 | | if (operationType.HasValue && document.Paths.ContainsKey(uri)) |
| | | 218 | | { |
| | 1 | 219 | | if (!document.Paths[uri].Operations.ContainsKey(operationType.Value)) |
| | | 220 | | { |
| | 1 | 221 | | document.Paths[uri].Operations.Add(operationType.Value, operation); |
| | | 222 | | } |
| | | 223 | | } |
| | 42 | 224 | | else if (operationType.HasValue) |
| | | 225 | | { |
| | 42 | 226 | | document.Paths.Add(uri, new OpenApiPathItem |
| | 42 | 227 | | { |
| | 42 | 228 | | Operations = new Dictionary<OperationType, OpenApiOperation> |
| | 42 | 229 | | { |
| | 42 | 230 | | { operationType.Value, operation } |
| | 42 | 231 | | } |
| | 42 | 232 | | }); |
| | | 233 | | } |
| | 43 | 234 | | } |
| | | 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 | | { |
| | 43 | 243 | | switch (method.ToLower()) |
| | | 244 | | { |
| | | 245 | | case "get": |
| | 22 | 246 | | return OperationType.Get; |
| | | 247 | | case "put": |
| | 0 | 248 | | return OperationType.Put; |
| | | 249 | | case "post": |
| | 21 | 250 | | return OperationType.Post; |
| | | 251 | | case "delete": |
| | 0 | 252 | | return OperationType.Delete; |
| | | 253 | | case "options": |
| | 0 | 254 | | return OperationType.Options; |
| | | 255 | | case "head": |
| | 0 | 256 | | return OperationType.Head; |
| | | 257 | | case "patch": |
| | 0 | 258 | | return OperationType.Patch; |
| | | 259 | | case "trace": |
| | 0 | 260 | | return OperationType.Trace; |
| | | 261 | | default: |
| | 0 | 262 | | 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 | | { |
| | 46 | 273 | | WebGetAttribute attribute = methodInfo.GetCustomAttribute<WebGetAttribute>() |
| | 46 | 274 | | ?? WebHttpServiceModelCompat.GetNativeAttribute<WebGetAttribute>(methodInfo); |
| | | 275 | | |
| | 46 | 276 | | if (attribute == null) |
| | | 277 | | { |
| | 22 | 278 | | return new OperationInfo(); |
| | | 279 | | } |
| | | 280 | | |
| | 24 | 281 | | return new OperationInfo |
| | 24 | 282 | | { |
| | 24 | 283 | | Method = "get", |
| | 24 | 284 | | UriTemplate = attribute.UriTemplate, |
| | 24 | 285 | | IsResponseFormatSetExplicitly = attribute.IsResponseFormatSetExplicitly, |
| | 24 | 286 | | ResponseFormat = attribute.ResponseFormat, |
| | 24 | 287 | | IsRequestFormatSetExplicitly = attribute.IsRequestFormatSetExplicitly, |
| | 24 | 288 | | RequestFormat = attribute.RequestFormat |
| | 24 | 289 | | }; |
| | | 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 | | { |
| | 46 | 299 | | WebInvokeAttribute attribute = methodInfo.GetCustomAttribute<WebInvokeAttribute>() |
| | 46 | 300 | | ?? WebHttpServiceModelCompat.GetNativeAttribute<WebInvokeAttribute>(methodInfo); |
| | | 301 | | |
| | 46 | 302 | | if (attribute == null) |
| | | 303 | | { |
| | 24 | 304 | | return new OperationInfo(); |
| | | 305 | | } |
| | | 306 | | |
| | 22 | 307 | | return new OperationInfo |
| | 22 | 308 | | { |
| | 22 | 309 | | Method = attribute.Method?.ToLower(CultureInfo.InvariantCulture), |
| | 22 | 310 | | UriTemplate = attribute.UriTemplate, |
| | 22 | 311 | | IsResponseFormatSetExplicitly = attribute.IsResponseFormatSetExplicitly, |
| | 22 | 312 | | ResponseFormat = attribute.ResponseFormat, |
| | 22 | 313 | | IsRequestFormatSetExplicitly = attribute.IsRequestFormatSetExplicitly, |
| | 22 | 314 | | RequestFormat = attribute.RequestFormat |
| | 22 | 315 | | }; |
| | | 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 | | { |
| | 43 | 334 | | IEnumerable<OpenApiResponseAttribute> attributes = method.GetCustomAttributes<OpenApiResponseAttribute>(); |
| | 43 | 335 | | if (attributes.Any()) |
| | | 336 | | { |
| | 16 | 337 | | foreach (OpenApiResponseAttribute responseAttribute in method.GetCustomAttributes<OpenApiResponseAttribu |
| | | 338 | | { |
| | 4 | 339 | | PopulateOpenApiResponse( |
| | 4 | 340 | | responseAttribute.Type, |
| | 4 | 341 | | document, |
| | 4 | 342 | | operation, |
| | 4 | 343 | | defaultContentType, |
| | 4 | 344 | | tagsToHide, |
| | 4 | 345 | | nsManager, |
| | 4 | 346 | | responseAttribute); |
| | | 347 | | } |
| | | 348 | | } |
| | 39 | 349 | | else if (method.ReturnType != null && method.ReturnType != typeof(Task)) |
| | | 350 | | { |
| | 38 | 351 | | Type type = method.ReturnType; |
| | 38 | 352 | | if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>)) |
| | | 353 | | { |
| | 1 | 354 | | type = type.GetGenericArguments()[0]; |
| | | 355 | | } |
| | | 356 | | |
| | 38 | 357 | | PopulateOpenApiResponse( |
| | 38 | 358 | | type, |
| | 38 | 359 | | document, |
| | 38 | 360 | | operation, |
| | 38 | 361 | | defaultContentType, |
| | 38 | 362 | | tagsToHide, |
| | 38 | 363 | | nsManager); |
| | | 364 | | } |
| | 43 | 365 | | } |
| | | 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 | | { |
| | 42 | 386 | | DataContractAttribute dataContractAttribute = type?.GetCustomAttribute<DataContractAttribute>(); |
| | | 387 | | OpenApiSchema schemaSchema; |
| | | 388 | | |
| | 42 | 389 | | if (type == null) |
| | | 390 | | { |
| | 2 | 391 | | schemaSchema = null; |
| | | 392 | | } |
| | 40 | 393 | | else if (!string.IsNullOrEmpty(dataContractAttribute?.Name)) |
| | | 394 | | { |
| | 6 | 395 | | PopulateOpenApiSchema(document, type, tagsToHide, nsManager); |
| | | 396 | | |
| | 6 | 397 | | schemaSchema = new OpenApiSchema |
| | 6 | 398 | | { |
| | 6 | 399 | | Reference = new OpenApiReference |
| | 6 | 400 | | { |
| | 6 | 401 | | Type = ReferenceType.Schema, |
| | 6 | 402 | | Id = dataContractAttribute.Name |
| | 6 | 403 | | } |
| | 6 | 404 | | }; |
| | | 405 | | } |
| | | 406 | | else |
| | | 407 | | { |
| | 34 | 408 | | schemaSchema = new OpenApiSchema |
| | 34 | 409 | | { |
| | 34 | 410 | | Type = GetType(type) |
| | 34 | 411 | | }; |
| | | 412 | | } |
| | | 413 | | |
| | 42 | 414 | | OpenApiResponse response = new OpenApiResponse |
| | 42 | 415 | | { |
| | 42 | 416 | | Description = responseAttribute?.Description |
| | 42 | 417 | | }; |
| | | 418 | | |
| | 42 | 419 | | if (responseAttribute?.ContentTypes != null) |
| | | 420 | | { |
| | 8 | 421 | | response.Content = responseAttribute.ContentTypes.ToDictionary(contentType => contentType, _ => new Open |
| | | 422 | | } |
| | 40 | 423 | | else if (schemaSchema?.Reference != null) |
| | | 424 | | { |
| | 15 | 425 | | response.Content = defaultContentType.GetContentTypes(true).ToDictionary(contentType => contentType, _ = |
| | | 426 | | } |
| | | 427 | | |
| | 42 | 428 | | int statusCode = responseAttribute?.StatusCode == null ? 200 : (int)responseAttribute.StatusCode; |
| | | 429 | | |
| | 42 | 430 | | operation.Responses.Add(statusCode.ToString(CultureInfo.InvariantCulture), response); |
| | 42 | 431 | | } |
| | | 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 | | { |
| | 136 | 452 | | foreach (ParameterInfo parameter in method.GetParameters()) |
| | | 453 | | { |
| | 25 | 454 | | if (operation.Parameters == null) |
| | | 455 | | { |
| | 0 | 456 | | operation.Parameters = new List<OpenApiParameter>(); |
| | | 457 | | } |
| | | 458 | | |
| | 25 | 459 | | OpenApiParameterAttribute attribute = parameter.GetCustomAttribute<OpenApiParameterAttribute>(); |
| | | 460 | | |
| | 25 | 461 | | bool isHidden = false; |
| | 52 | 462 | | foreach (OpenApiTagAttribute tagAttribute in parameter.GetCustomAttributes<OpenApiTagAttribute>()) |
| | | 463 | | { |
| | 1 | 464 | | if (tagsToHide.Contains(tagAttribute.Tag)) |
| | | 465 | | { |
| | 1 | 466 | | isHidden = true; |
| | | 467 | | } |
| | | 468 | | } |
| | | 469 | | |
| | 25 | 470 | | OpenApiHiddenAttribute hiddenAttribute = parameter.GetCustomAttribute<OpenApiHiddenAttribute>(); |
| | | 471 | | |
| | 25 | 472 | | if (isHidden || hiddenAttribute != null) |
| | | 473 | | { |
| | | 474 | | continue; |
| | | 475 | | } |
| | | 476 | | |
| | 23 | 477 | | UriTemplate uriTemplate = new UriTemplate(uriTemplateRaw); |
| | 23 | 478 | | ParameterLocation? parameterLocation = null; |
| | 26 | 479 | | if (uriTemplate.PathSegmentVariableNames.Any(variableName => string.Equals(variableName, parameter.Name, |
| | | 480 | | { |
| | 2 | 481 | | parameterLocation = ParameterLocation.Path; |
| | | 482 | | } |
| | 25 | 483 | | else if (uriTemplate.QueryValueVariableNames.Any(variableName => string.Equals(variableName, parameter.N |
| | | 484 | | { |
| | 3 | 485 | | parameterLocation = ParameterLocation.Query; |
| | | 486 | | } |
| | | 487 | | |
| | 23 | 488 | | Dictionary<string, StringValues> queryString = QueryHelpers.ParseQuery(new Uri("http://microsoft.com" + |
| | 23 | 489 | | string name = parameter.Name; |
| | 23 | 490 | | if (parameterLocation == ParameterLocation.Query && queryString.ContainsValue("{" + parameter.Name + "}" |
| | | 491 | | { |
| | 7 | 492 | | KeyValuePair<string, StringValues> queryStringParameter = queryString.First(kvp => kvp.Value == "{" |
| | 3 | 493 | | name = queryStringParameter.Key; |
| | | 494 | | } |
| | | 495 | | |
| | 23 | 496 | | DataContractAttribute dataContractAttribute = parameter.ParameterType.GetCustomAttribute<DataContractAtt |
| | 23 | 497 | | if (parameterLocation == null) |
| | | 498 | | { |
| | | 499 | | OpenApiMediaType content; |
| | | 500 | | |
| | 18 | 501 | | if (!string.IsNullOrEmpty(dataContractAttribute?.Name)) |
| | | 502 | | { |
| | 17 | 503 | | PopulateOpenApiSchema(document, parameter.ParameterType, tagsToHide, nsManager); |
| | | 504 | | |
| | 17 | 505 | | content = new OpenApiMediaType |
| | 17 | 506 | | { |
| | 17 | 507 | | Schema = new OpenApiSchema |
| | 17 | 508 | | { |
| | 17 | 509 | | Reference = new OpenApiReference |
| | 17 | 510 | | { |
| | 17 | 511 | | Type = ReferenceType.Schema, |
| | 17 | 512 | | Id = dataContractAttribute.Name |
| | 17 | 513 | | } |
| | 17 | 514 | | } |
| | 17 | 515 | | }; |
| | | 516 | | } |
| | | 517 | | else |
| | | 518 | | { |
| | 1 | 519 | | content = new OpenApiMediaType |
| | 1 | 520 | | { |
| | 1 | 521 | | Schema = new OpenApiSchema |
| | 1 | 522 | | { |
| | 1 | 523 | | Type = GetType(parameter.ParameterType) |
| | 1 | 524 | | } |
| | 1 | 525 | | }; |
| | | 526 | | } |
| | | 527 | | |
| | 18 | 528 | | if (attribute?.ContentTypes != null) |
| | | 529 | | { |
| | 16 | 530 | | operation.RequestBody = new OpenApiRequestBody |
| | 16 | 531 | | { |
| | 32 | 532 | | Content = attribute.ContentTypes.ToDictionary(contentType => contentType, _ => content), |
| | 16 | 533 | | Required = !parameter.IsOptional, |
| | 16 | 534 | | Description = attribute?.Description |
| | 16 | 535 | | }; |
| | | 536 | | } |
| | | 537 | | else |
| | | 538 | | { |
| | 2 | 539 | | operation.RequestBody = new OpenApiRequestBody |
| | 2 | 540 | | { |
| | 16 | 541 | | Content = defaultContentType.GetContentTypes(false).ToDictionary(contentType => contentType, |
| | 2 | 542 | | Required = !parameter.IsOptional, |
| | 2 | 543 | | Description = attribute?.Description |
| | 2 | 544 | | }; |
| | | 545 | | } |
| | | 546 | | } |
| | | 547 | | else |
| | | 548 | | { |
| | 5 | 549 | | operation.Parameters.Add(new OpenApiParameter |
| | 5 | 550 | | { |
| | 5 | 551 | | Name = name, |
| | 5 | 552 | | Description = attribute?.Description, |
| | 5 | 553 | | Schema = new OpenApiSchema |
| | 5 | 554 | | { |
| | 5 | 555 | | Type = GetType(parameter.ParameterType) |
| | 5 | 556 | | }, |
| | 5 | 557 | | In = parameterLocation, |
| | 5 | 558 | | Required = !parameter.IsOptional || parameterLocation == ParameterLocation.Path |
| | 5 | 559 | | }); |
| | | 560 | | } |
| | | 561 | | } |
| | 43 | 562 | | } |
| | | 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 | | { |
| | 94 | 572 | | foreach (OpenApiTagAttribute attribute in method.GetCustomAttributes<OpenApiTagAttribute>()) |
| | | 573 | | { |
| | 4 | 574 | | if (operation.Tags == null) |
| | | 575 | | { |
| | 0 | 576 | | operation.Tags = new List<OpenApiTag>(); |
| | | 577 | | } |
| | | 578 | | |
| | 4 | 579 | | operation.Tags.Add(new OpenApiTag { Name = attribute.Tag }); |
| | | 580 | | |
| | 6 | 581 | | if (!document.Tags.Any(existingTag => existingTag.Name == attribute.Tag)) |
| | | 582 | | { |
| | 4 | 583 | | document.Tags.Add(new OpenApiTag |
| | 4 | 584 | | { |
| | 4 | 585 | | Name = attribute.Tag |
| | 4 | 586 | | }); |
| | | 587 | | } |
| | | 588 | | } |
| | 43 | 589 | | } |
| | | 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 | | { |
| | 43 | 598 | | OpenApiOperationAttribute operationSummaryAttribute = method.GetCustomAttribute<OpenApiOperationAttribute>() |
| | 43 | 599 | | if (operationSummaryAttribute != null) |
| | | 600 | | { |
| | 1 | 601 | | operation.Summary = operationSummaryAttribute.Summary; |
| | 1 | 602 | | operation.Description = operationSummaryAttribute.Description; |
| | | 603 | | } |
| | 43 | 604 | | } |
| | | 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 | | { |
| | 37 | 617 | | DataContractAttribute parentDataContractAttribute = parentType?.GetCustomAttribute<DataContractAttribute |
| | 37 | 618 | | DataContractAttribute dataContractAttribute = type?.GetCustomAttribute<DataContractAttribute>(); |
| | | 619 | | |
| | 37 | 620 | | if (dataContractAttribute?.Name == null) |
| | | 621 | | { |
| | 9 | 622 | | return false; |
| | | 623 | | } |
| | | 624 | | |
| | 28 | 625 | | string schemaKey = GetSchemaKey(parentDataContractAttribute, dataContractAttribute, isInArray); |
| | 28 | 626 | | if (seenKeys.Contains(schemaKey)) |
| | | 627 | | { |
| | 0 | 628 | | return false; |
| | | 629 | | } |
| | | 630 | | else |
| | | 631 | | { |
| | 28 | 632 | | seenKeys.Add(schemaKey); |
| | | 633 | | } |
| | | 634 | | |
| | 28 | 635 | | return !document.Components.Schemas.ContainsKey(GetSchemaKey(parentDataContractAttribute, dataContractAt |
| | | 636 | | } |
| | | 637 | | |
| | 16 | 638 | | bool IsDataMemberProperty(PropertyInfo property) => property.GetCustomAttribute<DataMemberAttribute>() != nu |
| | | 639 | | |
| | 23 | 640 | | HashSet<string> seenKeys = new HashSet<string>(); |
| | | 641 | | |
| | 23 | 642 | | if (!IsContractAndIsNewContract(null, definition, seenKeys, false)) |
| | | 643 | | { |
| | 4 | 644 | | return; |
| | | 645 | | } |
| | | 646 | | |
| | 19 | 647 | | Queue<(Type parent, Type type, bool isInArray)> queue = new Queue<(Type parent, Type type, bool isInArary)>( |
| | 19 | 648 | | queue.Enqueue((null, definition, false)); |
| | | 649 | | |
| | 43 | 650 | | while (queue.Any()) |
| | | 651 | | { |
| | 24 | 652 | | (Type parent, Type type, bool isInArray) = queue.Dequeue(); |
| | 24 | 653 | | AddTypeToSchemas(document, parent, type, isInArray, tagsToHide, nsManager); |
| | | 654 | | |
| | 70 | 655 | | foreach (PropertyInfo property in type.GetProperties().Where(IsDataMemberProperty)) |
| | | 656 | | { |
| | 11 | 657 | | if (IsContractAndIsNewContract(type, property.PropertyType, seenKeys, false)) |
| | | 658 | | { |
| | 3 | 659 | | queue.Enqueue((type, property.PropertyType, false)); |
| | | 660 | | } |
| | 8 | 661 | | else if ( |
| | 8 | 662 | | property.PropertyType.GetInterface("IEnumerable") != null && |
| | 8 | 663 | | property.PropertyType != typeof(string) && |
| | 8 | 664 | | IsContractAndIsNewContract(type, property.PropertyType.GetGenericArguments().FirstOrDefault(), s |
| | | 665 | | { |
| | 2 | 666 | | queue.Enqueue((type, property.PropertyType.GetGenericArguments().FirstOrDefault(), true)); |
| | | 667 | | } |
| | | 668 | | } |
| | | 669 | | } |
| | 19 | 670 | | } |
| | | 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 | | { |
| | 85 | 681 | | if (parentDataContractAttribute != null && isInArray) |
| | | 682 | | { |
| | 8 | 683 | | return $"{parentDataContractAttribute.Name}-Array-{dataContractAttribute.Name}"; |
| | | 684 | | } |
| | 77 | 685 | | else if (parentDataContractAttribute != null) |
| | | 686 | | { |
| | 12 | 687 | | return $"{parentDataContractAttribute.Name}-{dataContractAttribute.Name}"; |
| | | 688 | | } |
| | | 689 | | else |
| | | 690 | | { |
| | 65 | 691 | | 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 | | { |
| | 24 | 706 | | DataContractAttribute parentDataContractAttribute = parent?.GetCustomAttribute<DataContractAttribute>(); |
| | 24 | 707 | | DataContractAttribute dataContractAttribute = type.GetCustomAttribute<DataContractAttribute>(); |
| | | 708 | | |
| | 24 | 709 | | string parentNs = FindNamespace(parentDataContractAttribute?.Namespace ?? dataContractAttribute?.Namespace, |
| | 24 | 710 | | string parentPrefix = FindPrefix(nsManager, parentNs); |
| | | 711 | | |
| | 24 | 712 | | string ns = FindNamespace(dataContractAttribute?.Namespace, type); |
| | 24 | 713 | | string prefix = FindPrefix(nsManager, ns); |
| | | 714 | | |
| | 24 | 715 | | Dictionary<string, IOpenApiExtension> xmlBlock = isInArray ? |
| | 24 | 716 | | new Dictionary<string, IOpenApiExtension> |
| | 24 | 717 | | { |
| | 24 | 718 | | {"xml", new OpenApiObject |
| | 24 | 719 | | { |
| | 24 | 720 | | {"name", new OpenApiString(dataContractAttribute?.Name ?? type.Name)}, |
| | 24 | 721 | | {"namespace", new OpenApiString(ns)}, |
| | 24 | 722 | | {"prefix", new OpenApiString(prefix)} |
| | 24 | 723 | | } |
| | 24 | 724 | | } |
| | 24 | 725 | | } : |
| | 24 | 726 | | new Dictionary<string, IOpenApiExtension> |
| | 24 | 727 | | { |
| | 24 | 728 | | {"xml", new OpenApiObject |
| | 24 | 729 | | { |
| | 24 | 730 | | {"namespace", new OpenApiString(parentNs)}, |
| | 24 | 731 | | {"prefix", new OpenApiString(parentPrefix)} |
| | 24 | 732 | | } |
| | 24 | 733 | | } |
| | 24 | 734 | | }; |
| | | 735 | | |
| | 24 | 736 | | SortedSet<string> required = new SortedSet<string>(); |
| | 24 | 737 | | OpenApiSchema definitionSchema = new OpenApiSchema |
| | 24 | 738 | | { |
| | 24 | 739 | | Type = "object", |
| | 24 | 740 | | Properties = new Dictionary<string, OpenApiSchema>(), |
| | 24 | 741 | | // The URI type might mangle the namespace so we do this manually. |
| | 24 | 742 | | Extensions = xmlBlock, |
| | 24 | 743 | | }; |
| | | 744 | | |
| | 24 | 745 | | IEnumerable<(PropertyInfo, DataMemberAttribute)> properties = type |
| | 24 | 746 | | .GetProperties() |
| | 16 | 747 | | .Select(property => (Property: property, DataMemberAttribute: property.GetCustomAttribute<DataMemberAttr |
| | 16 | 748 | | .Where(property => property.DataMemberAttribute != null) |
| | 35 | 749 | | .OrderBy(property => property.DataMemberAttribute.Order); |
| | | 750 | | |
| | 70 | 751 | | foreach ((PropertyInfo property, DataMemberAttribute dataMemberAttribute) in properties) |
| | | 752 | | { |
| | 11 | 753 | | OpenApiHiddenAttribute hiddenAttribute = property.GetCustomAttribute<OpenApiHiddenAttribute>(); |
| | 11 | 754 | | if (hiddenAttribute != null) |
| | | 755 | | { |
| | | 756 | | continue; |
| | | 757 | | } |
| | | 758 | | |
| | 10 | 759 | | bool isHidden = false; |
| | 24 | 760 | | foreach (OpenApiTagAttribute tagAttribute in property.GetCustomAttributes<OpenApiTagAttribute>()) |
| | | 761 | | { |
| | 2 | 762 | | if (tagsToHide.Contains(tagAttribute.Tag)) |
| | | 763 | | { |
| | 1 | 764 | | isHidden = true; |
| | | 765 | | } |
| | | 766 | | } |
| | | 767 | | |
| | 10 | 768 | | if (isHidden) |
| | | 769 | | { |
| | | 770 | | continue; |
| | | 771 | | } |
| | | 772 | | |
| | 9 | 773 | | string name = dataMemberAttribute.Name ?? property.Name; |
| | | 774 | | |
| | 9 | 775 | | OpenApiPropertyAttribute memberPropertiesAttribute = property.GetCustomAttribute<OpenApiPropertyAttribut |
| | | 776 | | |
| | 9 | 777 | | IEnumerable<CustomAttributeNamedArgument> memberPropertiesAttributeData = property |
| | 9 | 778 | | .GetCustomAttributesData() |
| | 14 | 779 | | .FirstOrDefault(data => data.AttributeType == typeof(OpenApiPropertyAttribute)) |
| | 9 | 780 | | ?.NamedArguments; |
| | 21 | 781 | | bool maxLengthSet = memberPropertiesAttributeData?.Any(arg => arg.MemberName == "MaxLength") ?? false; |
| | 20 | 782 | | bool minLengthSet = memberPropertiesAttributeData?.Any(arg => arg.MemberName == "MinLength") ?? false; |
| | | 783 | | |
| | 9 | 784 | | if (memberPropertiesAttribute?.IsRequired ?? false) |
| | | 785 | | { |
| | 5 | 786 | | required.Add(name); |
| | | 787 | | } |
| | | 788 | | |
| | 9 | 789 | | DataContractAttribute innerDataContractAttribute = property.PropertyType.GetCustomAttribute<DataContract |
| | 9 | 790 | | if (innerDataContractAttribute?.Name != null) |
| | | 791 | | { |
| | 3 | 792 | | DataContractAttribute innerDataMemberAttribute = property.PropertyType.GetCustomAttribute<DataContra |
| | | 793 | | |
| | 3 | 794 | | definitionSchema.Properties.Add(name, new OpenApiSchema |
| | 3 | 795 | | { |
| | 3 | 796 | | Reference = new OpenApiReference |
| | 3 | 797 | | { |
| | 3 | 798 | | Type = ReferenceType.Schema, |
| | 3 | 799 | | Id = GetSchemaKey(dataContractAttribute, innerDataMemberAttribute, false) |
| | 3 | 800 | | }, |
| | 3 | 801 | | Description = memberPropertiesAttribute?.Description |
| | 3 | 802 | | }); |
| | | 803 | | } |
| | 6 | 804 | | 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. |
| | 3 | 807 | | Type innerType = property.PropertyType.GetGenericArguments().FirstOrDefault(); |
| | 3 | 808 | | if (innerType == null && property.PropertyType.BaseType != null) |
| | | 809 | | { |
| | 0 | 810 | | innerType = property.PropertyType.BaseType.GetGenericArguments().FirstOrDefault(); |
| | | 811 | | } |
| | | 812 | | |
| | 3 | 813 | | if (innerType != null) |
| | | 814 | | { |
| | 3 | 815 | | DataContractAttribute innerDataMemberAttribute = innerType.GetCustomAttribute<DataContractAttrib |
| | 3 | 816 | | if (innerDataMemberAttribute != null) |
| | | 817 | | { |
| | 2 | 818 | | definitionSchema.Properties.Add(name, new OpenApiSchema |
| | 2 | 819 | | { |
| | 2 | 820 | | Type = "array", |
| | 2 | 821 | | Description = memberPropertiesAttribute?.Description, |
| | 2 | 822 | | Items = new OpenApiSchema |
| | 2 | 823 | | { |
| | 2 | 824 | | Reference = new OpenApiReference |
| | 2 | 825 | | { |
| | 2 | 826 | | Type = ReferenceType.Schema, |
| | 2 | 827 | | Id = GetSchemaKey(dataContractAttribute, innerDataMemberAttribute, true), |
| | 2 | 828 | | }, |
| | 2 | 829 | | Xml = new OpenApiXml |
| | 2 | 830 | | { |
| | 2 | 831 | | Namespace = new Uri(ArrayNamespace), |
| | 2 | 832 | | Name = innerDataMemberAttribute.Name, |
| | 2 | 833 | | Prefix = FindPrefix(nsManager, ArrayNamespace) |
| | 2 | 834 | | } |
| | 2 | 835 | | }, |
| | 2 | 836 | | // The URI type might mangle the namespace so we do this manually. |
| | 2 | 837 | | Extensions = new Dictionary<string, IOpenApiExtension> |
| | 2 | 838 | | { |
| | 2 | 839 | | {"xml", new OpenApiObject |
| | 2 | 840 | | { |
| | 2 | 841 | | {"name", new OpenApiString(name) }, |
| | 2 | 842 | | {"namespace", new OpenApiString(parentNs)}, |
| | 2 | 843 | | {"prefix", new OpenApiString(parentPrefix)}, |
| | 2 | 844 | | {"wrapped", new OpenApiBoolean(true) } |
| | 2 | 845 | | } |
| | 2 | 846 | | } |
| | 2 | 847 | | }, |
| | 2 | 848 | | }); |
| | | 849 | | } |
| | | 850 | | else |
| | | 851 | | { |
| | 1 | 852 | | string openApiType = GetType(innerType); |
| | | 853 | | |
| | 1 | 854 | | if (!string.IsNullOrEmpty(openApiType)) |
| | | 855 | | { |
| | 1 | 856 | | definitionSchema.Properties.Add(name, new OpenApiSchema |
| | 1 | 857 | | { |
| | 1 | 858 | | Type = "array", |
| | 1 | 859 | | Description = memberPropertiesAttribute?.Description, |
| | 1 | 860 | | Items = new OpenApiSchema |
| | 1 | 861 | | { |
| | 1 | 862 | | Type = openApiType, |
| | 1 | 863 | | Xml = new OpenApiXml |
| | 1 | 864 | | { |
| | 1 | 865 | | Namespace = new Uri(ArrayNamespace), |
| | 1 | 866 | | Name = innerType.Name.ToLower(), |
| | 1 | 867 | | Prefix = FindPrefix(nsManager, ArrayNamespace) |
| | 1 | 868 | | } |
| | 1 | 869 | | }, |
| | 1 | 870 | | // The URI type might mangle the namespace so we do this manually. |
| | 1 | 871 | | Extensions = new Dictionary<string, IOpenApiExtension> |
| | 1 | 872 | | { |
| | 1 | 873 | | {"xml", new OpenApiObject |
| | 1 | 874 | | { |
| | 1 | 875 | | {"name", new OpenApiString(name) }, |
| | 1 | 876 | | {"namespace", new OpenApiString(parentNs)}, |
| | 1 | 877 | | {"prefix", new OpenApiString(parentPrefix)}, |
| | 1 | 878 | | {"wrapped", new OpenApiBoolean(true) } |
| | 1 | 879 | | } |
| | 1 | 880 | | } |
| | 1 | 881 | | }, |
| | 1 | 882 | | }); ; |
| | | 883 | | } |
| | | 884 | | } |
| | | 885 | | } |
| | | 886 | | } |
| | 3 | 887 | | else if (property.PropertyType.IsEnum) |
| | | 888 | | { |
| | 1 | 889 | | List<IOpenApiAny> enumValues = new List<IOpenApiAny>(); |
| | 6 | 890 | | foreach (object value in Enum.GetValues(property.PropertyType)) |
| | | 891 | | { |
| | 2 | 892 | | enumValues.Add(new OpenApiString(value.ToString())); |
| | | 893 | | } |
| | | 894 | | |
| | 1 | 895 | | definitionSchema.Properties.Add(name, new OpenApiSchema |
| | 1 | 896 | | { |
| | 1 | 897 | | Type = "string", |
| | 1 | 898 | | Description = memberPropertiesAttribute?.Description, |
| | 1 | 899 | | Enum = enumValues, |
| | 1 | 900 | | // The URI type might mangle the namespace so we do this manually. |
| | 1 | 901 | | Extensions = new Dictionary<string, IOpenApiExtension> |
| | 1 | 902 | | { |
| | 1 | 903 | | {"xml", new OpenApiObject |
| | 1 | 904 | | { |
| | 1 | 905 | | {"namespace", new OpenApiString(ns)}, |
| | 1 | 906 | | {"prefix", new OpenApiString(prefix)} |
| | 1 | 907 | | } |
| | 1 | 908 | | } |
| | 1 | 909 | | } |
| | 1 | 910 | | }); |
| | | 911 | | } |
| | | 912 | | else |
| | | 913 | | { |
| | 2 | 914 | | definitionSchema.Properties.Add(name, new OpenApiSchema |
| | 2 | 915 | | { |
| | 2 | 916 | | Type = GetType(property.PropertyType), |
| | 2 | 917 | | Description = memberPropertiesAttribute?.Description, |
| | 2 | 918 | | MinLength = minLengthSet ? memberPropertiesAttribute?.MinLength : null, |
| | 2 | 919 | | MaxLength = maxLengthSet ? memberPropertiesAttribute?.MaxLength : null, |
| | 2 | 920 | | Format = memberPropertiesAttribute?.Format, |
| | 2 | 921 | | // The URI type might mangle the namespace so we do this manually. |
| | 2 | 922 | | Extensions = new Dictionary<string, IOpenApiExtension> |
| | 2 | 923 | | { |
| | 2 | 924 | | {"xml", new OpenApiObject |
| | 2 | 925 | | { |
| | 2 | 926 | | {"namespace", new OpenApiString(ns)}, |
| | 2 | 927 | | {"prefix", new OpenApiString(prefix)} |
| | 2 | 928 | | } |
| | 2 | 929 | | } |
| | 2 | 930 | | } |
| | 2 | 931 | | }); |
| | | 932 | | } |
| | | 933 | | } |
| | | 934 | | |
| | 24 | 935 | | definitionSchema.Required = required.Count > 0 ? required : null; |
| | | 936 | | |
| | 24 | 937 | | document.Components.Schemas.Add(GetSchemaKey(parentDataContractAttribute, dataContractAttribute, isInArray), |
| | 24 | 938 | | } |
| | | 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 | | { |
| | 48 | 948 | | if (ns == null) |
| | | 949 | | { |
| | 48 | 950 | | return $"{DataContractNamespace}{type.Namespace}"; |
| | | 951 | | } |
| | | 952 | | |
| | 0 | 953 | | 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 | | { |
| | 51 | 964 | | string prefix = nsManager.LookupPrefix(ns); |
| | 51 | 965 | | if (!string.IsNullOrEmpty(prefix)) |
| | | 966 | | { |
| | 29 | 967 | | return prefix; |
| | | 968 | | } |
| | | 969 | | |
| | 22 | 970 | | int index = 0; |
| | 22 | 971 | | IEnumerator enumerator = nsManager.GetEnumerator(); |
| | 91 | 972 | | while (enumerator.MoveNext()) |
| | | 973 | | { |
| | 69 | 974 | | index++; |
| | | 975 | | } |
| | 22 | 976 | | index++; |
| | | 977 | | |
| | 22 | 978 | | prefix = $"ns{index}"; |
| | 22 | 979 | | nsManager.AddNamespace(prefix, ns); |
| | 22 | 980 | | 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 | | { |
| | 43 | 990 | | if (type == null) |
| | | 991 | | { |
| | 0 | 992 | | return null; |
| | | 993 | | } |
| | | 994 | | |
| | 43 | 995 | | Type actualType = IsNullable(type) ? Nullable.GetUnderlyingType(type) : type; |
| | | 996 | | |
| | 43 | 997 | | if (actualType == typeof(int) || actualType == typeof(long) || actualType == typeof(short) || actualType == |
| | | 998 | | { |
| | 0 | 999 | | return "integer"; |
| | | 1000 | | } |
| | 43 | 1001 | | else if (actualType == typeof(float) || actualType == typeof(double) || actualType == typeof(decimal)) |
| | | 1002 | | { |
| | 0 | 1003 | | return "number"; |
| | | 1004 | | } |
| | 43 | 1005 | | else if (actualType == typeof(string) || actualType == typeof(DateTime) || actualType == typeof(Stream) || a |
| | | 1006 | | { |
| | 11 | 1007 | | return "string"; |
| | | 1008 | | } |
| | 32 | 1009 | | else if (actualType == typeof(bool)) |
| | | 1010 | | { |
| | 0 | 1011 | | return "boolean"; |
| | | 1012 | | } |
| | | 1013 | | |
| | 32 | 1014 | | 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> |
| | 43 | 1022 | | 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> |
| | 48 | 1032 | | public bool ResponseFormatExplicitlySet { get; set; } |
| | | 1033 | | |
| | | 1034 | | /// <summary> |
| | | 1035 | | /// The format set in the WebGet/WebInvoke attribute for the response. |
| | | 1036 | | /// </summary> |
| | 45 | 1037 | | public WebMessageFormat ResponseAttributeFormat { get; set; } |
| | | 1038 | | |
| | | 1039 | | /// <summary> |
| | | 1040 | | /// The default format in the WebHttpBehavior for the response. |
| | | 1041 | | /// </summary> |
| | 46 | 1042 | | 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> |
| | 2 | 1047 | | public bool RequestFormatExplicitlySet { get; set; } |
| | | 1048 | | |
| | | 1049 | | /// <summary> |
| | | 1050 | | /// The format set in the WebGet/WebInvoke attribute for the request. |
| | | 1051 | | /// </summary> |
| | 0 | 1052 | | 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 | | { |
| | 7 | 1059 | | if (isResponse) |
| | | 1060 | | { |
| | 5 | 1061 | | WebMessageFormat format = ResponseFormatExplicitlySet ? ResponseAttributeFormat : ResponseBehaviorFo |
| | | 1062 | | switch (format) |
| | | 1063 | | { |
| | | 1064 | | case WebMessageFormat.Json: |
| | 3 | 1065 | | return new string[] { "application/json" }; |
| | | 1066 | | case WebMessageFormat.Xml: |
| | 2 | 1067 | | return new string[] { "application/xml" }; |
| | | 1068 | | default: |
| | 0 | 1069 | | return null; |
| | | 1070 | | } |
| | | 1071 | | } |
| | | 1072 | | else |
| | | 1073 | | { |
| | 2 | 1074 | | if (RequestFormatExplicitlySet) |
| | | 1075 | | { |
| | 0 | 1076 | | switch (RequestAttributeFormat) |
| | | 1077 | | { |
| | | 1078 | | case WebMessageFormat.Json: |
| | 0 | 1079 | | return new string[] { "application/json", "text/json" }; |
| | | 1080 | | case WebMessageFormat.Xml: |
| | 0 | 1081 | | return new string[] { "application/xml", "text/xml" }; |
| | | 1082 | | default: |
| | 0 | 1083 | | return null; |
| | | 1084 | | } |
| | | 1085 | | } |
| | | 1086 | | else |
| | | 1087 | | { |
| | 2 | 1088 | | 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> |
| | 181 | 1102 | | public string Method { get; set; } |
| | | 1103 | | |
| | | 1104 | | /// <summary> |
| | | 1105 | | /// Operation URI. |
| | | 1106 | | /// </summary> |
| | 177 | 1107 | | public string UriTemplate { get; set; } |
| | | 1108 | | |
| | | 1109 | | /// <summary> |
| | | 1110 | | /// Whether the response format was explicitly set. |
| | | 1111 | | /// </summary> |
| | 89 | 1112 | | public bool IsResponseFormatSetExplicitly { get; set; } |
| | | 1113 | | |
| | | 1114 | | /// <summary> |
| | | 1115 | | /// The response format. |
| | | 1116 | | /// </summary> |
| | 89 | 1117 | | public WebMessageFormat ResponseFormat { get; set; } |
| | | 1118 | | |
| | | 1119 | | /// <summary> |
| | | 1120 | | /// Whether the request format was explicitly set. |
| | | 1121 | | /// </summary> |
| | 46 | 1122 | | public bool IsRequestFormatSetExplicitly { get; set; } |
| | | 1123 | | |
| | | 1124 | | /// <summary> |
| | | 1125 | | /// The request format. |
| | | 1126 | | /// </summary> |
| | 46 | 1127 | | public WebMessageFormat RequestFormat { get; set; } |
| | | 1128 | | } |
| | | 1129 | | } |
| | | 1130 | | } |